Files
Aime/app/inference/transcribe_daemon_cpu.py
Claude f9e1eb82c4 feat: komplette Codebase vom Board ins Repo gespiegelt
app/ = /opt/callrec (Node-Backend, Vite-Frontend, Python-Inferenz,
Audio-Skripte, rkllm_server.cpp), system/ = systemd-Units,
WirePlumber/PipeWire-Drop-ins, sudoers. Modelle, Binaries, Aufnahmen
und node_modules bleiben draussen (.gitignore).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:18:29 +02:00

88 lines
3.0 KiB
Python

#!/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 <base64(json)>" or "ERR <base64(msg)>"
"""
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()