From 5e168ad74b01aa4f6e66e07fa35b1f66b5ffbf69 Mon Sep 17 00:00:00 2001 From: badmark Date: Sun, 16 Aug 2026 11:26:57 -0600 Subject: [PATCH] corrected pronounciation --- .../dependency_manager.cpython-314.pyc | Bin 8748 -> 9007 bytes .../__pycache__/script_gen.cpython-314.pyc | Bin 23616 -> 27169 bytes pipeline/__pycache__/tts_gen.cpython-314.pyc | Bin 14877 -> 16231 bytes .../__pycache__/visual_gen.cpython-314.pyc | Bin 47799 -> 51611 bytes pipeline/dependency_manager.py | 26 +++-- pipeline/script_gen.py | 50 +++++++++- pipeline/tts_gen.py | 33 +++++-- pipeline/visual_gen.py | 92 +++++++++++++----- 8 files changed, 161 insertions(+), 40 deletions(-) diff --git a/pipeline/__pycache__/dependency_manager.cpython-314.pyc b/pipeline/__pycache__/dependency_manager.cpython-314.pyc index ec6aa43374787ce5c29e9421bf415dfeea677b32..9d87033e542a3d84924199ceae276382fb224003 100644 GIT binary patch delta 1223 zcmZ`(TTC2P7(Qp`vNL;QfL(Uk8?!D_7FiS|E`^emT9(*dQCDe5TC*8;$L-2qre|ig z5EIhGr0`HlJP-0>;)~Hnl2-RYANpWYZAdg`OhD{}_+;Xv5J@zxM$Z8hYvP&AfBy4* z|M~y#TyoaW-b{NQ^SB*=H0R#cSKXZ-d)C;F7;N1G#H_Qsw7bn7jPPwXWIN3?@3FlQ zU$94Uj1SO0n6u}EoS1Xw90UA$*iVxkHA?p#(XO2Hzw`j3vFBL{q=x~}xDdcUcw#;- z2M<1XiapGt@fTfq*B$YDhT$R71-d{E#(@Z;fN+u&fh~B@o`4c57>l(IF~^ZPE)HaH zQTSUD4~l;L5#Ps7!b}-&^GQDkK$s+qfXpWVl))Gi0cDtI!ynkUM;HCckUI|82Fkn2D|YcAwDs%=zG4R zD!MA8Moq3NwT4ouER4#FV{)=8Yh_I?*K~tm^8XaWlIX>cie{&D6qqm6t0YfRx0ph; zXhDSk6cdgo$r`)KuNU!<3QX^_uy zc`4z$ugA&i-|d)`ZFdG952ab#mvT4_xw`;T%lgt0?rs z@*E11$<6k*?_e-RJd1B=Ma59>Gs(-B6C4^R7)oKr`^HR+x{pvp>mj=3=masApj8y( zl{%_g4sxEz&|fbbmrxI-*h*P9ET*IyC_&@{(Vy#dws>d=duBgs6 zN@OK3;y;1`ID|VwC%Bi%{DtP3&>V!lhQ+P=p$sBBnt*)a9nt{r*?3H?{RIZg! wl#WEQ8(9LV=ul>u#+|0-VG*c+ZLu>BjWF~9(6{k)>x-at^fc4 delta 984 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 diff --git a/pipeline/__pycache__/tts_gen.cpython-314.pyc b/pipeline/__pycache__/tts_gen.cpython-314.pyc index e987689d58de84cda110a95c637846f5896602e5..dd0c18a2e635435c11948d1236640feb1f8a8d43 100644 GIT binary patch delta 3726 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 diff --git a/pipeline/__pycache__/visual_gen.cpython-314.pyc b/pipeline/__pycache__/visual_gen.cpython-314.pyc index de54692547efa51136005d10be364ebe5d64a029..89905d152e676418314e4b3cb2318a42907317d9 100644 GIT binary patch delta 11686 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 delta 8075 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 bool: return False logger.info("[DEPS] Automatically installing missing Python packages via pip...") - try: - cmd = [sys.executable, "-m", "pip", "install", "--upgrade"] + missing - r = subprocess.run(cmd, check=True, text=True) - logger.info("[DEPS] All Python packages successfully installed!") - return True - except subprocess.CalledProcessError as e: - logger.error(f"[DEPS] Automatic pip install failed: {e}") - logger.error(f"Please run manually: {sys.executable} -m pip install -r requirements.txt") - return False + install_commands = [ + [sys.executable, "-m", "pip", "install", "--upgrade"] + missing, + [sys.executable, "-m", "pip", "install", "--break-system-packages", "--upgrade"] + missing, + [sys.executable, "-m", "pip", "install", "--user", "--break-system-packages"] + missing, + ] + + for cmd in install_commands: + try: + r = subprocess.run(cmd, check=True, capture_output=True, text=True) + logger.info("[DEPS] All Python packages successfully installed!") + return True + except subprocess.CalledProcessError: + continue + + logger.error(f"[DEPS] Automatic pip install failed.") + logger.error(f"Please run manually: {sys.executable} -m pip install --break-system-packages -r requirements.txt") + return False def check_system_binaries(auto_install: bool = True) -> bool: """Check for required system binaries (ffmpeg, ssh, scp, etc.) and attempt self-install.""" diff --git a/pipeline/script_gen.py b/pipeline/script_gen.py index 36f3060..8aacbe1 100644 --- a/pipeline/script_gen.py +++ b/pipeline/script_gen.py @@ -37,7 +37,7 @@ For EACH segment provide: 1. "chapter_title": Short 2-4 word chapter title (e.g., "The Ancient Mystery", "The Critical Discovery") 2. "text": English spoken narration (rich 2-4 sentence narrative paragraph, natural pacing, compelling storytelling) 3. "visual_prompt": Highly detailed visual scene description for an AI video/image generator (cinematic lighting, camera angle, 8k documentary style) -4. "stock_query": 2-4 word search term to find relevant stock b-roll footage online (e.g., "ancient library books", "desert ruins aerial") +4. "stock_query": 2-4 concrete, high-precision visual search keywords describing physical objects/scenery that exist in stock video libraries (e.g., "ancient temple monoliths", "medieval cathedral altar", "archaeological excavation desert", "prehistoric stone tools", "ancient scrolls manuscript"). Never use abstract concepts, full sentences, or obscure person names. 5. "on_screen_text": Optional 2-5 word lower-third label, key statistic, date, or scene title to display on screen (e.g., "Depth: 11,034m", "Year: 1960", "Pacific Ocean Abyss", or "" if none) 6. "is_historical": boolean (true if this segment depicts past events, ancient civilizations, historical figures, archaeological discoveries, or vintage eras; false if modern/contemporary) 7. "duration_estimate": Estimated duration in seconds ({max(10, duration // num_segments)}s) @@ -300,6 +300,51 @@ Return STRICT JSON only: {{"titles_es": [...], "description_es": "...", "segment return script_data + def _generate_phonetic_pronunciation_lexicon(self, pc: RemotePC, script_data: Dict[str, Any], topic: str) -> Dict[str, Dict[str, str]]: + """ + Scan script for foreign, ancient, archaeological, mythological, or diacritic-heavy words + (e.g., 'Göbeklitepe', 'Derinkuyu', 'Tutankhamun', 'Quetzalcoatl') and look up phonetic respellings + for English and Spanish voice synthesizers. + """ + logger.info("[SCRIPT:PHONETICS] Analyzing and looking up proper noun phonetic pronunciations...") + + segments = script_data.get("script_segments", []) + sample_texts = [seg.get("text", "") for seg in segments[:10]] + [topic] + combined_text = "\n".join(sample_texts) + + prompt = f"""You are an expert linguistic phonetics engine and voice synthesis director. +Analyze the following documentary script for topic "{topic}". + +OBJECTIVE: +Identify any foreign, ancient, archaeological, mythological, scientific, non-standard, or diacritic-heavy words (e.g., "Göbeklitepe", "Derinkuyu", "Tutankhamun", "Quetzalcoatl", "Chichen Itza", "Oppenheimer", "Oumuamua", "Tiahuanaco", "Mohenjo-Daro", etc.) that a Text-to-Speech (TTS) voice synthesizer might struggle to pronounce correctly. + +For EACH identified word, provide: +1. "en": Phonetic respelling for English TTS (using clear syllables, e.g., "Göbeklitepe" -> "Goh-beck-lee Teh-peh", "Chichen Itza" -> "Chee-chen Eet-zah") +2. "es": Phonetic respelling for Spanish TTS (using Spanish phonetic rules, e.g., "Göbeklitepe" -> "Guebekli Tepe", "Stonehenge" -> "Ston-jench") + +If no difficult words exist, return empty objects. + +Script text: +{combined_text} + +STRICT JSON OUTPUT: +{{ + "phonetic_mappings": {{"en": {{"word": "phonetic_respelling_en"}}, "es": {{"word": "phonetic_respelling_es"}}}} +}}""" + + try: + raw_text = self._chat(pc, prompt, max_tokens=2048) + if raw_text: + parsed = self._extract_json(raw_text) + mappings = parsed.get("phonetic_mappings", {}) + if mappings and (mappings.get("en") or mappings.get("es")): + logger.info(f"[SCRIPT:PHONETICS] Discovered {len(mappings.get('en', {}))} phonetic pronunciation overrides.") + return mappings + except Exception as e: + logger.debug(f"[SCRIPT:PHONETICS] Phonetic lookup parse note: {e}") + + return {"en": {}, "es": {}} + def generate(self, topic: str, duration: int) -> Dict[str, Any]: """Generate master bilingual script with secondary LLM verification & proofreading passes.""" logger.info(f"[SCRIPT] Generating script for topic: '{topic}' (~{duration}s)...") @@ -347,6 +392,9 @@ Return STRICT JSON only: {{"titles_es": [...], "description_es": "...", "segment # ── Step 1.7: Secondary LLM Spanish Translation Audit ── data = self._proofread_spanish_translation(pc, data, topic) + + # ── Step 1.8: Phonetic Pronunciation Lookup for Non-Standard Vocabulary ── + data["phonetic_mappings"] = self._generate_phonetic_pronunciation_lexicon(pc, data, topic) logger.info(f"[SCRIPT] Master script successfully created & audited: '{data['title']}' ({len(data.get('script_segments', []))} segments)") return data diff --git a/pipeline/tts_gen.py b/pipeline/tts_gen.py index 6362b5a..f14d43a 100644 --- a/pipeline/tts_gen.py +++ b/pipeline/tts_gen.py @@ -124,6 +124,18 @@ def _run_edge_tts(text: str, voice: str, out_wav_path: str, rate: str = "-4%"): if os.path.exists(tmp_mp3): os.remove(tmp_mp3) +def apply_phonetic_respellings(text: str, mappings: Dict[str, str]) -> str: + """Substitute difficult / non-standard proper nouns with phonetic respellings for TTS audio synthesis.""" + if not mappings: + return text + t = text + for word, phonetic in mappings.items(): + if not word or not phonetic: + continue + pattern = re.compile(rf'\b{re.escape(word)}\b', re.IGNORECASE) + t = pattern.sub(phonetic, t) + return t + class TTSGen: def __init__(self, pcs: Dict[str, RemotePC], voices: Dict[str, str], tts_port: int = 8003): self.pcs = pcs @@ -134,12 +146,16 @@ class TTSGen: """Generate voiceover audio for all segments in the specified language.""" segments = script_data.get("script_segments", []) text_key = "text" if lang == "en" else "text_es" - default_voice = "af_heart" if lang == "en" else "es-MX-JorgeNeural" + default_voice = "af_heart" if lang == "en" else "es-US-AlonsoNeural" voice = self.voices.get(lang, default_voice) # Ensure Spanish uses native neural voice if an old Kokoro voice was configured if lang == "es" and voice in ("ef_dora", "em_alex", "ef_clara", "em_santa"): - voice = "es-MX-JorgeNeural" + voice = "es-US-AlonsoNeural" + + phonetic_map = (script_data.get("phonetic_mappings") or {}).get(lang, {}) + if phonetic_map: + logger.info(f"[TTS:{lang.upper()}] Applied {len(phonetic_map)} phonetic pronunciation rules for voice synthesis.") logger.info(f"[TTS:{lang.upper()}] Synthesizing narration with voice '{voice}'...") @@ -153,16 +169,19 @@ class TTSGen: is_neural = voice.startswith(("es-", "en-", "fr-", "de-", "it-", "pt-")) for i, seg in enumerate(segments): - text = seg.get(text_key, "").strip() - if not text: - text = seg.get("text", f"Segment {i+1}") + raw_text = seg.get(text_key, "").strip() + if not raw_text: + raw_text = seg.get("text", f"Segment {i+1}") + + # Phonetic respelling applied specifically for spoken audio + spoken_text = apply_phonetic_respellings(raw_text, phonetic_map) seg_path = os.path.join(abs_job_dir, f"tts_{lang}_{i:03d}.wav") if is_neural: # Use native neural speech engine try: - _run_edge_tts(text, voice, seg_path) + _run_edge_tts(spoken_text, voice, seg_path) except Exception as e: logger.warning(f"[TTS:{lang.upper()}] Neural TTS failed: {e}, attempting Kokoro fallback...") is_neural = False @@ -174,7 +193,7 @@ class TTSGen: raise RuntimeError(f"IntelLLM Kokoro TTS not reachable on port {self.tts_port}") payload = { - "input": text, + "input": spoken_text, "voice": "af_heart" if lang == "en" else "ef_dora", "speed": 1.0, "lang": "e" if lang == "es" else "a" diff --git a/pipeline/visual_gen.py b/pipeline/visual_gen.py index 431a1d3..7c6d795 100644 --- a/pipeline/visual_gen.py +++ b/pipeline/visual_gen.py @@ -45,6 +45,27 @@ def h3_snap_length(seconds: float) -> int: # ─── Stock Media Fetcher ──────────────────────────────────────────────────── +STOP_WORDS = { + "the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", + "by", "about", "against", "between", "into", "through", "during", "before", "after", + "above", "below", "from", "up", "down", "out", "off", "over", "under", "again", + "further", "then", "once", "here", "there", "when", "where", "why", "how", "all", + "any", "both", "each", "few", "more", "most", "other", "some", "such", "no", "nor", + "not", "only", "own", "same", "so", "than", "too", "very", "can", "will", "just", + "should", "now", "what", "reveals", "stopped", "being", "mankind", "humanity", + "established", "used", "throughout", "history", "origins", "world", "worlds", + "has", "have", "had", "been", "was", "were", "are", "is", "its", "their", "they", + "them", "subjugate", "subjugating", "subjugation", "revealed", "revealing", "secrets", + "truth", "behind", "explained", "documentary", "mystery", "mysteries" +} + +def clean_search_keywords(text: str) -> str: + """Extract clean, concrete visual search terms from a descriptive prompt or query.""" + words = re.findall(r'[A-Za-z0-9]+', text.lower()) + filtered = [w for w in words if w not in STOP_WORDS and len(w) > 2] + # Keep up to 3 most specific visual keywords + return " ".join(filtered[:3]) if filtered else text.strip() + class StockMediaFetcher: def __init__(self, pexels_key: Optional[str] = None, pixabay_key: Optional[str] = None): self.pexels_key = pexels_key @@ -176,28 +197,39 @@ class StockMediaFetcher: pass return False - def search_and_download(self, query: str, out_path: str, prefer_video: bool = False) -> Optional[str]: - """Search across stock engines and download asset.""" - logger.info(f"[STOCK] Searching stock media for: '{query}' (video={prefer_video})...") - if prefer_video: - for label, fn in (("Pexels", self._query_pexels_video), - ("Pixabay", self._query_pixabay_video), - ("Wikimedia", lambda q: self._query_wikimedia(q, is_video=True))): - v_url = fn(query) - if v_url and self.download_media(v_url, out_path): - logger.info(f"[STOCK] Downloaded {label} video for '{query}'") - return out_path - return None + def search_and_download(self, queries: Any, out_path: str, prefer_video: bool = False) -> Optional[str]: + """Search across stock engines with candidate query cascade and download asset.""" + if isinstance(queries, str): + candidate_list = [queries] + else: + candidate_list = list(queries) - for label, fn in (("Pexels", self._query_pexels_photo), - ("Pixabay", self._query_pixabay_photo), - ("Wikimedia", lambda q: self._query_wikimedia(q, is_video=False))): - p_url = fn(query) - if p_url and self.download_media(p_url, out_path): - logger.info(f"[STOCK] Downloaded {label} photo for '{query}'") - return out_path + for raw_q in candidate_list: + if not raw_q: + continue + cleaned_q = clean_search_keywords(raw_q) + if not cleaned_q: + continue + + logger.info(f"[STOCK] Searching stock media for: '{cleaned_q}' (video={prefer_video})...") + if prefer_video: + for label, fn in (("Pexels", self._query_pexels_video), + ("Pixabay", self._query_pixabay_video), + ("Wikimedia", lambda q: self._query_wikimedia(q, is_video=True))): + v_url = fn(cleaned_q) + if v_url and self.download_media(v_url, out_path): + logger.info(f"[STOCK] Downloaded {label} video for '{cleaned_q}'") + return out_path + else: + for label, fn in (("Pexels", self._query_pexels_photo), + ("Pixabay", self._query_pixabay_photo), + ("Wikimedia", lambda q: self._query_wikimedia(q, is_video=False))): + p_url = fn(cleaned_q) + if p_url and self.download_media(p_url, out_path): + logger.info(f"[STOCK] Downloaded {label} photo for '{cleaned_q}'") + return out_path - logger.debug(f"[STOCK] No stock media found for: '{query}'") + logger.debug(f"[STOCK] No matching stock media found for queries: {candidate_list}") return None # ─── Ken Burns Motion Engine ──────────────────────────────────────────────── @@ -529,7 +561,21 @@ class VisualGen: Each source falls back to the next one in the rotation on failure. """ prompt = (seg.get("visual_prompt") or seg.get("text", "")) + ", cinematic documentary style, 8k, detailed" - stock_query = seg.get("stock_query") or seg.get("chapter_title") or seg.get("text", "")[:40] + primary_stock = seg.get("stock_query") or "" + chapter_stock = seg.get("chapter_title") or "" + topic_anchor = clean_search_keywords(getattr(self.cfg, "topic", "") or "") + + # Build prioritized topical query cascade + query_candidates = [] + if primary_stock: + query_candidates.append(primary_stock) + if chapter_stock and topic_anchor: + query_candidates.append(f"{topic_anchor} {chapter_stock}") + elif chapter_stock: + query_candidates.append(chapter_stock) + if topic_anchor: + query_candidates.append(topic_anchor) + seed = zlib.crc32(prompt.encode()) % (2**31) final_clip_path = os.path.join(out_dir, f"clip_{idx:03d}.mp4") @@ -539,9 +585,9 @@ class VisualGen: strategy = getattr(self.cfg, "visual_strategy", "hybrid") attempts = { "ai_video": lambda: self._try_ai_video(pc, prompt, seed, duration_sec, raw_video, final_clip_path, idx), - "stock_video": lambda: self._try_stock(stock_query, duration_sec, raw_video, final_clip_path, idx, prefer_video=True), + "stock_video": lambda: self._try_stock(query_candidates, duration_sec, raw_video, final_clip_path, idx, prefer_video=True), "ai_image": lambda: self._try_ai_image(pc, prompt, seed, duration_sec, raw_img, final_clip_path, idx), - "stock_image": lambda: self._try_stock(stock_query, duration_sec, raw_img, final_clip_path, idx, prefer_video=False), + "stock_image": lambda: self._try_stock(query_candidates, duration_sec, raw_img, final_clip_path, idx, prefer_video=False), } if strategy == "all_ai": order = ["ai_video", "ai_image", "stock_video", "stock_image"]