#!/opt/aime-venv/bin/python """CallRec CPU transcriber daemon (SHORT clips). Loads faster-whisper large-v3-turbo (CT2, CPU int8) ONCE and keeps it warm for the whole callrec lifetime. Used for SHORT recordings where per-call model load (~28-40s) dwarfs the transcription itself. CPU-ONLY — it never touches the NPU, so it coexists with a resident rkllm_server (Qwen-3B): short clips need NO NPU-encoder swap AND no model reload → a 10s clip drops from ~120s to ~20s. (The NPU turbo-encoder path stays in transcribe_cli.py for LONG clips, where the 2.4x encoder speedup beats the swap overhead.) Protocol (line based, like the NPU daemon / spk_embed / rkllm_server): stdin : one absolute wav path per line stdout: "READY" once, then per request "OK " or "ERR " """ import sys import json import base64 import time import os sys.path.insert(0, "/opt/callrec") MODEL = "large-v3-turbo" ROOT = "/opt/callrec/models/whisper" BATCH = int(os.environ.get("WHISPER_BATCH", "8")) from faster_whisper import WhisperModel, BatchedInferencePipeline # CT2 model loaded ONCE (CPU), stays warm. No NPU encoder override -> ct2 runs # its own encoder on the CPU, so nothing here competes with rkllm on the NPU. _model = WhisperModel(MODEL, device="cpu", compute_type="int8", download_root=ROOT, cpu_threads=4) _engine = BatchedInferencePipeline(model=_model) if BATCH > 1 else _model def transcribe(wav): t0 = time.time() if BATCH > 1: segments, info = _engine.transcribe( wav, language="de", beam_size=1, batch_size=BATCH, word_timestamps=True) else: segments, info = _engine.transcribe( wav, language="de", beam_size=1, word_timestamps=True, vad_filter=True) words = [] texts = [] for seg in segments: texts.append(seg.text.strip()) for w in (seg.words or []): words.append({ "word": w.word.strip(), "t_start_ms": int(round((w.start or 0) * 1000)), "t_end_ms": int(round((w.end or 0) * 1000)), "probability": round(w.probability, 3), }) dur = float(info.duration or 0) trans_s = time.time() - t0 return { "text": " ".join(texts).strip(), "words": words, "duration": round(dur, 1), "load_s": 0.0, "transcribe_s": round(trans_s, 1), "rtf": round(trans_s / max(dur, 0.1), 3), "compute": "cpu-daemon", } def main(): print("READY", flush=True) for line in sys.stdin: wav = line.strip() if not wav: continue try: out = transcribe(wav) payload = base64.b64encode( json.dumps(out, ensure_ascii=False).encode("utf-8")).decode("ascii") print("OK " + payload, flush=True) except Exception as e: # noqa: BLE001 msg = base64.b64encode(str(e).encode("utf-8")).decode("ascii") print("ERR " + msg, flush=True) if __name__ == "__main__": main()