-
-
Save BuyMyMojo/2db85f71fc1e1f5cb58042c6a1784276 to your computer and use it in GitHub Desktop.
Convert audio to a twitter-style rendered video.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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