corrected Spanish VO
This commit is contained in:
parent
e472ef8035
commit
44fa4da187
5 changed files with 85 additions and 25 deletions
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -139,20 +139,29 @@ JSON Schema:
|
|||
"description": desc
|
||||
}
|
||||
|
||||
prompt = f"""You are a professional documentary translator.
|
||||
Translate the following English YouTube documentary package into fluent, natural Latin-American Spanish for voiceover and YouTube upload.
|
||||
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 captivating, natural, spoken Latin-American Spanish for voiceover narration and YouTube publishing.
|
||||
|
||||
English Data:
|
||||
{json.dumps(translation_payload, ensure_ascii=False, indent=2)}
|
||||
|
||||
Requirements:
|
||||
- "titles_es": 3 catchy Spanish titles matching the English titles
|
||||
- "narrations_es": array of exact same length containing the Spanish voiceover text
|
||||
- "chapters_es": array of exact same length containing short 2-4 word Spanish chapter titles
|
||||
- "on_screen_texts_es": array of exact same length containing Spanish translations of the on-screen text labels (keep empty if original is empty)
|
||||
- "description_es": full Spanish translation of the YouTube description with Spanish hashtags
|
||||
- Output STRICT JSON only with keys: {{"titles_es": [...], "narrations_es": [...], "chapters_es": [...], "on_screen_texts_es": [...], "description_es": "..."}}
|
||||
- No markdown formatting or extra text."""
|
||||
CRITICAL SPOKEN VOICE REQUIREMENTS:
|
||||
1. Spoken Eloquence & Natural Flow:
|
||||
- Write as if spoken by a native Latin-American documentary orator with rich, gripping cadence.
|
||||
- Use natural spoken Spanish sentence connectors ("Sin embargo,", "A lo largo de los siglos,", "Lo más fascinante es que...", "No obstante,").
|
||||
- Avoid rigid word-for-word literal translations from English.
|
||||
2. Punctuation for Breathing & Timing:
|
||||
- Use commas and question marks (¿...?) thoughtfully so the text-to-speech engine pauses with natural human breathing.
|
||||
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):
|
||||
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.
|
||||
Review and polish this Spanish translation for topic "{topic}".
|
||||
prompt = f"""You are a senior Spanish documentary voiceover director and phonetic editor.
|
||||
Review, polish, and perfect this Spanish documentary narration for topic "{topic}".
|
||||
|
||||
OBJECTIVES:
|
||||
1. Fix proper nouns (keep standard names like "Whitechapel", "Jack el Destripador", "Londres", "Alejandría" accurate).
|
||||
2. Correct any phonetic spelling mistakes or awkward phrasing.
|
||||
3. Ensure natural Latin-American documentary narration tone and correct accent marks (tildes).
|
||||
4. Preserve the exact number of segments.
|
||||
OBJECTIVES FOR 100% NATURAL SPOKEN SPANISH:
|
||||
1. Native Cadence & Phrasing:
|
||||
- Eliminate any awkward literal translations or robotic sentence structures.
|
||||
- Refine sentence flow so every line sounds natural, authoritative, and compelling when spoken aloud.
|
||||
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:
|
||||
{json.dumps(payload, ensure_ascii=False, indent=2)}
|
||||
|
||||
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:
|
||||
raw_text = self._chat(pc, prompt, max_tokens=8192)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ Generates crystal-clear narration in English and Spanish with accurate per-segme
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import logging
|
||||
import wave
|
||||
|
|
@ -38,8 +41,35 @@ try:
|
|||
except ImportError:
|
||||
edge_tts = None
|
||||
|
||||
def _run_edge_tts(text: str, voice: str, out_wav_path: str):
|
||||
"""Synthesize high-quality natural neural voiceover directly via module or CLI."""
|
||||
def clean_and_format_spanish_for_tts(text: str) -> 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]:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue