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>
This commit is contained in:
Claude
2026-07-16 11:18:29 +02:00
parent 560622e125
commit f9e1eb82c4
61 changed files with 5546 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
#!/opt/aime-venv/bin/python
"""CallRec persistent transcriber daemon.
Loads the faster-whisper large-v3-turbo CT2 model (decoder, CPU) ONCE and keeps
it warm for the whole callrec lifetime — this kills the ~22s CT2 load + ~14s
import cost that the old per-call `transcribe_cli.py` paid on every recording.
The NPU turbo ENCODER (rknn, 1.35 GB NPU memory) is acquired per request and
RELEASED right after, because the RK3588 NPU cannot hold the encoder and the
Qwen-3B LLM (rkllm_server) at the same time (proven: mali OOM). Freeing it here
lets rkllm load for the analysis that follows. The rknn file stays in the page
cache, so re-init after the first request is a few seconds, not the cold ~17s.
Protocol (line based, like spk_embed / rkllm_server):
stdin : one absolute wav path per line
stdout: "READY" once, then per request "OK <base64(json)>" or "ERR <base64(msg)>"
The JSON is base64-wrapped so newlines inside it never break the line protocol.
"""
import sys
import json
import base64
import time
import numpy as np
sys.path.insert(0, "/opt/callrec")
MODEL = "large-v3-turbo"
ROOT = "/opt/callrec/models/whisper"
RKNN = "/opt/callrec/models/turbo-encoder-fp16.rknn"
from faster_whisper import WhisperModel
from faster_whisper.transcribe import get_ctranslate2_storage
from inference.rknpu_whisper_encoder import RknnWhisperEncoder
# CT2 decoder loaded ONCE, stays warm.
_model = WhisperModel(MODEL, device="cpu", compute_type="int8",
download_root=ROOT, cpu_threads=4)
def transcribe(wav):
t_load = time.time()
npu = RknnWhisperEncoder(RKNN, lib_path="/usr/lib/librknnrt.so")
load_s = time.time() - t_load
def npu_encode(features):
feats = np.asarray(features, dtype=np.float32)
return get_ctranslate2_storage(np.ascontiguousarray(npu.run(feats)))
_model.encode = npu_encode
try:
t0 = time.time()
segments, info = _model.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": round(load_s, 1),
"transcribe_s": round(trans_s, 1),
"rtf": round(trans_s / max(dur, 0.1), 3),
"compute": "npu",
}
finally:
# Release the NPU encoder so rkllm can load for the analysis step.
npu.close()
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()