corrected Spanish VO

This commit is contained in:
badmark 2026-08-16 10:48:10 -06:00
parent e472ef8035
commit 44fa4da187
5 changed files with 85 additions and 25 deletions

View file

@ -47,7 +47,7 @@ pipeline:
# Spanish: es-MX-JorgeNeural, es-MX-DaliaNeural, es-US-AlonsoNeural, es-ES-AlvaroNeural # Spanish: es-MX-JorgeNeural, es-MX-DaliaNeural, es-US-AlonsoNeural, es-ES-AlvaroNeural
voices: voices:
en: "af_heart" en: "af_heart"
es: "es-MX-JorgeNeural" es: "es-US-AlonsoNeural"
# A/B Testing Thumbnails (generates N visual concepts in EN, ES, and Clean) # A/B Testing Thumbnails (generates N visual concepts in EN, ES, and Clean)
thumbnail_count: 3 thumbnail_count: 3

View file

@ -139,20 +139,29 @@ JSON Schema:
"description": desc "description": desc
} }
prompt = f"""You are a professional documentary translator. prompt = f"""You are a master Spanish documentary voiceover narrator and localization expert (National Geographic / Discovery Channel en Español).
Translate the following English YouTube documentary package into fluent, natural Latin-American Spanish for voiceover and YouTube upload. Translate the following English YouTube documentary package into captivating, natural, spoken Latin-American Spanish for voiceover narration and YouTube publishing.
English Data: English Data:
{json.dumps(translation_payload, ensure_ascii=False, indent=2)} {json.dumps(translation_payload, ensure_ascii=False, indent=2)}
Requirements: CRITICAL SPOKEN VOICE REQUIREMENTS:
- "titles_es": 3 catchy Spanish titles matching the English titles 1. Spoken Eloquence & Natural Flow:
- "narrations_es": array of exact same length containing the Spanish voiceover text - Write as if spoken by a native Latin-American documentary orator with rich, gripping cadence.
- "chapters_es": array of exact same length containing short 2-4 word Spanish chapter titles - Use natural spoken Spanish sentence connectors ("Sin embargo,", "A lo largo de los siglos,", "Lo más fascinante es que...", "No obstante,").
- "on_screen_texts_es": array of exact same length containing Spanish translations of the on-screen text labels (keep empty if original is empty) - Avoid rigid word-for-word literal translations from English.
- "description_es": full Spanish translation of the YouTube description with Spanish hashtags 2. Punctuation for Breathing & Timing:
- Output STRICT JSON only with keys: {{"titles_es": [...], "narrations_es": [...], "chapters_es": [...], "on_screen_texts_es": [...], "description_es": "..."}} - Use commas and question marks (¿...?) thoughtfully so the text-to-speech engine pauses with natural human breathing.
- No markdown formatting or extra text.""" 3. Proper Nouns & Accuracy:
- Keep recognized proper names accurate ("Whitechapel", "Londres", "Alejandría", "Göbeklitepe").
4. "titles_es": 3 high-CTR Spanish titles
5. "narrations_es": array of exact same length containing the rich Spanish voiceover text
6. "chapters_es": array of exact same length containing short 2-4 word Spanish chapter titles
7. "on_screen_texts_es": array of exact same length containing Spanish lower-third labels
8. "description_es": full Spanish description with Spanish hashtags
STRICT OUTPUT FORMAT:
Output STRICT JSON only: {{"titles_es": [...], "narrations_es": [...], "chapters_es": [...], "on_screen_texts_es": [...], "description_es": "..."}}"""
for attempt in (1, 2): for attempt in (1, 2):
text = self._chat(pc, prompt, max_tokens=4096) text = self._chat(pc, prompt, max_tokens=4096)
@ -251,20 +260,25 @@ Return ONLY the corrected script as a single valid JSON object starting with {{
] ]
} }
prompt = f"""You are a master Spanish documentary editor and translator. prompt = f"""You are a senior Spanish documentary voiceover director and phonetic editor.
Review and polish this Spanish translation for topic "{topic}". Review, polish, and perfect this Spanish documentary narration for topic "{topic}".
OBJECTIVES: OBJECTIVES FOR 100% NATURAL SPOKEN SPANISH:
1. Fix proper nouns (keep standard names like "Whitechapel", "Jack el Destripador", "Londres", "Alejandría" accurate). 1. Native Cadence & Phrasing:
2. Correct any phonetic spelling mistakes or awkward phrasing. - Eliminate any awkward literal translations or robotic sentence structures.
3. Ensure natural Latin-American documentary narration tone and correct accent marks (tildes). - Refine sentence flow so every line sounds natural, authoritative, and compelling when spoken aloud.
4. Preserve the exact number of segments. 2. Proper Punctuation & Breathing Pauses:
- Ensure natural comma placement to give the voice synthesizer natural breathing pauses.
3. Proper Noun & Pronunciation Accuracy:
- Ensure historical figures, locations, and proper nouns are written in their correct standard Spanish forms (e.g., "Whitechapel", "Jack el Destripador", "Londres", "Alejandría", "Göbeklitepe").
4. Preserve Structure:
- Maintain the exact same number of segments.
Spanish Data: Spanish Data:
{json.dumps(payload, ensure_ascii=False, indent=2)} {json.dumps(payload, ensure_ascii=False, indent=2)}
STRICT OUTPUT FORMAT: STRICT OUTPUT FORMAT:
Return STRICT JSON only with keys: {{"titles_es": [...], "description_es": "...", "segments_es": [{{"chapter_title_es": "...", "text_es": "...", "on_screen_text_es": "..."}}, ...]}}""" Return STRICT JSON only: {{"titles_es": [...], "description_es": "...", "segments_es": [{{"chapter_title_es": "...", "text_es": "...", "on_screen_text_es": "..."}}, ...]}}"""
try: try:
raw_text = self._chat(pc, prompt, max_tokens=8192) raw_text = self._chat(pc, prompt, max_tokens=8192)

