feat: add optional llm merge-decider for smart caption revisions

This commit is contained in:
Adolfo Reyna
2026-03-06 19:56:07 -05:00
parent 564fcaac1a
commit 2881bd96dd
2 changed files with 89 additions and 3 deletions
+8
View File
@@ -168,3 +168,11 @@ The project now features a high-performance, Apple Silicon-optimized pipeline th
- 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.
## Phase 21: LLM Merge-Decider for Line Revisions
- **Goal:** Improve English line-merge quality when smart-correct revision is ambiguous.
- **Approach:**
- Added optional `--llm-merge-decider` to validate/refine heuristic merge candidates using the local post-correct LLM.
- Enforced strict low-latency behavior with `--llm-merge-timeout` (default 0.7s) and automatic fallback to heuristic merges on errors/timeouts.
- Added safety guards to reject overly large LLM rewrites and preserve buffered caption state when merge is rejected.
- **Outcome:** Merge decisions remain fast and deterministic by default, with optional LLM arbitration for cleaner sentence continuity.
+81 -3
View File
@@ -292,6 +292,66 @@ def maybe_merge_recent_caption(recent_en_lines, current_text, now_ts, window_sec
previous["ts"] = now_ts
return old, merged
def llm_decide_caption_merge(previous_text, current_text, candidate_text, args, llm_state):
"""Ask local LLM to approve/refine a merge candidate; fallback to heuristic decision."""
if not args.llm_merge_decider or not args.post_correct_llm or not llm_state.get("available", True):
return True, candidate_text
prompt = (
"You decide whether two live English caption lines should be merged.\n"
"Return strict JSON only in this shape:\n"
"{\"merge\": true|false, \"text\": \"final merged text or empty\"}\n"
"Rules:\n"
"- Preserve meaning exactly.\n"
"- If uncertain, return merge=false.\n"
"- If merge=true, provide a single corrected line in text.\n"
f"Previous: {previous_text}\n"
f"Current: {current_text}\n"
f"HeuristicMerged: {candidate_text}\n"
"JSON:"
)
try:
response = requests.post(
args.post_correct_ollama_url,
json={
"model": args.post_correct_model,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.0},
},
timeout=args.llm_merge_timeout,
)
response.raise_for_status()
raw = response.json().get("response", "").strip()
if not raw:
return True, candidate_text
try:
parsed = json.loads(raw)
except json.JSONDecodeError:
match = re.search(r"\{.*\}", raw, re.DOTALL)
if not match:
return True, candidate_text
parsed = json.loads(match.group(0))
merge = bool(parsed.get("merge", False))
llm_text = normalize_english_caption(parsed.get("text", "").strip())
if not merge:
return False, ""
if not llm_text:
llm_text = candidate_text
# Safety: reject large rewrites and keep heuristic merge.
if len(llm_text) > max(2 * len(candidate_text), len(candidate_text) + 80):
return True, candidate_text
return True, llm_text
except Exception as exc:
if not llm_state.get("merge_warned", False):
print(f"\n[SYSTEM]: LLM merge-decider unavailable ({exc}). Falling back to heuristic merge.")
llm_state["merge_warned"] = True
return True, candidate_text
def normalize_arabic_punctuation(text):
"""Convert common Latin punctuation to Arabic-friendly equivalents."""
updated = text.replace(",", "،").replace(";", "؛")
@@ -418,6 +478,8 @@ def main():
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)")
parser.add_argument("--llm-merge-decider", action="store_true", help="Use local LLM to validate/refine smart-correct line merges")
parser.add_argument("--llm-merge-timeout", type=float, default=0.7, help="Timeout seconds for LLM merge decision (default: 0.7)")
args = parser.parse_args()
if args.post_correct_llm and not args.post_correct:
@@ -431,6 +493,9 @@ def main():
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.llm_merge_decider and not args.post_correct_llm:
print("[SYSTEM]: --llm-merge-decider requires --post-correct-llm. Falling back to heuristic merge.")
args.llm_merge_decider = False
if args.quantize:
WHISPER_MODEL = "mlx-community/whisper-small-mlx-4bit"
@@ -512,7 +577,7 @@ def main():
rolling_context_en = ""
last_draft_text = ""
recent_en_lines = deque(maxlen=2)
llm_post_correct_state = {"available": True, "warned": False}
llm_post_correct_state = {"available": True, "warned": False, "merge_warned": False}
try:
with sd.InputStream(samplerate=SAMPLERATE, channels=args.channels, callback=callback, blocksize=BLOCK_SIZE, device=device_index):
@@ -679,8 +744,21 @@ def main():
)
if merged:
old_line, new_line = merged
english_for_caption = new_line
print(f"[EN-REV]: {old_line} -> {new_line}")
merge_ok, merge_text = llm_decide_caption_merge(
old_line,
english_for_caption,
new_line,
args,
llm_post_correct_state,
)
if merge_ok:
english_for_caption = merge_text
print(f"[EN-REV]: {old_line} -> {english_for_caption}")
else:
if recent_en_lines:
recent_en_lines[-1]["text"] = old_line
recent_en_lines[-1]["ts"] = now_ts
recent_en_lines.append({"text": english_for_caption, "ts": now_ts})
else:
recent_en_lines.append({"text": english_for_caption, "ts": now_ts})