Add subtitle dictionaries, Polish number normalizer, ComfyUI lifecycle control
Text processing before TTS:
- dictionary.txt rewrites words and phrases (km/h, np., units). Rules compile
into one alternation applied in a single pass, so replacements never chain
and longest-literal wins. Case-sensitive, whole-word.
- A per-queue second dictionary, written by the GUI into the first video's job
folder and merged with the shared one (--dictionary-extra).
- ALL-CAPS blocks become Title Case rather than sentence case: lowercasing
destroyed proper nouns inside captions ("NASA - MARGO MADISON") and invited
the normalizer to "correct" the name. Paired clips in output/clips/all-caps
showed the TTS reads both casings the same.
normalizer/ — digits to spoken Polish, in its own venv because it needs
transformers>=5, which must not touch ComfyUI's pinned versions:
- Two backends. Plain integers go to num2words for the value and a general
Polish LLM for the case; times, dates, decimals and identifiers go to a 0.6B
fine-tune. Measured on 40 real subtitle lines, each is clearly better than
the other on its half. 74% of numbered blocks take the first leg.
- Guards on every answer: runaway length, repetition loops, invalid bytes,
leftover digits, a lost minus sign, a changed number, a lost word. A failure
falls back to num2words, which is nominative but never wrong about the value.
- ComfyUI is asked to unload before normalizing; a normalizer that does not fit
in VRAM is spilled to RAM silently at 5x the cost.
Backend lifecycle:
- ComfyUI is found by port through /proc, so a backend started by hand is
managed too. SIGTERM escalates to SIGKILL, and a suspended process is killed
outright since it never runs its handlers.
- A clip that kills the backend is retried three times, each with a different
seed (the aborts are seed-dependent, not text-dependent), then skipped and
reported so an overnight queue keeps moving.
- start_comfyui.sh execs python, so the script's PID is ComfyUI's.
- GUI gains ComfyUI and Queue dictionary buttons; quitting stops the backend.
--resume-from now reuses the job folder's tts_input.srt instead of reparsing
the subtitles, so block N still pairs with clip_NNNN.flac.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""JSON-lines front end for normalizer.py.
|
||||
|
||||
The normalizer needs `transformers>=5`, which must not be installed into the
|
||||
ComfyUI/Higgs environment, so it lives in its own venv and is driven as a
|
||||
separate process. Loading a 1.1 GB model per subtitle block would be absurd, so
|
||||
this is a long-lived worker: the caller starts it once per run, streams blocks
|
||||
through it, and closes stdin when done.
|
||||
|
||||
Protocol — one JSON object per line, in both directions:
|
||||
|
||||
← {"ready": true, "mode": "model", "device": "cuda:0", "llm": true, …}
|
||||
→ {"id": 1, "text": "Wygrał 1247 złotych."}
|
||||
← {"id": 1, "text": "Wygrał tysiąc dwieście…", "changed": true,
|
||||
"leg": "plain", "guard": "", "error": null}
|
||||
→ {"quit": true} (or just close stdin)
|
||||
|
||||
`leg` is which model answered — "plain" for num2words plus the inflection
|
||||
model, "complex" for the fine-tune, "skipped" when the block needed nothing.
|
||||
`guard` names the check that rejected the model's answer, empty when it passed.
|
||||
|
||||
stdout carries the protocol and nothing else; every log line, warning and
|
||||
traceback goes to stderr. A request that fails is answered with the original
|
||||
text and an `error` field — the worker does not die on one bad block.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import normalizer
|
||||
|
||||
|
||||
def _send(obj) -> None:
|
||||
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s",
|
||||
stream=sys.stderr)
|
||||
|
||||
state = normalizer.status() # loads the model, or reports why not
|
||||
state["llm"] = normalizer.load_llm() # the inflection leg is optional
|
||||
state["llm_dir"] = str(normalizer.LLM_DIR)
|
||||
_send({"ready": True, **state})
|
||||
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
req = json.loads(line)
|
||||
except ValueError as exc:
|
||||
_send({"id": None, "text": None, "error": f"bad request: {exc}"})
|
||||
continue
|
||||
if req.get("quit"):
|
||||
break
|
||||
|
||||
text = req.get("text") or ""
|
||||
leg, guard = "", ""
|
||||
try:
|
||||
result, leg, guard = normalizer.normalize_detail(text)
|
||||
error = None
|
||||
except Exception as exc: # normalize_detail() is not meant to raise
|
||||
logging.exception("normalize_detail() raised")
|
||||
result, error = text, f"{type(exc).__name__}: {exc}"
|
||||
_send({"id": req.get("id"), "text": result, "changed": result != text,
|
||||
"leg": leg, "guard": guard, "error": error})
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user