View file

@ -4,6 +4,9 @@ Generates crystal-clear narration in English and Spanish with accurate per-segme
""" """
import os import os
import sys
import re
import shutil
import subprocess import subprocess
import logging import logging
import wave import wave
@ -38,8 +41,35 @@ try:
except ImportError: except ImportError:
edge_tts = None edge_tts = None
def _run_edge_tts(text: str, voice: str, out_wav_path: str): def clean_and_format_spanish_for_tts(text: str) -> str:
"""Synthesize high-quality natural neural voiceover directly via module or CLI.""" """
Format Spanish narration text to sound 100% natural when spoken:
- Expands historical abbreviations (a.C., d.C., siglos, etc.)
- Formats numbers, units, and punctuation for natural human breathing pauses.
"""
t = text.strip()
# Normalize historical dates and eras
t = re.sub(r'\b(?:a\.\s*C\.|a\s*C|A\.\s*C\.|A\s*C)\b', 'antes de Cristo', t)
t = re.sub(r'\b(?:d\.\s*C\.|d\s*C|D\.\s*C\.|D\s*C)\b', 'después de Cristo', t)
t = re.sub(r'\bsiglo\s+XXI\b', 'siglo veintiuno', t, flags=re.IGNORECASE)
t = re.sub(r'\bsiglo\s+XX\b', 'siglo veinte', t, flags=re.IGNORECASE)
t = re.sub(r'\bsiglo\s+XIX\b', 'siglo diecinueve', t, flags=re.IGNORECASE)
t = re.sub(r'\bsiglo\s+XVIII\b', 'siglo dieciocho', t, flags=re.IGNORECASE)
t = re.sub(r'\bsiglo\s+XV\b', 'siglo quince', t, flags=re.IGNORECASE)
t = re.sub(r'\bEE\.?\s*UU\.?\b', 'Estados Unidos', t)
t = re.sub(r'\bkm/h\b', 'kilómetros por hora', t)
t = re.sub(r'(\d+)\s*km\b', r'\1 kilómetros', t)
t = re.sub(r'(\d+)\s*m\b', r'\1 metros', t)
t = re.sub(r'(\d+)\s*ha\b', r'\1 hectáreas', t)
# Soften ellipses and replace multiple hyphens with clean commas for natural pausing
t = re.sub(r'\.{3,}', ', ', t)
t = re.sub(r'\s*--+\s*', ', ', t)
t = re.sub(r'\s+', ' ', t)
return t
def _run_edge_tts(text: str, voice: str, out_wav_path: str, rate: str = "-4%"):
"""Synthesize high-quality natural neural voiceover directly via module or CLI with studio mastering."""
global edge_tts global edge_tts
if edge_tts is None: if edge_tts is None:
try: try:
@ -51,28 +81,44 @@ def _run_edge_tts(text: str, voice: str, out_wav_path: str):
except Exception as e: except Exception as e:
logger.warning(f"[TTS] Dynamic edge-tts install failed: {e}") logger.warning(f"[TTS] Dynamic edge-tts install failed: {e}")
# Preprocess Spanish text for maximum naturalness
if voice.startswith("es-") or "Neural" in voice:
spoken_text = clean_and_format_spanish_for_tts(text)
else:
spoken_text = text
tmp_mp3 = out_wav_path.replace(".wav", "_temp.mp3") tmp_mp3 = out_wav_path.replace(".wav", "_temp.mp3")
if edge_tts is not None: if edge_tts is not None:
async def _speak(): async def _speak():
communicate = edge_tts.Communicate(text, voice) communicate = edge_tts.Communicate(spoken_text, voice, rate=rate)
await communicate.save(tmp_mp3) await communicate.save(tmp_mp3)
asyncio.run(_speak()) asyncio.run(_speak())
else: else:
# Fallback to sys.executable -m edge_tts # Fallback to sys.executable -m edge_tts
cmd = [sys.executable, "-m", "edge_tts", "--voice", voice, "--text", text, "--write-media", tmp_mp3] cmd = [sys.executable, "-m", "edge_tts", "--voice", voice, "--rate", rate, "--text", spoken_text, "--write-media", tmp_mp3]
r = subprocess.run(cmd, capture_output=True, text=True) r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0: if r.returncode != 0:
edge_bin = shutil.which("edge-tts") or os.path.expanduser("~/.local/bin/edge-tts") or os.path.expanduser("~/miniconda3/bin/edge-tts") edge_bin = shutil.which("edge-tts") or os.path.expanduser("~/.local/bin/edge-tts") or os.path.expanduser("~/miniconda3/bin/edge-tts")
if edge_bin and os.path.exists(edge_bin): if edge_bin and os.path.exists(edge_bin):
r2 = subprocess.run([edge_bin, "--voice", voice, "--text", text, "--write-media", tmp_mp3], capture_output=True, text=True) r2 = subprocess.run([edge_bin, "--voice", voice, "--rate", rate, "--text", spoken_text, "--write-media", tmp_mp3], capture_output=True, text=True)
if r2.returncode != 0: if r2.returncode != 0:
raise RuntimeError(f"edge-tts failed: {r.stderr or r2.stderr}") raise RuntimeError(f"edge-tts failed: {r.stderr or r2.stderr}")
else: else:
raise RuntimeError(f"edge-tts execution error: {r.stderr}") raise RuntimeError(f"edge-tts execution error: {r.stderr}")
# Broadcast vocal mastering filter (warm low-mid EQ, subtle presence, transparent leveling)
vocal_af = (
"highpass=f=75,"
"equalizer=f=200:t=q:w=1.2:g=1.8,"
"equalizer=f=3500:t=q:w=1.5:g=1.0,"
"compand=attacks=0.03:decays=0.15:points=-80/-80|-40/-30|-20/-14|0/-2:gain=2,"
"volume=1.05"
)
subprocess.run([ subprocess.run([
"ffmpeg", "-y", "-i", tmp_mp3, "ffmpeg", "-y", "-i", tmp_mp3,
"-af", vocal_af,
"-ar", "44100", "-ac", "2", "-c:a", "pcm_s16le", out_wav_path "-ar", "44100", "-ac", "2", "-c:a", "pcm_s16le", out_wav_path
], check=True, capture_output=True) ], check=True, capture_output=True)
if os.path.exists(tmp_mp3): if os.path.exists(tmp_mp3):
@ -81,7 +127,7 @@ def _run_edge_tts(text: str, voice: str, out_wav_path: str):
class TTSGen: class TTSGen:
def __init__(self, pcs: Dict[str, RemotePC], voices: Dict[str, str], tts_port: int = 8003): def __init__(self, pcs: Dict[str, RemotePC], voices: Dict[str, str], tts_port: int = 8003):
self.pcs = pcs self.pcs = pcs
self.voices = voices or {"en": "af_heart", "es": "es-MX-JorgeNeural"} self.voices = voices or {"en": "af_heart", "es": "es-US-AlonsoNeural"}
self.tts_port = tts_port self.tts_port = tts_port
def generate(self, script_data: Dict[str, Any], job_dir: str, lang: str = "en") -> Dict[str, Any]: def generate(self, script_data: Dict[str, Any], job_dir: str, lang: str = "en") -> Dict[str, Any]: