Initial commit: Polish AI dubbing pipeline (ComfyUI + Higgs Audio V3)

Converts SRT subtitles into a Polish lektor-style voiceover using a local
ComfyUI instance running Higgs Audio V3, then muxes the result back into the
source video as a second audio track. Video and original audio are always
stream-copied; only the new voiceover track is encoded.

Contents:
  lektor_ai_comfy.py    main pipeline (standard library only, needs ffmpeg)
  lektor_ai_comfy_v2.py batched-context experiment (timing unresolved)
  lektor_ai.py          earlier version using the hosted Boson AI API
  install_comfyui.sh    reproduces the ComfyUI environment, versions pinned
  docs/                 requirements, installation, usage, architecture,
                        troubleshooting
  gudowski/             voice references (.wav) with their transcripts (.txt)

Source media and generated video are gitignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 14:29:53 +02:00
co-authored by Claude Opus 5
commit fc4b761f52
23 changed files with 4452 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Inspect voice picker and Text mode on AI Studio TTS page."""
import json, time
from pathlib import Path
from playwright.sync_api import sync_playwright
profile_dir = Path.home() / ".config" / "lektor_ai" / "browser_profile"
def dump_elements(page, label):
elements = page.evaluate("""() => {
const sel = 'input, textarea, select, [role="combobox"], [role="listbox"], [role="textbox"], [role="option"], button, [contenteditable]';
return [...document.querySelectorAll(sel)].map(el => ({
tag: el.tagName,
role: el.getAttribute('role') || '',
ariaLabel: el.getAttribute('aria-label') || '',
placeholder: el.getAttribute('placeholder') || '',
class: el.className?.toString().slice(0, 100) || '',
text: (el.innerText || el.value || '').slice(0, 80),
}));
}""")
path = f"output/elements_{label}.json"
Path(path).write_text(json.dumps(elements, indent=2, ensure_ascii=False))
print(f"[{label}] {len(elements)} elements → {path}")
with sync_playwright() as pw:
ctx = pw.chromium.launch_persistent_context(
str(profile_dir),
channel="chrome",
headless=False,
viewport={"width": 1400, "height": 900},
ignore_default_args=["--enable-automation"],
args=["--disable-blink-features=AutomationControlled", "--start-maximized"],
)
page = ctx.new_page()
page.goto("https://aistudio.google.com/generate-speech", wait_until="networkidle", timeout=60_000)
time.sleep(3)
# Open editor
page.locator('.text-input-container').first.click()
time.sleep(2)
# Step 1: Click Text mode (the other toggle)
page.locator('button[role="radio"]').filter(has_text="Text").click()
time.sleep(2)
page.screenshot(path="output/shot_3_text_mode.png", full_page=True)
dump_elements(page, "3_text_mode")
# Step 2: Back to Composer, click voice chip
page.locator('button[role="radio"]').filter(has_text="Composer").click()
time.sleep(2)
page.locator('button.voice-chip').first.click()
time.sleep(2)
page.screenshot(path="output/shot_4_voice_picker.png", full_page=True)
dump_elements(page, "4_voice_picker")
input("Press Enter to close...")
ctx.close()