Add session logging and tuned runtime preset
This commit is contained in:
+11
@@ -203,3 +203,14 @@ The project now features a high-performance, Apple Silicon-optimized pipeline th
|
|||||||
- Routed post-correct and merge-decider requests through a shared Ollama caller with keep-alive support.
|
- Routed post-correct and merge-decider requests through a shared Ollama caller with keep-alive support.
|
||||||
- Added flags for warmup timeout and optional warmup skip for troubleshooting.
|
- Added flags for warmup timeout and optional warmup skip for troubleshooting.
|
||||||
- **Outcome:** Lower first-request latency and fewer fallback-to-rules events due to local model cold starts.
|
- **Outcome:** Lower first-request latency and fewer fallback-to-rules events due to local model cold starts.
|
||||||
|
|
||||||
|
## Phase 25: Session Logging + Tuned Live Preset
|
||||||
|
- **Goal:** Preserve reliable debugging context while locking in the best-performing live settings discovered through manual testing.
|
||||||
|
- **Approach:**
|
||||||
|
- Added session file logging hooks so finalized captions, revisions, startup state, and runtime errors can be written to a dedicated log via `--session-log-file`.
|
||||||
|
- Improved interactive terminal behavior by clearing stale draft lines cleanly and right-aligning Arabic output more consistently against terminal width.
|
||||||
|
- Recorded the current preferred live preset for English-first filtered transcription with multilingual output and local LLM post-correction:
|
||||||
|
```bash
|
||||||
|
python3 transcribe.py --silence 100 -q -s -c -es -en -fr -ar --lang en --filter-lang --mt-max-chars 450 --mt-max-new-tokens 220 --smart-correct --post-correct --post-correct-llm --post-correct-model qwen2.5:3b-instruct --session-log-file debug_session_1.log
|
||||||
|
```
|
||||||
|
- **Outcome:** The project now has a repeatable "known good" runtime profile plus persistent session diagnostics for reviewing caption issues after a run.
|
||||||
|
|||||||
+78
-16
@@ -2,6 +2,8 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
import requests
|
import requests
|
||||||
import threading
|
import threading
|
||||||
import json
|
import json
|
||||||
@@ -33,7 +35,7 @@ from silero_vad import load_silero_vad, get_speech_timestamps
|
|||||||
from transformers import MarianMTModel, MarianTokenizer
|
from transformers import MarianMTModel, MarianTokenizer
|
||||||
|
|
||||||
# Parameters
|
# Parameters
|
||||||
WHISPER_MODEL = "mlx-community/whisper-small-mlx"
|
WHISPER_MODEL = "mlx-community/whisper-base-mlx"
|
||||||
INGEST_URL = "https://emiapi.reynafamily.com/live-captions/ingest"
|
INGEST_URL = "https://emiapi.reynafamily.com/live-captions/ingest"
|
||||||
|
|
||||||
# Translation models (English -> Target)
|
# Translation models (English -> Target)
|
||||||
@@ -50,6 +52,16 @@ VAD_THRESHOLD = 0.5
|
|||||||
|
|
||||||
audio_queue = queue.Queue()
|
audio_queue = queue.Queue()
|
||||||
ingest_queue = queue.Queue()
|
ingest_queue = queue.Queue()
|
||||||
|
session_log_handle = None
|
||||||
|
|
||||||
|
def log_session(message):
|
||||||
|
"""Write a timestamped line to the session debug log if enabled."""
|
||||||
|
global session_log_handle
|
||||||
|
if session_log_handle is None:
|
||||||
|
return
|
||||||
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
session_log_handle.write(f"{ts} {message}\n")
|
||||||
|
session_log_handle.flush()
|
||||||
|
|
||||||
def parse_temperature_fallback(value):
|
def parse_temperature_fallback(value):
|
||||||
"""Parse comma-separated temperatures into a tuple of floats."""
|
"""Parse comma-separated temperatures into a tuple of floats."""
|
||||||
@@ -299,16 +311,21 @@ def call_ollama_generate(args, prompt, timeout, temperature=0.1):
|
|||||||
|
|
||||||
def post_correct_with_local_llm(text, args, llm_state, context_lines=None):
|
def post_correct_with_local_llm(text, args, llm_state, context_lines=None):
|
||||||
"""Optionally rewrite finalized English caption via a local Ollama model."""
|
"""Optionally rewrite finalized English caption via a local Ollama model."""
|
||||||
if not args.post_correct_llm or not llm_state.get("available", True):
|
if args.post_correct_debug:
|
||||||
|
print(f"[POST-LLM][PRE]: {text}", flush=True)
|
||||||
|
if not args.post_correct_llm:
|
||||||
|
if args.post_correct_debug:
|
||||||
|
print("[POST-LLM][SKIP]: --post-correct-llm not enabled; using rules-only.", flush=True)
|
||||||
|
return text
|
||||||
|
if not llm_state.get("available", True):
|
||||||
|
if args.post_correct_debug:
|
||||||
|
print("[POST-LLM][SKIP]: LLM marked unavailable after prior failure; using rules-only.", flush=True)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
context_lines = context_lines or []
|
context_lines = context_lines or []
|
||||||
prev2 = context_lines[-2] if len(context_lines) >= 2 else ""
|
prev2 = context_lines[-2] if len(context_lines) >= 2 else ""
|
||||||
prev1 = context_lines[-1] if len(context_lines) >= 1 else ""
|
prev1 = context_lines[-1] if len(context_lines) >= 1 else ""
|
||||||
|
|
||||||
if args.post_correct_debug:
|
|
||||||
print(f"[POST-LLM][PRE]: {text}")
|
|
||||||
|
|
||||||
prompt = (
|
prompt = (
|
||||||
"You are a real-time English caption corrector for live speech.\n"
|
"You are a real-time English caption corrector for live speech.\n"
|
||||||
"Task:\n"
|
"Task:\n"
|
||||||
@@ -333,27 +350,27 @@ def post_correct_with_local_llm(text, args, llm_state, context_lines=None):
|
|||||||
raw = response.json().get("response", "").strip()
|
raw = response.json().get("response", "").strip()
|
||||||
cleaned = normalize_english_caption(raw)
|
cleaned = normalize_english_caption(raw)
|
||||||
if args.post_correct_debug:
|
if args.post_correct_debug:
|
||||||
print(f"[POST-LLM][RAW]: {raw}")
|
print(f"[POST-LLM][RAW]: {raw}", flush=True)
|
||||||
if cleaned:
|
if cleaned:
|
||||||
overlap = word_overlap_ratio(text, cleaned)
|
overlap = word_overlap_ratio(text, cleaned)
|
||||||
if overlap < args.post_correct_min_overlap:
|
if overlap < args.post_correct_min_overlap:
|
||||||
if args.post_correct_debug:
|
if args.post_correct_debug:
|
||||||
print(
|
print(
|
||||||
f"[POST-LLM][REJECT]: low overlap ({overlap:.2f} < {args.post_correct_min_overlap:.2f}); keeping pre text"
|
f"[POST-LLM][REJECT]: low overlap ({overlap:.2f} < {args.post_correct_min_overlap:.2f}); keeping pre text"
|
||||||
)
|
, flush=True)
|
||||||
return text
|
return text
|
||||||
if args.post_correct_debug:
|
if args.post_correct_debug:
|
||||||
print(f"[POST-LLM][POST]: {cleaned}")
|
print(f"[POST-LLM][POST]: {cleaned}", flush=True)
|
||||||
return cleaned
|
return cleaned
|
||||||
if args.post_correct_debug:
|
if args.post_correct_debug:
|
||||||
print("[POST-LLM][POST]: <empty response, keeping pre text>")
|
print("[POST-LLM][POST]: <empty response, keeping pre text>", flush=True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if not llm_state.get("warned", False):
|
if not llm_state.get("warned", False):
|
||||||
print(f"\n[SYSTEM]: Local post-correct LLM unavailable ({exc}). Falling back to rules-only.")
|
print(f"\n[SYSTEM]: Local post-correct LLM unavailable ({exc}). Falling back to rules-only.")
|
||||||
llm_state["warned"] = True
|
llm_state["warned"] = True
|
||||||
llm_state["available"] = False
|
llm_state["available"] = False
|
||||||
if args.post_correct_debug:
|
if args.post_correct_debug:
|
||||||
print(f"[POST-LLM][ERROR]: {exc}")
|
print(f"[POST-LLM][ERROR]: {exc}", flush=True)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def maybe_merge_recent_caption(recent_en_lines, current_text, now_ts, window_sec):
|
def maybe_merge_recent_caption(recent_en_lines, current_text, now_ts, window_sec):
|
||||||
@@ -447,8 +464,11 @@ def format_caption_lines(lang_key, text):
|
|||||||
key = lang_key.lower()
|
key = lang_key.lower()
|
||||||
if key == "ar":
|
if key == "ar":
|
||||||
cleaned = normalize_arabic_punctuation(text)
|
cleaned = normalize_arabic_punctuation(text)
|
||||||
# Put label on separate line and force RTL embedding for the content line.
|
# Put label on separate line, right-align content, and force RTL embedding.
|
||||||
return ["[AR]:", f"\u202B{cleaned}\u202C"]
|
term_width = shutil.get_terminal_size(fallback=(100, 20)).columns
|
||||||
|
visible_len = len(cleaned)
|
||||||
|
left_pad = max(0, term_width - visible_len)
|
||||||
|
return ["[AR]:", f"{' ' * left_pad}\u202B{cleaned}\u202C"]
|
||||||
return [f"[{lang_key.upper()}]: {text}"]
|
return [f"[{lang_key.upper()}]: {text}"]
|
||||||
|
|
||||||
def print_caption(lang_key, text, leading_newline=False):
|
def print_caption(lang_key, text, leading_newline=False):
|
||||||
@@ -457,6 +477,7 @@ def print_caption(lang_key, text, leading_newline=False):
|
|||||||
lines[0] = "\n" + lines[0]
|
lines[0] = "\n" + lines[0]
|
||||||
for line in lines:
|
for line in lines:
|
||||||
print(line)
|
print(line)
|
||||||
|
log_session(line.strip())
|
||||||
|
|
||||||
def is_hallucination(text):
|
def is_hallucination(text):
|
||||||
"""Detect common Whisper hallucinations or high repetition."""
|
"""Detect common Whisper hallucinations or high repetition."""
|
||||||
@@ -534,7 +555,7 @@ def warmup_ollama_model(args):
|
|||||||
print(f"[SYSTEM]: Ollama warmup failed ({exc}). Continuing; runtime fallback will apply.")
|
print(f"[SYSTEM]: Ollama warmup failed ({exc}). Continuing; runtime fallback will apply.")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global WHISPER_MODEL
|
global WHISPER_MODEL, session_log_handle
|
||||||
parser = argparse.ArgumentParser(description="Live transcription and translation with Whisper.")
|
parser = argparse.ArgumentParser(description="Live transcription and translation with Whisper.")
|
||||||
parser.add_argument("-es", action="store_true", help="Enable Spanish translation")
|
parser.add_argument("-es", action="store_true", help="Enable Spanish translation")
|
||||||
parser.add_argument("-en", action="store_true", help="Enable English (detected or bridged)")
|
parser.add_argument("-en", action="store_true", help="Enable English (detected or bridged)")
|
||||||
@@ -587,35 +608,57 @@ def main():
|
|||||||
parser.add_argument("--speaker-sim-threshold", type=float, default=0.72, help="Cosine similarity threshold below which a speaker change is assumed (default: 0.72)")
|
parser.add_argument("--speaker-sim-threshold", type=float, default=0.72, help="Cosine similarity threshold below which a speaker change is assumed (default: 0.72)")
|
||||||
parser.add_argument("--speaker-min-buffer", type=float, default=1.6, help="Minimum buffered speech seconds before speaker-change checks (default: 1.6)")
|
parser.add_argument("--speaker-min-buffer", type=float, default=1.6, help="Minimum buffered speech seconds before speaker-change checks (default: 1.6)")
|
||||||
parser.add_argument("--speaker-check-interval", type=float, default=0.6, help="Seconds between speaker-change checks while buffering (default: 0.6)")
|
parser.add_argument("--speaker-check-interval", type=float, default=0.6, help="Seconds between speaker-change checks while buffering (default: 0.6)")
|
||||||
|
parser.add_argument("--session-log-file", type=str, default="transcribe_session.log", help="Path to session debug log file (default: transcribe_session.log)")
|
||||||
|
parser.add_argument("--disable-session-log", action="store_true", help="Disable writing session debug logs to file")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if not args.disable_session_log:
|
||||||
|
session_log_handle = open(args.session_log_file, "a", encoding="utf-8")
|
||||||
|
log_session("=" * 72)
|
||||||
|
log_session("SESSION_START")
|
||||||
|
log_session(f"ARGS {json.dumps(vars(args), sort_keys=True)}")
|
||||||
|
print(f"Session logging to: {args.session_log_file}")
|
||||||
if args.post_correct_llm and not args.post_correct:
|
if args.post_correct_llm and not args.post_correct:
|
||||||
args.post_correct = True
|
args.post_correct = True
|
||||||
print("[SYSTEM]: Enabling --post-correct because --post-correct-llm was requested.")
|
print("[SYSTEM]: Enabling --post-correct because --post-correct-llm was requested.")
|
||||||
|
log_session("[SYSTEM]: Enabling --post-correct because --post-correct-llm was requested.")
|
||||||
file_glossary_pairs = load_glossary_file(args.glossary_file)
|
file_glossary_pairs = load_glossary_file(args.glossary_file)
|
||||||
merged_glossary_pairs = file_glossary_pairs + args.glossary_pair
|
merged_glossary_pairs = file_glossary_pairs + args.glossary_pair
|
||||||
if file_glossary_pairs:
|
if file_glossary_pairs:
|
||||||
print(f"Loaded {len(file_glossary_pairs)} glossary terms from {args.glossary_file}")
|
print(f"Loaded {len(file_glossary_pairs)} glossary terms from {args.glossary_file}")
|
||||||
|
log_session(f"Loaded {len(file_glossary_pairs)} glossary terms from {args.glossary_file}")
|
||||||
if args.post_correct:
|
if args.post_correct:
|
||||||
print("Post-correct mode: rules enabled.")
|
print("Post-correct mode: rules enabled.")
|
||||||
|
log_session("Post-correct mode: rules enabled.")
|
||||||
if args.post_correct_llm:
|
if args.post_correct_llm:
|
||||||
print(
|
print(
|
||||||
f"Post-correct LLM: {args.post_correct_model} via {args.post_correct_ollama_url} "
|
f"Post-correct LLM: {args.post_correct_model} via {args.post_correct_ollama_url} "
|
||||||
f"(keep_alive={args.post_correct_keep_alive})"
|
f"(keep_alive={args.post_correct_keep_alive})"
|
||||||
)
|
)
|
||||||
|
log_session(
|
||||||
|
f"Post-correct LLM: {args.post_correct_model} via {args.post_correct_ollama_url} "
|
||||||
|
f"(keep_alive={args.post_correct_keep_alive})"
|
||||||
|
)
|
||||||
if args.post_correct_debug:
|
if args.post_correct_debug:
|
||||||
print("Post-correct debug logging enabled.")
|
print("Post-correct debug logging enabled.")
|
||||||
|
log_session("Post-correct debug logging enabled.")
|
||||||
if args.llm_merge_decider and not args.post_correct_llm:
|
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.")
|
print("[SYSTEM]: --llm-merge-decider requires --post-correct-llm. Falling back to heuristic merge.")
|
||||||
args.llm_merge_decider = False
|
args.llm_merge_decider = False
|
||||||
|
log_session("[SYSTEM]: --llm-merge-decider requires --post-correct-llm. Falling back to heuristic merge.")
|
||||||
if args.speaker_change_detect:
|
if args.speaker_change_detect:
|
||||||
print(
|
print(
|
||||||
"Speaker-change detection enabled "
|
"Speaker-change detection enabled "
|
||||||
f"(threshold={args.speaker_sim_threshold}, min_buffer={args.speaker_min_buffer}s)."
|
f"(threshold={args.speaker_sim_threshold}, min_buffer={args.speaker_min_buffer}s)."
|
||||||
)
|
)
|
||||||
|
log_session(
|
||||||
|
"Speaker-change detection enabled "
|
||||||
|
f"(threshold={args.speaker_sim_threshold}, min_buffer={args.speaker_min_buffer}s)."
|
||||||
|
)
|
||||||
|
|
||||||
if args.quantize:
|
if args.quantize:
|
||||||
WHISPER_MODEL = "mlx-community/whisper-small-mlx-4bit"
|
WHISPER_MODEL = "mlx-community/whisper-small-mlx-4bit"
|
||||||
|
log_session("Using quantized Whisper model variant.")
|
||||||
|
|
||||||
if args.list_devices:
|
if args.list_devices:
|
||||||
print("\nAvailable Audio Devices:")
|
print("\nAvailable Audio Devices:")
|
||||||
@@ -625,6 +668,7 @@ def main():
|
|||||||
buffer_limit = SAMPLERATE * args.max_buffer
|
buffer_limit = SAMPLERATE * args.max_buffer
|
||||||
device = "mps" if torch.backends.mps.is_available() else "cpu"
|
device = "mps" if torch.backends.mps.is_available() else "cpu"
|
||||||
print(f"Using device: {device}")
|
print(f"Using device: {device}")
|
||||||
|
log_session(f"Using device: {device}")
|
||||||
|
|
||||||
# Only start ingest thread if enabled
|
# Only start ingest thread if enabled
|
||||||
if args.ingest:
|
if args.ingest:
|
||||||
@@ -688,6 +732,7 @@ def main():
|
|||||||
print("Using system default input device.")
|
print("Using system default input device.")
|
||||||
|
|
||||||
print(f"\nStarting live transcription{' & server ingest' if args.ingest else ''}... (Press Ctrl+C to stop)")
|
print(f"\nStarting live transcription{' & server ingest' if args.ingest else ''}... (Press Ctrl+C to stop)")
|
||||||
|
log_session(f"Starting live transcription (ingest={args.ingest}).")
|
||||||
|
|
||||||
audio_buffer = []
|
audio_buffer = []
|
||||||
speech_started = False
|
speech_started = False
|
||||||
@@ -696,6 +741,7 @@ def main():
|
|||||||
rolling_context = ""
|
rolling_context = ""
|
||||||
rolling_context_en = ""
|
rolling_context_en = ""
|
||||||
last_draft_text = ""
|
last_draft_text = ""
|
||||||
|
draft_line_active = False
|
||||||
recent_en_lines = deque(maxlen=2)
|
recent_en_lines = deque(maxlen=2)
|
||||||
llm_post_correct_state = {"available": True, "warned": False, "merge_warned": False}
|
llm_post_correct_state = {"available": True, "warned": False, "merge_warned": False}
|
||||||
speaker_reference_signature = None
|
speaker_reference_signature = None
|
||||||
@@ -781,6 +827,7 @@ def main():
|
|||||||
if args.stream:
|
if args.stream:
|
||||||
sys.stdout.write("\r\033[K")
|
sys.stdout.write("\r\033[K")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
draft_line_active = False
|
||||||
|
|
||||||
# Prepare transcription kwargs
|
# Prepare transcription kwargs
|
||||||
transcribe_kwargs = {"path_or_hf_repo": WHISPER_MODEL}
|
transcribe_kwargs = {"path_or_hf_repo": WHISPER_MODEL}
|
||||||
@@ -901,6 +948,7 @@ def main():
|
|||||||
if merge_ok:
|
if merge_ok:
|
||||||
english_for_caption = merge_text
|
english_for_caption = merge_text
|
||||||
print(f"[EN-REV]: {old_line} -> {english_for_caption}")
|
print(f"[EN-REV]: {old_line} -> {english_for_caption}")
|
||||||
|
log_session(f"[EN-REV]: {old_line} -> {english_for_caption}")
|
||||||
else:
|
else:
|
||||||
if recent_en_lines:
|
if recent_en_lines:
|
||||||
recent_en_lines[-1]["text"] = old_line
|
recent_en_lines[-1]["text"] = old_line
|
||||||
@@ -912,6 +960,7 @@ def main():
|
|||||||
if args.en and english_for_caption:
|
if args.en and english_for_caption:
|
||||||
payload["en"] = english_for_caption
|
payload["en"] = english_for_caption
|
||||||
print(f"[EN]: {english_for_caption}")
|
print(f"[EN]: {english_for_caption}")
|
||||||
|
log_session(f"[EN]: {english_for_caption}")
|
||||||
|
|
||||||
# Update rolling context for next segment
|
# Update rolling context for next segment
|
||||||
if args.context:
|
if args.context:
|
||||||
@@ -959,6 +1008,7 @@ def main():
|
|||||||
stuck_draft_count = 0
|
stuck_draft_count = 0
|
||||||
last_draft_text = ""
|
last_draft_text = ""
|
||||||
speaker_reference_signature = None
|
speaker_reference_signature = None
|
||||||
|
draft_line_active = False
|
||||||
|
|
||||||
elif args.stream and (time.time() - last_stream_time) > 1.0:
|
elif args.stream and (time.time() - last_stream_time) > 1.0:
|
||||||
# Draft transcription
|
# Draft transcription
|
||||||
@@ -981,13 +1031,17 @@ def main():
|
|||||||
if draft_text != last_draft_text:
|
if draft_text != last_draft_text:
|
||||||
last_change_time = time.time()
|
last_change_time = time.time()
|
||||||
last_draft_text = draft_text
|
last_draft_text = draft_text
|
||||||
|
if not draft_line_active:
|
||||||
sys.stdout.write(f"\r\033[K[DRAFT]: {draft_text}")
|
# Reserve one blank line before the rolling draft line.
|
||||||
sys.stdout.flush()
|
sys.stdout.write("\n")
|
||||||
|
draft_line_active = True
|
||||||
|
sys.stdout.write(f"\r\033[K[DRAFT]: {draft_text}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
# Send draft to ingest if enabled
|
# Send draft to ingest if enabled
|
||||||
if args.ingest:
|
if args.ingest:
|
||||||
ingest_queue.put({"draft": draft_text})
|
ingest_queue.put({"draft": draft_text})
|
||||||
|
log_session("[DRAFT]: queued draft ingest payload")
|
||||||
else:
|
else:
|
||||||
# If Whisper returns empty, check if we've been silent for too long
|
# If Whisper returns empty, check if we've been silent for too long
|
||||||
# even though VAD says there is speech.
|
# even though VAD says there is speech.
|
||||||
@@ -1003,6 +1057,7 @@ def main():
|
|||||||
last_draft_text = ""
|
last_draft_text = ""
|
||||||
sys.stdout.write("\r\033[K")
|
sys.stdout.write("\r\033[K")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
draft_line_active = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
last_stream_time = time.time()
|
last_stream_time = time.time()
|
||||||
@@ -1013,8 +1068,15 @@ def main():
|
|||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nStopped by user.")
|
print("\nStopped by user.")
|
||||||
|
log_session("Stopped by user.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\nError: {e}")
|
print(f"\nError: {e}")
|
||||||
|
log_session(f"Error: {e}")
|
||||||
|
finally:
|
||||||
|
if session_log_handle is not None:
|
||||||
|
log_session("SESSION_END")
|
||||||
|
session_log_handle.close()
|
||||||
|
session_log_handle = None
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|||||||
Reference in New Issue
Block a user