mirror of
https://github.com/Jeuners/Logpy-AgentOne.git
synced 2026-09-09 15:02:33 +02:00
Der Fehler "[Errno 2] No such file or directory: '...stt_out_*.json'" sah wie ein Transkriptions-Bug aus, war aber ein Timing-Problem: _fertig() prüfte nur, ob die Dateigröße 2 Sekunden (RUHE_S) unveraendert blieb - eine normale Sprechpause reicht dafuer schon aus, waehrend FreeSWITCH den Anruf noch aktiv aufnimmt. Belegt im Verlauf-Log: die Aufnahme wurde um 10:15:56 als "fertig" gegriffen, der Anrufer legte laut Log aber erst um 10:16:01 auf. ffmpeg/whisper-cli lasen dabei einen mitten im Schreiben befindlichen RIFF-Header, produzierten keine Ausgabe, und process_call brach mit einem FileNotFoundError auf die erwartete JSON-Datei ab. Die Original-Audiodatei selbst blieb dabei unbeschaedigt (FreeSWITCH schrieb einfach in die schon nach telefon/fehler/ verschobene Datei weiter) - deshalb sah eine manuelle Nachpruefung der "kaputten" Datei immer intakt aus und liess sich nicht reproduzieren. _fertig() prueft jetzt zusaetzlich per `lsof -t`, ob noch ein Prozess die Datei offen haelt - nur wenn weder die Groesse waechst noch ein Handle offen ist, gilt die Aufnahme als abgeschlossen. Nebenbei: tempfile.mkstemp() in transcribe.py gab einen offenen File- Descriptor zurueck, der nie geschlossen wurde (fd-Leak in beiden Tempdatei-Erzeugungen) - behoben. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PuP3zfgYqhnrwyEne2ejG7
153 lines
5.4 KiB
Python
153 lines
5.4 KiB
Python
"""Beobachtet den Eingangsordner und schiebt neue Aufnahmen durch die Pipe.
|
|
|
|
python3 -m pipe.watch [--einmal]
|
|
|
|
Die Telefonanlage legt Aufnahmen in ``telefon/eingang`` ab. Der Dateiname trägt
|
|
die Rufnummer des Anrufers (CLIP), damit sie nicht verloren geht:
|
|
|
|
JJJJMMTT-HHMMSS_<nummer>.wav
|
|
|
|
Verarbeitete Aufnahmen wandern nach ``telefon/verarbeitet``, gescheiterte nach
|
|
``telefon/fehler`` — dort bleiben sie liegen, statt still verloren zu gehen.
|
|
|
|
Bewusst mit Polling statt Dateisystem-Ereignissen: eine Praxis bekommt Anrufe im
|
|
Minutenabstand, nicht im Millisekundenabstand, und Polling übersteht Neustarts
|
|
und Netzlaufwerke ohne Sonderfälle.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from . import config, process_call, protokoll
|
|
|
|
TAKT_S = 3 # Wartezeit zwischen zwei Durchläufen
|
|
RUHE_S = 2 # So lange muss die Dateigröße stabil sein (Aufnahme fertig)
|
|
ENDUNGEN = {".wav", ".mp3", ".opus", ".ogg", ".m4a", ".aiff"}
|
|
|
|
# JJJJMMTT-HHMMSS_<nummer>.<ext> - beides optional, damit auch handverlesene
|
|
# Dateien verarbeitet werden.
|
|
_NAME = re.compile(r"^(?:(\d{8})-(\d{6}))?_?(.*)$")
|
|
|
|
|
|
def _nummer_und_zeit(datei: Path) -> tuple[str | None, datetime | None]:
|
|
"""Liest Rufnummer und Aufnahmezeit aus dem Dateinamen."""
|
|
treffer = _NAME.match(datei.stem)
|
|
if not treffer:
|
|
return None, None
|
|
tag, uhrzeit, rest = treffer.groups()
|
|
zeit = None
|
|
if tag and uhrzeit:
|
|
try:
|
|
zeit = datetime.strptime(f"{tag}{uhrzeit}", "%Y%m%d%H%M%S")
|
|
except ValueError:
|
|
zeit = None
|
|
nummer = rest.strip() or None
|
|
if nummer in {"unbekannt", "anonymous", "restricted", "0"}:
|
|
nummer = None
|
|
return nummer, zeit
|
|
|
|
|
|
def _noch_offen(datei: Path) -> bool:
|
|
"""Wahr, wenn ein Prozess (FreeSWITCH) die Datei noch zum Schreiben offen
|
|
haelt. Reine Groessenstabilitaet reicht nicht: eine Sprechpause laesst die
|
|
Datei fuer RUHE_S Sekunden still stehen, obwohl der Anruf noch laeuft - der
|
|
Watcher hat dann eine Aufnahme mitten im Schreiben gegriffen, ffmpeg/
|
|
whisper-cli lasen einen unfertigen RIFF-Header und scheiterten wortlos
|
|
(kein Absturz, nur keine Ausgabedatei - sah wie ein Transkriptions-Bug
|
|
aus, war aber ein Race gegen die laufende Aufnahme)."""
|
|
try:
|
|
r = subprocess.run(["lsof", "-t", str(datei)],
|
|
capture_output=True, text=True, timeout=5)
|
|
return bool(r.stdout.strip())
|
|
except Exception:
|
|
return False # lsof fehlt/schlaegt fehl: nicht blockieren
|
|
|
|
|
|
def _fertig(datei: Path) -> bool:
|
|
"""Wahr, wenn die Datei nicht mehr waechst UND kein Prozess sie noch offen
|
|
haelt - die Aufnahme also wirklich abgeschlossen ist."""
|
|
try:
|
|
groesse = datei.stat().st_size
|
|
except FileNotFoundError:
|
|
return False
|
|
if groesse == 0:
|
|
return False
|
|
time.sleep(RUHE_S)
|
|
try:
|
|
if datei.stat().st_size != groesse:
|
|
return False
|
|
except FileNotFoundError:
|
|
return False
|
|
return not _noch_offen(datei)
|
|
|
|
|
|
def _neue_dateien(eingang: Path):
|
|
for datei in sorted(eingang.iterdir()):
|
|
if datei.is_file() and datei.suffix.lower() in ENDUNGEN and not datei.name.startswith("."):
|
|
yield datei
|
|
|
|
|
|
def verarbeite_datei(datei: Path) -> bool:
|
|
"""Schiebt eine Aufnahme durch die Pipe und räumt sie weg. True bei Erfolg."""
|
|
nummer, zeit = _nummer_und_zeit(datei)
|
|
print(f"\n▸ Neue Aufnahme: {datei.name}"
|
|
f"{f' (Rufnummer {nummer})' if nummer else ' (Rufnummer unbekannt)'}")
|
|
protokoll.schreibe("eingang", f"Neue Aufnahme {datei.name}", nummer=nummer)
|
|
try:
|
|
process_call.verarbeite(str(datei), anrufer_nummer=nummer, empfangen=zeit)
|
|
except Exception as fehler:
|
|
print(f"✖ Verarbeitung fehlgeschlagen: {fehler}", file=sys.stderr)
|
|
protokoll.schreibe("fehler", f"{datei.name}: {fehler}", nummer=nummer)
|
|
config.TELEFON_FEHLER.mkdir(parents=True, exist_ok=True)
|
|
shutil.move(str(datei), config.TELEFON_FEHLER / datei.name)
|
|
print(f" Aufnahme liegt in {config.TELEFON_FEHLER}/{datei.name} und geht nicht verloren.")
|
|
return False
|
|
config.TELEFON_VERARBEITET.mkdir(parents=True, exist_ok=True)
|
|
shutil.move(str(datei), config.TELEFON_VERARBEITET / datei.name)
|
|
return True
|
|
|
|
|
|
def durchlauf(eingang: Path) -> int:
|
|
"""Verarbeitet alles, was gerade fertig im Eingang liegt."""
|
|
zahl = 0
|
|
for datei in _neue_dateien(eingang):
|
|
if _fertig(datei):
|
|
verarbeite_datei(datei)
|
|
zahl += 1
|
|
return zahl
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
p = argparse.ArgumentParser(description="Eingangsordner beobachten und Anrufe verarbeiten.")
|
|
p.add_argument("--einmal", action="store_true",
|
|
help="nur einmal durchlaufen statt dauerhaft beobachten")
|
|
args = p.parse_args(argv)
|
|
|
|
eingang = config.TELEFON_EINGANG
|
|
eingang.mkdir(parents=True, exist_ok=True)
|
|
|
|
if args.einmal:
|
|
zahl = durchlauf(eingang)
|
|
print(f"{zahl} Aufnahme(n) verarbeitet.")
|
|
return 0
|
|
|
|
print(f"Beobachte {eingang} — Abbruch mit Strg-C")
|
|
protokoll.schreibe("system", "Watcher gestartet")
|
|
try:
|
|
while True:
|
|
durchlauf(eingang)
|
|
time.sleep(TAKT_S)
|
|
except KeyboardInterrupt:
|
|
print("\nBeobachtung beendet.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|