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>
88 lines
3.0 KiB
Python
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()
|