From 44fa4da18779c2e3e434738aee5b17edc3b7cab6 Mon Sep 17 00:00:00 2001 From: badmark Date: Sun, 16 Aug 2026 10:48:10 -0600 Subject: [PATCH] corrected Spanish VO --- factory_config.yaml | 2 +- .../__pycache__/script_gen.cpython-314.pyc | Bin 22560 -> 23616 bytes pipeline/__pycache__/tts_gen.cpython-314.pyc | Bin 12032 -> 14877 bytes pipeline/script_gen.py | 50 +++++++++------ pipeline/tts_gen.py | 58 ++++++++++++++++-- 5 files changed, 85 insertions(+), 25 deletions(-) diff --git a/factory_config.yaml b/factory_config.yaml index bd34350..5c47e6e 100644 --- a/factory_config.yaml +++ b/factory_config.yaml @@ -47,7 +47,7 @@ pipeline: # Spanish: es-MX-JorgeNeural, es-MX-DaliaNeural, es-US-AlonsoNeural, es-ES-AlvaroNeural voices: en: "af_heart" - es: "es-MX-JorgeNeural" + es: "es-US-AlonsoNeural" # A/B Testing Thumbnails (generates N visual concepts in EN, ES, and Clean) thumbnail_count: 3 diff --git a/pipeline/__pycache__/script_gen.cpython-314.pyc b/pipeline/__pycache__/script_gen.cpython-314.pyc index 39b4585c24a027bbefcafb9a9c3eae190eb01cbe..8e68608c96526d536a3a392f6f7e56b453dcc01d 100644 GIT binary patch delta 2160 zcmZuzO>7%Q6!yAJNG*}lK!4Om?ZZ-2r?odpQz%L+HICyXZc@j!ousWI((!t{yUluM zJG&bkU$*Z8XzL=jW@e?iYhF5XU6mQ z-uJ%mTfcKty7QUTd93rSD0d*1^@qfOamrLqEKOIS4@Pxc5eFr;cion<2{bci4h0eVvE%DUhLWH<&NxZ-Y@PG0bFEm(JfK1+>)W&8t(M2 z9ywZfzuz7TMIxzurkF`3r=XCVo=j(9b~=+vLq2_eCX-K}OJ|FPp~!#?1^)&}8|G%;T3?Wkxb{k^Rf{tKUON6T| z9*sjZiFE)&453W1Z-cF?b!`3FDF)TmcWtN;ThUE|2LiPLGnHjo?9MV^CEF1b@n~-| zrX&`;jA5(ztTA3rR2WZ)4j71nK*r(3v<<)1cs6HMNSmstfgCw5L(Z+6isSmzfO}zt zQ{ssFAh<=nik&8`B722Zs|c(pD8^;`Y^oBzh{=9^8^b))3y#KIRdXt?VJw5qa7@9W z*PMjI61GLDq9LDD-9#!7*Tw|vKJZNzf#z0;38hvLa%2!=~EHMwL_)EV;7W zjBAoo3piDnYU(Sr3|6q(Bvm||_-hf#SaeQ9KopHw)DUP+F|*969gSbWx)}5d=EPS& zB4TQM^^+1^6!=+G0DVG+sG~auwHI*15DbE*t6Cye%p)U%`UlgImvCjEJLJ80K9=YZ z=?hYMeX)Jvmt}7vHXlAId+){4yOFV!ClG5sYP> zgqbQYLtEPJ2!t~4$ly%UzsdixWhdl(;&UJGr8P6_+L$E|(q zqPe&IUSTaibN9k`o&I=jp%k?0;+nzMt~&P(=Yi_tmd4wsKW=;OEz~#=j?xoy(3$t@ z$exKOepJ@6NG(6IG-gr<{f3rx{7sJJ>5@)saj=-6X56P37QKR7kV3lcEsX5;zDPyt zf};+zaBI|k5DtZ+awIZ6ayFeRW@gg`7@N++K!5*X$R>+3`Q(a=GWK>S$#D2o`v>|v;go&7F9JR$7+0s+}DM4X0oRyeJWy=F>hr6gLWLHz9d0G*K z-&#jo}Yp{AiJ2Zjs8U~V}Ob3px^ z`$W<`{}FV~7hF>|Y176CTNeh(2FoA{t)iD9#Gne01b_V+g1v0wpl+kc#LFeH-7TRu z)%>(x1-P7<7?2)V%v0iRMu7!J7U=knf{LyRf^-yPCk~X?S;r2K8zp?+u z_&bMhN`3-*orTs7H4&m$o delta 1154 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 str: + """ + 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 if edge_tts is None: try: @@ -51,28 +81,44 @@ def _run_edge_tts(text: str, voice: str, out_wav_path: str): except Exception as 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") if edge_tts is not None: async def _speak(): - communicate = edge_tts.Communicate(text, voice) + communicate = edge_tts.Communicate(spoken_text, voice, rate=rate) await communicate.save(tmp_mp3) asyncio.run(_speak()) else: # 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) 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") 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: raise RuntimeError(f"edge-tts failed: {r.stderr or r2.stderr}") else: 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([ "ffmpeg", "-y", "-i", tmp_mp3, + "-af", vocal_af, "-ar", "44100", "-ac", "2", "-c:a", "pcm_s16le", out_wav_path ], check=True, capture_output=True) if os.path.exists(tmp_mp3): @@ -81,7 +127,7 @@ def _run_edge_tts(text: str, voice: str, out_wav_path: str): class TTSGen: def __init__(self, pcs: Dict[str, RemotePC], voices: Dict[str, str], tts_port: int = 8003): 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 def generate(self, script_data: Dict[str, Any], job_dir: str, lang: str = "en") -> Dict[str, Any]: