Skip to content

Instantly share code, notes, and snippets.

@JupyterJones
Created May 23, 2025 19:34
Show Gist options
  • Save JupyterJones/992edb3dfbf94c5259a7fe7722e5da08 to your computer and use it in GitHub Desktop.
Save JupyterJones/992edb3dfbf94c5259a7fe7722e5da08 to your computer and use it in GitHub Desktop.
convert a text file to a scroling mp4 that matches length of mp3
from PIL import Image, ImageDraw, ImageFont
import subprocess
import textwrap
from icecream import ic
#--------------------
def get_mp3_duration(mp3_path):
cmd = [
"ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
mp3_path
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
try:
duration = float(result.stdout.strip())
ic(f"MP3 duration: {duration:.2f} seconds")
return duration
except ValueError:
ic("Could not extract duration from mp3")
return 5.0
#--------------------
def text_to_long_image(text_file_path, output_image_path, font_path=None, font_size=24, max_line_width=1000, line_spacing=8):
ic("Generating long image from text")
with open(text_file_path, 'r', encoding='utf-8') as f:
text = f.read()
ic(f"Text length: {len(text)} characters")
if not font_path:
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
font = ImageFont.truetype(font_path, font_size)
lines = []
for paragraph in text.split('\n'):
if paragraph.strip() == '':
lines.append('')
continue
wrapped = textwrap.wrap(paragraph, width=100)
for line in wrapped:
while font.getlength(line) > max_line_width:
cut = len(line)
low, high = 0, cut
while low < high:
mid = (low + high) // 2
if font.getlength(line[:mid]) <= max_line_width:
low = mid + 1
else:
high = mid
cut = low - 1 if low > 0 else 1
lines.append(line[:cut])
line = line[cut:]
lines.append(line)
line_height = font.getbbox("A")[3] + line_spacing
img_height = line_height * len(lines) + 20
img_width = max_line_width + 40
ic(f"Image size: {img_width}x{img_height}")
img = Image.new("RGB", (img_width, img_height), color="white")
draw = ImageDraw.Draw(img)
y = 10
for line in lines:
draw.text((20, y), line, fill="black", font=font)
y += line_height
img.save(output_image_path)
ic(f"Saved long image: {output_image_path}")
#--------------------
def scroll_image_to_video(image_path, output_video_path, mp3_path, width=512, height=768, scroll_speed_factor=1.0):
img = Image.open(image_path)
img_width, img_height = img.size
ic(f"Image dimensions: {img_width}x{img_height}")
scroll_distance = img_height - height
ic(f"Scroll distance: {scroll_distance} pixels")
duration = get_mp3_duration(mp3_path)
if scroll_distance <= 0:
ic("No scrolling needed")
scroll_filter = f"scale={width}:{height}"
else:
scroll_speed = (scroll_distance / duration) * scroll_speed_factor
ic(f"Scroll speed: {scroll_speed:.2f} px/sec")
scroll_filter = (
f"scale={width}:-1,"
f"crop={width}:{height}:0:y='min(t*{scroll_speed},{scroll_distance})'"
)
cmd = [
"ffmpeg",
"-y",
"-loop", "1",
"-i", image_path,
"-i", mp3_path,
"-vf", scroll_filter,
"-t", f"{duration}",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-c:a", "aac",
"-b:a", "192k",
"-shortest",
"-movflags", "+faststart",
output_video_path
]
ic("Running ffmpeg command:")
ic(' '.join(cmd))
subprocess.run(cmd, check=True)
ic(f"Video created: {output_video_path}")
#--------------------
if __name__ == "__main__":
text_file = "short.txt"
long_image = "long_text.png"
output_video = "scroll_synced.mp4"
mp3_audio = "short.mp3"
# Generate the long image from the text file
text_to_long_image(text_file, long_image)
# Create the scrolling video synchronized with the MP3 file
#scroll_image_to_video(long_image, output_video, mp3_audio)
# scroll_speed_factor adjust scrolling speed + 1 is faster example: +1.3 | -float is slower example: -.5
scroll_image_to_video(long_image, output_video, mp3_audio, scroll_speed_factor=.5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment