Skip to content

Instantly share code, notes, and snippets.

@BuyMyMojo
Forked from espeon/a2v.sh
Last active May 7, 2025 00:02
Show Gist options
  • Save BuyMyMojo/2db85f71fc1e1f5cb58042c6a1784276 to your computer and use it in GitHub Desktop.
Save BuyMyMojo/2db85f71fc1e1f5cb58042c6a1784276 to your computer and use it in GitHub Desktop.
Convert audio to a twitter-style rendered video.
#!/bin/bash
# --- Configuration ---
RES=3 # Resolution multiplier. 2 = 720p, 3 = 1080p, 4 = 1440p, 5 = 1800p, 6 = 2160p(4K)
PFP_SIZE=$((200 * $RES))
TEXT_SIZE=$((20 * $RES))
VIDEO_WIDTH=$((640 * $RES))
VIDEO_HEIGHT=$((360 * $RES))
VIDEO_FPS=50 # High FPS PAL is funny...
ENCODE_CODEC="libx264"
ENCODE_PRESET="veryslow"
ENCODE_CRF=22
AUDIO_CODEC="libopus"
AUDIO_BITRATE="320k"
WAVEFORM_STYLE="cline" # Options: cline, p2p, line, point
FONT_FILE="/home/buymymojo/.local/share/fonts/TX-02 2.002/TX-02-Regular.ttf"
# COLOR variable will be set dynamically below
DEFAULT_COLOR="#e74b4b" # Fallback background color
TEXT_COLOR="#ffffffCC" # Text color (white with some transparency)
DIS_FROM_BOT=$((40 * $RES)) # Distance of text/speaker from bottom
DID="did:plc:bzrn33tcfgjxnsanvg6py3xn" # Bluesky DID for profile picture
# --- Script Logic ---
# Exit immediately if a command exits with a non-zero status.
set -e
# Check if an audio file argument was provided
if [ -z "$1" ]; then
echo "Usage: $0 <audio_file>"
echo "Requires: ffmpeg, ffprobe, curl, jq, convert (ImageMagick)"
exit 1
fi
AUDIO_FILE="$1"
# Check if the input audio file exists
if [ ! -f "$AUDIO_FILE" ]; then
echo "Error: Audio file not found: $AUDIO_FILE"
exit 1
fi
# Check if required commands exist
for cmd in ffmpeg ffprobe curl jq convert; do
if ! command -v $cmd &> /dev/null; then
echo "Error: Required command '$cmd' not found."
if [ "$cmd" == "convert" ]; then echo "Hint: Install ImageMagick."; fi
exit 1
fi
done
echo "Fetching profile picture..."
# Fetch the profile picture URL
PROFILE_JSON=$(curl -fsSL --connect-timeout 5 --max-time 10 "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=$DID")
USER_HANDLE="@$( echo $PROFILE_JSON | jq -r '.handle')"
PFP_URL=$( echo $PROFILE_JSON | jq -r '.avatar')
if [ -z "$PFP_URL" ] || [ "$PFP_URL" == "null" ]; then
echo "Error: Failed to retrieve profile picture URL for DID $DID."
exit 1
fi
echo "Using PFP URL: $PFP_URL"
# --- Calculate Accent Color ---
echo "Calculating accent color from profile picture..."
ACCENT_COLOR=""
# Try using the URL directly with ImageMagick first
ACCENT_COLOR=$(convert "$PFP_URL" -alpha remove -alpha off -resize 1x1\! txt:- 2>/dev/null | grep -Eo '#[0-9a-fA-F]{6}' | head -n 1 || true)
# If direct URL failed or produced no color, download to temp file and try again
if [ -z "$ACCENT_COLOR" ]; then
echo "ImageMagick failed with URL or no color found, attempting temporary download..."
TEMP_PFP_FILE=$(mktemp --suffix=.img) # Create secure temp file
if curl -fsSL --connect-timeout 5 --max-time 15 -o "$TEMP_PFP_FILE" "$PFP_URL"; then
ACCENT_COLOR=$(convert "$TEMP_PFP_FILE" -alpha remove -alpha off -resize 1x1\! txt:- 2>/dev/null | grep -Eo '#[0-9a-fA-F]{6}' | head -n 1 || true)
rm "$TEMP_PFP_FILE" # Clean up temp file
else
echo "Warning: Failed to download PFP to temporary file: $PFP_URL"
# Ensure temp file is removed even if curl failed
if [ -f "$TEMP_PFP_FILE" ]; then rm "$TEMP_PFP_FILE"; fi
fi
fi
# Set the final color, using default if calculation failed
if [ -z "$ACCENT_COLOR" ]; then
echo "Warning: Could not determine accent color from PFP. Using default: $DEFAULT_COLOR"
COLOR="$DEFAULT_COLOR"
else
COLOR="$ACCENT_COLOR"
echo "Using calculated accent color: $COLOR"
fi
# --- End Accent Color Calculation ---
echo "Calculating audio duration..."
# Get audio duration (use quotes around "$AUDIO_FILE")
AUDIO_SECS=$(ffprobe -v error -i "$AUDIO_FILE" -show_entries format=duration -of default=noprint_wrappers=1:nokey=1)
if [ -z "$AUDIO_SECS" ]; then
echo "Error: Could not determine audio duration for $AUDIO_FILE"
exit 1
fi
# Handle potential floating point duration for comparisons if needed
AUDIO_SECS_INT=$(printf "%.0f" "$AUDIO_SECS")
echo "Audio duration: $AUDIO_SECS seconds (rounded: $AUDIO_SECS_INT)"
OUTPUT_FILE="${AUDIO_FILE%.*}_output.mp4"
echo "Output file will be: $OUTPUT_FILE"
echo "Starting FFmpeg process with visualizer..."
ffmpeg \
-y \
-i "$AUDIO_FILE" \
-f lavfi -i color=c=$COLOR:s=${VIDEO_WIDTH}x${VIDEO_HEIGHT}:d=$AUDIO_SECS \
-i "$PFP_URL" \
-r $VIDEO_FPS \
-filter_complex \
" [0:a]asplit[ao][a1]; \
[a1]equalizer=f=100:t=h:g=-25:w=100,equalizer=f=3000:t=h:g=-10:w=200[a1_bass]; \
[a1_bass]volume=-30dB[a1_q]; \
[a1_q]showwaves=s=${VIDEO_WIDTH}x${VIDEO_HEIGHT}:mode=${WAVEFORM_STYLE}:rate=${VIDEO_FPS}:scale=cbrt:colors=white[wave]; \
[2:v]scale=${PFP_SIZE}:${PFP_SIZE}[profile]; \
[profile]format=rgba,split[main][alpha]; \
[alpha]scale=iw*4:ih*4:flags=neighbor[alpha_up]; \
[alpha_up]geq=lum='255*lte(sqrt(pow(X-W/2,2)+pow(Y-H/2,2)),min(W,H)/2)':a=255[mask_up]; \
[mask_up]scale=iw/4:ih/4:flags=bilinear[smoothmask]; \
[main][smoothmask]alphamerge[profile_rounded]; \
[1:v][wave]blend=all_mode=\'overlay\':all_opacity=0.4[bg]; \
[bg][profile_rounded]overlay=x=(W-w)/2:y=(H-h)/2:enable='between(t,0,${AUDIO_SECS})'[bg2]; \
[bg2]drawtext=text='%{eif\\:max(0,floor(($AUDIO_SECS-t)/60))\\:d\\:1}\\:%{eif\\:max(0,mod(floor($AUDIO_SECS-t)\\,60))\\:d\\:2}':x=$((20 * $RES)):y=H-${DIS_FROM_BOT}:fontfile=${FONT_FILE}:fontsize=$TEXT_SIZE:fontcolor=$TEXT_COLOR:enable='between(t,0,$AUDIO_SECS)'[bg3]; \
[bg3]drawtext=text='$USER_HANDLE':x=(W-text_w)-$((20 * $RES)):y=H-$DIS_FROM_BOT:fontfile=${FONT_FILE}:fontsize=$TEXT_SIZE:fontcolor=$TEXT_COLOR:enable='between(t,0,$AUDIO_SECS)'[outv]" \
-map "[outv]" -map "[ao]" \
-c:v $ENCODE_CODEC -preset $ENCODE_PRESET -crf $ENCODE_CRF \
-c:a $AUDIO_CODEC -b:a $AUDIO_BITRATE \
-shortest \
-movflags +faststart \
"$OUTPUT_FILE"
echo "Visualizer klaar! 🔊✨ Output saved to $OUTPUT_FILE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment