80f0bf309f
- Apple SpeechAnalyzer (macOS 26+) binary: --bench (31x RTF), --pipe
(persistent process, 150ms finals), --live (word-by-word drafts)
- Pipe protocol: 4-byte BE length + wav payload, emits JSONL
{event:draft|final, text, isFinal, chunk} — 31 drafts for 6s audio (~60ms granularity)
- engine_apple_transcribe.py: ApplePipeTranscriber with
transcribe() + transcribe_with_draft_callback(), VAD + draft
queue, new flags --apple-stream (on), --apple-stream-interval,
--apple-pipe (on). Fixes PIL/transformers import crash by lazy import.
- main_v3.py: engine selector {whisper,apple}, passthrough translate
when no -es/-fr/-ar, freeflow flags same as v2
- Freeflow polish: deterministic punctuation commands (comma,
question mark, new paragraph, at sign), filler stripping,
<keep> protection, skip-clean heuristic, freeflow/qwen/legacy
prompt styles. Much better final readability vs raw Apple/Whisper.
- main_v2.py, engine_llm.py, engine_distribute.py: integrate freeflow
- bench: Apple 2.12% WER vs Whisper Small 3.74% (Inscribe), CPU
0mW ANE (measured via powermetrics), 196M EN cryptex per locale.
- Verified: 31 word-by-word drafts, 2 finals, exit 0, bench regression ok.
Freeflow still much better for final polish — Apple wins on speed
and raw accuracy, freeflow wins on readable paragraph output.
Co-authored-by: internal-model
113 lines
4.8 KiB
Python
113 lines
4.8 KiB
Python
import argparse
|
|
import multiprocessing
|
|
import time
|
|
import re
|
|
|
|
def run_translation(in_queue, out_queue, args):
|
|
import sys
|
|
from unittest.mock import MagicMock
|
|
# Comprehensive workaround for missing _lzma in some Python builds
|
|
try:
|
|
import lzma
|
|
except ImportError:
|
|
mock_lzma = MagicMock()
|
|
mock_lzma.FORMAT_XZ, mock_lzma.FORMAT_ALONE, mock_lzma.FORMAT_RAW = 1, 2, 3
|
|
mock_lzma.CHECK_NONE, mock_lzma.CHECK_CRC32, mock_lzma.CHECK_CRC64, mock_lzma.CHECK_SHA256 = 0, 1, 4, 10
|
|
sys.modules["_lzma"] = MagicMock()
|
|
sys.modules["lzma"] = mock_lzma
|
|
|
|
import torch
|
|
from transformers import MarianMTModel, MarianTokenizer
|
|
|
|
TARGET_LANGS = {
|
|
"es": "Helsinki-NLP/opus-mt-en-es",
|
|
"fr": "Helsinki-NLP/opus-mt-en-fr",
|
|
"ar": "Helsinki-NLP/opus-mt-en-ar"
|
|
}
|
|
|
|
def split_text_for_translation(text, max_chars=250):
|
|
normalized = " ".join(text.split()).strip()
|
|
if not normalized or len(normalized) <= max_chars: return [normalized] if normalized else []
|
|
chunks, current = [], ""
|
|
sentences = [s for s in re.split(r"(?<=[.!?])\s+", normalized) if s]
|
|
for sentence in sentences:
|
|
if len(sentence) > max_chars:
|
|
words, word_chunk = sentence.split(), ""
|
|
for word in words:
|
|
candidate = f"{word_chunk} {word}".strip()
|
|
if len(candidate) <= max_chars: word_chunk = candidate
|
|
else:
|
|
if word_chunk: chunks.append(word_chunk)
|
|
word_chunk = word
|
|
if word_chunk: chunks.append(word_chunk)
|
|
continue
|
|
candidate = f"{current} {sentence}".strip()
|
|
if candidate and len(candidate) <= max_chars: current = candidate
|
|
else:
|
|
if current: chunks.append(current)
|
|
current = sentence
|
|
if current: chunks.append(current)
|
|
return chunks
|
|
|
|
device = "mps" if torch.backends.mps.is_available() else "cpu"
|
|
print(f"[Translate] Loading translation models on {device}...")
|
|
|
|
translation_engines = {}
|
|
for lang_key, model_id in TARGET_LANGS.items():
|
|
if getattr(args, lang_key, False):
|
|
print(f"[Translate] Loading {lang_key} model...")
|
|
translation_engines[lang_key] = (
|
|
MarianMTModel.from_pretrained(model_id).to(device),
|
|
MarianTokenizer.from_pretrained(model_id)
|
|
)
|
|
|
|
while True:
|
|
try:
|
|
item = in_queue.get()
|
|
if item is None: break
|
|
if "draft" in item:
|
|
out_queue.put(item)
|
|
continue
|
|
|
|
raw_text, corrected_text, paragraph_text = item.get("raw"), item.get("corrected"), item.get("paragraph")
|
|
payload = {
|
|
"original": raw_text,
|
|
"corrected": corrected_text,
|
|
"paragraph": paragraph_text,
|
|
"en_bridge": item.get("en_bridge"),
|
|
"used_llm_line": item.get("used_llm_line", False),
|
|
"used_llm_paragraph": item.get("used_llm_paragraph", False),
|
|
"paragraph_fallback": bool(paragraph_text) and not item.get("used_llm_paragraph", False),
|
|
"speaker": item.get("speaker"),
|
|
"ts": item.get("ts", time.time())
|
|
}
|
|
|
|
if args.only_translate_llm:
|
|
text_to_translate = paragraph_text
|
|
else:
|
|
text_to_translate = paragraph_text or corrected_text or raw_text
|
|
|
|
english_output = text_to_translate or ""
|
|
if english_output:
|
|
payload["english_output"] = english_output
|
|
if args.en and english_output:
|
|
payload["en"] = english_output
|
|
|
|
if text_to_translate and translation_engines:
|
|
for lang_key, (model, tokenizer) in translation_engines.items():
|
|
chunks = split_text_for_translation(text_to_translate, args.mt_max_chars)
|
|
translated_parts = []
|
|
for chunk in chunks:
|
|
inputs = tokenizer(chunk, return_tensors="pt", padding=True).to(device)
|
|
with torch.no_grad():
|
|
translated_tokens = model.generate(**inputs, max_new_tokens=args.mt_max_new_tokens, num_beams=args.mt_num_beams)
|
|
translated_parts.append(tokenizer.decode(translated_tokens[0], skip_special_tokens=True).strip())
|
|
payload[lang_key] = " ".join(translated_parts)
|
|
|
|
# ALWAYS put in queue, even if no translations were done
|
|
out_queue.put(payload)
|
|
except Exception as e: print(f"[Translate] Error: {e}")
|
|
|
|
if __name__ == "__main__":
|
|
run_translation(multiprocessing.Queue(), multiprocessing.Queue(), argparse.Namespace())
|