feat: add v2 post-correct mode with optional local llm

This commit is contained in:
Adolfo Reyna
2026-03-06 19:36:08 -05:00
parent 57366543ec
commit 564fcaac1a
2 changed files with 97 additions and 0 deletions
+8
View File
@@ -160,3 +160,11 @@ The project now features a high-performance, Apple Silicon-optimized pipeline th
- Wrapped Arabic caption content in explicit RTL embedding marks for better bidirectional layout stability.
- Added punctuation normalization for Arabic display (`،`, `؛`, `؟`) to reduce LTR punctuation artifacts.
- **Outcome:** Arabic captions render more consistently in terminal output, especially when adjacent to English/French/Spanish caption lines.
## Phase 20: Post-Correct Mode (V2 Foundation)
- **Goal:** Add a practical second-stage correction pass for finalized English captions.
- **Approach:**
- Added `--post-correct` mode to run deterministic rule-based cleanup after ASR (spacing, punctuation cleanup, disfluency reduction, immediate duplicate-word collapse).
- Added optional local LLM correction via Ollama (`--post-correct-llm`, model/url/timeout flags) with strict fallback to rules-only when unavailable.
- Integrated post-correction after bridge-to-English and before downstream translation so improved English text propagates to target-language MT.
- **Outcome:** The pipeline now supports a two-stage transcription flow (ASR -> correction) with low-latency defaults and optional local generative enhancement.
+89
View File
@@ -207,6 +207,69 @@ def normalize_english_caption(text):
normalized = normalized[0].upper() + normalized[1:]
return normalized
def apply_rule_based_post_correction(text):
"""Apply lightweight deterministic cleanup for finalized English captions."""
if not text:
return ""
corrected = normalize_english_caption(text)
corrected = re.sub(r"\s+", " ", corrected).strip()
corrected = re.sub(r"\s+([,.;!?])", r"\1", corrected)
corrected = re.sub(r"([,.;!?]){2,}", r"\1", corrected)
# Remove common disfluencies conservatively.
corrected = re.sub(r"\b(uh+|um+|erm+|ah+|hmm+)\b", "", corrected, flags=re.IGNORECASE)
corrected = re.sub(r"\s+", " ", corrected).strip()
# Collapse immediate duplicated words (e.g., "the the", "I I").
corrected = re.sub(r"\b(\w+)\s+\1\b", r"\1", corrected, flags=re.IGNORECASE)
corrected = re.sub(r"\s+", " ", corrected).strip()
if corrected and corrected[0].isalpha():
corrected = corrected[0].upper() + corrected[1:]
return corrected
def post_correct_with_local_llm(text, args, llm_state, context_hint=""):
"""Optionally rewrite finalized English caption via a local Ollama model."""
if not args.post_correct_llm or not llm_state.get("available", True):
return text
prompt = (
"You correct live English captions.\n"
"Rules:\n"
"- Preserve original meaning exactly.\n"
"- Fix grammar, punctuation, and natural wording.\n"
"- Remove filler words only when they do not change meaning.\n"
"- Keep output concise and in one line.\n"
"- Return only corrected text, no quotes or explanation.\n"
)
if context_hint:
prompt += f"Previous caption context: {context_hint}\n"
prompt += f"Caption: {text}\nCorrected:"
try:
response = requests.post(
args.post_correct_ollama_url,
json={
"model": args.post_correct_model,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.1},
},
timeout=args.post_correct_llm_timeout,
)
response.raise_for_status()
raw = response.json().get("response", "").strip()
cleaned = normalize_english_caption(raw)
if cleaned:
return cleaned
except Exception as exc:
if not llm_state.get("warned", False):
print(f"\n[SYSTEM]: Local post-correct LLM unavailable ({exc}). Falling back to rules-only.")
llm_state["warned"] = True
llm_state["available"] = False
return text
def maybe_merge_recent_caption(recent_en_lines, current_text, now_ts, window_sec):
if not recent_en_lines:
return None
@@ -350,12 +413,24 @@ def main():
parser.add_argument("--caption-correction-window", type=float, default=3.0, help="Seconds where previous English line can still be merged/corrected (default: 3.0)")
parser.add_argument("--glossary-file", type=str, default="glossary.txt", help="Path to glossary file with SOURCE=TARGET pairs (default: glossary.txt if present)")
parser.add_argument("--glossary-pair", action="append", type=parse_glossary_pair, default=[], help="Term replacement pair SOURCE=TARGET (repeatable)")
parser.add_argument("--post-correct", action="store_true", help="Enable v2 finalized English caption post-correction")
parser.add_argument("--post-correct-llm", action="store_true", help="Use local LLM for post-correction (fallback to rules on failure)")
parser.add_argument("--post-correct-model", type=str, default="llama3.1:8b-instruct", help="Local Ollama model for post-correction")
parser.add_argument("--post-correct-ollama-url", type=str, default="http://127.0.0.1:11434/api/generate", help="Ollama generate endpoint for local post-correction")
parser.add_argument("--post-correct-llm-timeout", type=float, default=3.0, help="Timeout seconds for local LLM post-correction (default: 3.0)")
args = parser.parse_args()
if args.post_correct_llm and not args.post_correct:
args.post_correct = True
print("[SYSTEM]: Enabling --post-correct because --post-correct-llm was requested.")
file_glossary_pairs = load_glossary_file(args.glossary_file)
merged_glossary_pairs = file_glossary_pairs + args.glossary_pair
if file_glossary_pairs:
print(f"Loaded {len(file_glossary_pairs)} glossary terms from {args.glossary_file}")
if args.post_correct:
print("Post-correct mode: rules enabled.")
if args.post_correct_llm:
print(f"Post-correct LLM: {args.post_correct_model} via {args.post_correct_ollama_url}")
if args.quantize:
WHISPER_MODEL = "mlx-community/whisper-small-mlx-4bit"
@@ -437,6 +512,7 @@ def main():
rolling_context_en = ""
last_draft_text = ""
recent_en_lines = deque(maxlen=2)
llm_post_correct_state = {"available": True, "warned": False}
try:
with sd.InputStream(samplerate=SAMPLERATE, channels=args.channels, callback=callback, blocksize=BLOCK_SIZE, device=device_index):
@@ -578,6 +654,19 @@ def main():
if args.smart_correct and english_for_translation:
english_for_translation = normalize_english_caption(english_for_translation)
english_for_translation = apply_glossary(english_for_translation, merged_glossary_pairs)
elif merged_glossary_pairs and english_for_translation:
# Allow glossary use even when smart-correct is disabled.
english_for_translation = apply_glossary(english_for_translation, merged_glossary_pairs)
if args.post_correct and english_for_translation:
context_hint = recent_en_lines[-1]["text"] if recent_en_lines else ""
english_for_translation = apply_rule_based_post_correction(english_for_translation)
english_for_translation = post_correct_with_local_llm(
english_for_translation,
args,
llm_post_correct_state,
context_hint=context_hint,
)
english_for_caption = english_for_translation
if args.smart_correct and english_for_caption: