mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-20 21:48:40 +02:00
fix(ffmpeg): use SRT format and explicit DejaVu Sans font for reliable text overlay
This commit is contained in:
1 parent
d752437138
commit
c6bb589b09
1 file changed
+33
-22
+33
-22
@@ -146,8 +146,8 @@ async def generate_tts_with_subs(
|
|||||||
raise Exception(f"All TTS voices failed. Last error: {last_error}")
|
raise Exception(f"All TTS voices failed. Last error: {last_error}")
|
||||||
|
|
||||||
|
|
||||||
def generate_simple_vtt(text: str, vtt_path: Path):
|
def generate_simple_srt(text: str, srt_path: Path):
|
||||||
"""Generate a simple VTT subtitle file that displays the full text."""
|
"""Generate a simple SRT subtitle file that displays the full text."""
|
||||||
# Split text into chunks for better display
|
# Split text into chunks for better display
|
||||||
words = text.split()
|
words = text.split()
|
||||||
chunks = []
|
chunks = []
|
||||||
@@ -155,33 +155,42 @@ def generate_simple_vtt(text: str, vtt_path: Path):
|
|||||||
|
|
||||||
for word in words:
|
for word in words:
|
||||||
current_chunk.append(word)
|
current_chunk.append(word)
|
||||||
if len(current_chunk) >= 3 or word.endswith((".", "!", "?", ":")):
|
if len(current_chunk) >= 5 or word.endswith((".", "!", "?", ":")):
|
||||||
chunks.append(" ".join(current_chunk))
|
chunks.append(" ".join(current_chunk))
|
||||||
current_chunk = []
|
current_chunk = []
|
||||||
|
|
||||||
if current_chunk:
|
if current_chunk:
|
||||||
chunks.append(" ".join(current_chunk))
|
chunks.append(" ".join(current_chunk))
|
||||||
|
|
||||||
# Estimate timing (average speaking rate: ~150 words per minute = 0.4s per word)
|
|
||||||
# Each chunk gets time proportional to its word count
|
# Each chunk gets time proportional to its word count
|
||||||
vtt_content = "WEBVTT\n\n"
|
srt_content = ""
|
||||||
current_time = 0.0
|
current_time = 0.0
|
||||||
|
|
||||||
for i, chunk in enumerate(chunks):
|
for i, chunk in enumerate(chunks):
|
||||||
word_count = len(chunk.split())
|
word_count = len(chunk.split())
|
||||||
duration = word_count * 0.3 # 0.3 seconds per word (faster sync)
|
duration = word_count * 0.4 # 0.4 seconds per word
|
||||||
|
|
||||||
start_time = format_vtt_time(current_time)
|
start_time = format_srt_time(current_time)
|
||||||
end_time = format_vtt_time(current_time + duration)
|
end_time = format_srt_time(current_time + duration)
|
||||||
|
|
||||||
vtt_content += f"{i + 1}\n"
|
srt_content += f"{i + 1}\n"
|
||||||
vtt_content += f"{start_time} --> {end_time}\n"
|
srt_content += f"{start_time} --> {end_time}\n"
|
||||||
vtt_content += f"{chunk}\n\n"
|
srt_content += f"{chunk}\n\n"
|
||||||
|
|
||||||
current_time += duration
|
current_time += duration
|
||||||
|
|
||||||
with open(vtt_path, "w", encoding="utf-8") as f:
|
print(f"📄 Generated SRT content:\n{srt_content}")
|
||||||
f.write(vtt_content)
|
with open(srt_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(srt_content)
|
||||||
|
|
||||||
|
|
||||||
|
def format_srt_time(seconds: float) -> str:
|
||||||
|
"""Format seconds as SRT timestamp (HH:MM:SS,mmm)."""
|
||||||
|
hours = int(seconds // 3600)
|
||||||
|
minutes = int((seconds % 3600) // 60)
|
||||||
|
secs = int(seconds % 60)
|
||||||
|
millis = int((seconds % 1) * 1000)
|
||||||
|
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
|
||||||
|
|
||||||
|
|
||||||
def format_vtt_time(seconds: float) -> str:
|
def format_vtt_time(seconds: float) -> str:
|
||||||
@@ -230,7 +239,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
input_video_path = job_dir / "input.mp4"
|
input_video_path = job_dir / "input.mp4"
|
||||||
input_audio_path = job_dir / "music.mp3"
|
input_audio_path = job_dir / "music.mp3"
|
||||||
tts_audio_path = job_dir / "tts.mp3"
|
tts_audio_path = job_dir / "tts.mp3"
|
||||||
tts_vtt_path = job_dir / "tts.vtt"
|
tts_srt_path = job_dir / "tts.srt"
|
||||||
output_video_path = job_dir / "output.mp4"
|
output_video_path = job_dir / "output.mp4"
|
||||||
|
|
||||||
start_step = time.time()
|
start_step = time.time()
|
||||||
@@ -290,7 +299,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
if tts_clean_text:
|
if tts_clean_text:
|
||||||
print(f"🔊 Generating TTS audio to: {tts_audio_path}")
|
print(f"🔊 Generating TTS audio to: {tts_audio_path}")
|
||||||
await generate_tts_with_subs(
|
await generate_tts_with_subs(
|
||||||
tts_clean_text, voice, tts_audio_path, tts_vtt_path
|
tts_clean_text, voice, tts_audio_path, tts_srt_path
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify files were created
|
# Verify files were created
|
||||||
@@ -299,6 +308,8 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
f"✅ TTS audio generated: {tts_audio_path.stat().st_size} bytes"
|
f"✅ TTS audio generated: {tts_audio_path.stat().st_size} bytes"
|
||||||
)
|
)
|
||||||
has_tts = True
|
has_tts = True
|
||||||
|
# Generate SRT
|
||||||
|
generate_simple_srt(tts_clean_text, tts_srt_path)
|
||||||
else:
|
else:
|
||||||
print(f"❌ TTS audio file missing or empty!")
|
print(f"❌ TTS audio file missing or empty!")
|
||||||
else:
|
else:
|
||||||
@@ -435,12 +446,12 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
if request.text and request.draw_text:
|
if request.text and request.draw_text:
|
||||||
text_filter = ""
|
text_filter = ""
|
||||||
if has_tts:
|
if has_tts:
|
||||||
# Subtitles (TikTok style) using VTT
|
# Subtitles (TikTok style) using SRT
|
||||||
# Force style to look like TikTok/Reels text
|
# Force style to look like TikTok/Reels text
|
||||||
# Use generic 'Sans' which fontconfig should alias to DejaVu or similar
|
# We use DejaVu Sans exactly as listed in fc-list
|
||||||
style = f"FontName=Sans,FontSize={request.font_size},PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=3,Shadow=0,Bold=1,Alignment=2,MarginV=300"
|
style = f"FontName=DejaVu Sans,FontSize={request.font_size},PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=3,Shadow=0,Bold=1,Alignment=2,MarginV=300"
|
||||||
vtt_path_str = str(tts_vtt_path).replace("\\", "/").replace(":", "\\:")
|
srt_path_str = str(tts_srt_path).replace("\\", "/").replace(":", "\\:")
|
||||||
text_filter = f"subtitles='{vtt_path_str}':force_style='{style}'"
|
text_filter = f"subtitles='{srt_path_str}':force_style='{style}'"
|
||||||
else:
|
else:
|
||||||
# Standard Drawtext
|
# Standard Drawtext
|
||||||
# Ensure font path is valid (using FONT_PATH detected earlier)
|
# Ensure font path is valid (using FONT_PATH detected earlier)
|
||||||
@@ -596,7 +607,7 @@ async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
job_dir.mkdir()
|
job_dir.mkdir()
|
||||||
|
|
||||||
tts_audio_path = job_dir / "preview.mp3"
|
tts_audio_path = job_dir / "preview.mp3"
|
||||||
tts_vtt_path = job_dir / "preview.vtt" # Not used but needed for function sig
|
tts_srt_path = job_dir / "preview.srt" # Consistent with rest of app
|
||||||
|
|
||||||
if not request.text:
|
if not request.text:
|
||||||
raise HTTPException(status_code=400, detail="Text required for preview")
|
raise HTTPException(status_code=400, detail="Text required for preview")
|
||||||
@@ -612,7 +623,7 @@ async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
elif not voice or "Neural" not in voice:
|
elif not voice or "Neural" not in voice:
|
||||||
voice = "fr-FR-VivienneMultilingualNeural"
|
voice = "fr-FR-VivienneMultilingualNeural"
|
||||||
|
|
||||||
await generate_tts_with_subs(clean_text, voice, tts_audio_path, tts_vtt_path)
|
await generate_tts_with_subs(clean_text, voice, tts_audio_path, tts_srt_path)
|
||||||
|
|
||||||
if not tts_audio_path.exists():
|
if not tts_audio_path.exists():
|
||||||
raise Exception("TTS generation failed (file missing)")
|
raise Exception("TTS generation failed (file missing)")
|
||||||
|
|||||||
Reference in new issue
Block a user