update
This commit is contained in:
@@ -15,6 +15,7 @@ import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
import secrets
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import time
|
||||
import unicodedata
|
||||
@@ -791,6 +792,14 @@ def _extract_client_ip(request: Request) -> str:
|
||||
# Messages (practice-scoped)
|
||||
# =====================================================================
|
||||
|
||||
# Serverseitige Chat-Aufbewahrung: beim Laden von internem Chat
|
||||
# (empfang_nachrichten.json) und Cross-Praxis-DM (empfang_external_messages.json)
|
||||
# werden Nachrichten aelter als diese Tage aus dem jeweiligen Store ENTFERNT und
|
||||
# die Datei ggf. neu geschrieben (siehe _load_messages / _load_external_dm_messages).
|
||||
# Hinweis: Anhangsdateien unter data/empfang_attachments werden dabei NICHT
|
||||
# automatisch geloescht (separater Orphan-Pfad / kuenftige Wartung).
|
||||
# Aufgaben (empfang_tasks.json), Notizen (empfang_user_notes.json) und
|
||||
# Arzt-Brieflogik ausserhalb dieser Stores sind hiervon nicht betroffen.
|
||||
EMPFANG_MESSAGE_RETENTION_DAYS = 14
|
||||
|
||||
|
||||
@@ -7684,13 +7693,14 @@ def _audio_suffix_for_attachment(att: dict) -> str:
|
||||
return ".webm"
|
||||
|
||||
|
||||
def _empfang_transcribe_openai_from_bytes(
|
||||
def _empfang_transcribe_openai_from_bytes_core(
|
||||
audio_bytes: bytes,
|
||||
*,
|
||||
filename_suffix: str = ".webm",
|
||||
) -> str:
|
||||
"""Eine Audiodatei transkribieren (OpenAI wie backend /v1/transcribe). Tempfile wird geloescht."""
|
||||
) -> Tuple[str, float]:
|
||||
"""OpenAI-Transkription (Kernpfad). Tempfile wird geloescht. Zweiter Rueckgabewert: geschaetzte Audiodauer (s)."""
|
||||
import backend_main as bm
|
||||
from aza_ai_budget import estimate_audio_seconds_for_transcription
|
||||
|
||||
tmp_path: Optional[str] = None
|
||||
try:
|
||||
@@ -7733,7 +7743,12 @@ def _empfang_transcribe_openai_from_bytes(
|
||||
text = bm.apply_medical_corrections(text, "")
|
||||
text = bm.apply_medical_post_corrections(text)
|
||||
text = bm.apply_medication_fuzzy_corrections(text)
|
||||
return (text or "").strip()
|
||||
audio_sec = estimate_audio_seconds_for_transcription(
|
||||
byte_size=len(audio_bytes),
|
||||
file_path=tmp_path,
|
||||
suffix=filename_suffix,
|
||||
)
|
||||
return (text or "").strip(), float(audio_sec)
|
||||
finally:
|
||||
if tmp_path:
|
||||
try:
|
||||
@@ -7742,6 +7757,133 @@ def _empfang_transcribe_openai_from_bytes(
|
||||
pass
|
||||
|
||||
|
||||
def _empfang_transcribe_openai_budgeted(
|
||||
request: Request,
|
||||
audio_bytes: bytes,
|
||||
*,
|
||||
practice_id: str,
|
||||
filename_suffix: str = ".webm",
|
||||
) -> Tuple[Optional[JSONResponse], Optional[str]]:
|
||||
"""KI-Budget Gate + Recording fuer Empfang-Transkription; (JSONResponse-402, None) oder (None, transcript)."""
|
||||
import backend_main as bm
|
||||
from aza_ai_budget import (
|
||||
budget_gate_blocked_payload_or_none,
|
||||
ensure_ai_budget_schema,
|
||||
record_openai_error_event,
|
||||
record_success_after_openai,
|
||||
resolve_license_for_empfang,
|
||||
)
|
||||
|
||||
request_id = f"ef_tr_{uuid.uuid4().hex[:12]}"
|
||||
db_path = bm._stripe_db_path()
|
||||
x_dev = (request.headers.get("X-Device-Id") or "").strip() or None
|
||||
|
||||
if not db_path.exists():
|
||||
try:
|
||||
text, _sec = _empfang_transcribe_openai_from_bytes_core(
|
||||
audio_bytes, filename_suffix=filename_suffix
|
||||
)
|
||||
return None, text
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
lic = None
|
||||
try:
|
||||
bm.ensure_license_schema(db_path)
|
||||
with sqlite3.connect(str(db_path)) as con:
|
||||
ensure_ai_budget_schema(con)
|
||||
lic = resolve_license_for_empfang(
|
||||
con, x_device_id=x_dev, session_practice_id=(practice_id or "").strip()
|
||||
)
|
||||
if lic is None:
|
||||
return (
|
||||
JSONResponse(
|
||||
status_code=402,
|
||||
content={
|
||||
"success": False,
|
||||
"error_code": "AI_BUDGET_NO_LICENSE_MAPPING",
|
||||
"message_user": (
|
||||
"KI-Kontingent konnte nicht zugeordnet werden (keine aktive Lizenz fuer diese Praxis).\n"
|
||||
"Bitte wenden Sie sich an den Praxisadministrator."
|
||||
),
|
||||
"available_percent": 0,
|
||||
"period_end": 0,
|
||||
"request_id": request_id,
|
||||
},
|
||||
),
|
||||
None,
|
||||
)
|
||||
blocked = budget_gate_blocked_payload_or_none(
|
||||
con,
|
||||
lic,
|
||||
device_id=x_dev,
|
||||
request_id=request_id,
|
||||
operation_type="transcription",
|
||||
model=bm.TRANSCRIBE_MODEL,
|
||||
gate_meta={"route": "empfang_transcribe"},
|
||||
)
|
||||
if blocked is not None:
|
||||
return JSONResponse(status_code=402, content=blocked), None
|
||||
except Exception as exc:
|
||||
_log.warning("EMPFANG_AI_BUDGET_GATE err=%s", exc)
|
||||
lic = None
|
||||
|
||||
if lic is None:
|
||||
try:
|
||||
text, _s = _empfang_transcribe_openai_from_bytes_core(
|
||||
audio_bytes, filename_suffix=filename_suffix
|
||||
)
|
||||
return None, text
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
try:
|
||||
text, audio_sec = _empfang_transcribe_openai_from_bytes_core(
|
||||
audio_bytes, filename_suffix=filename_suffix
|
||||
)
|
||||
except Exception as exc:
|
||||
try:
|
||||
bm.ensure_license_schema(db_path)
|
||||
with sqlite3.connect(str(db_path)) as con:
|
||||
record_openai_error_event(
|
||||
con,
|
||||
lic,
|
||||
device_id=x_dev,
|
||||
request_id=request_id,
|
||||
model=bm.TRANSCRIBE_MODEL,
|
||||
operation_type="transcription",
|
||||
error_code=type(exc).__name__,
|
||||
meta={"route": "empfang_transcribe", "nbytes": len(audio_bytes)},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
try:
|
||||
bm.ensure_license_schema(db_path)
|
||||
with sqlite3.connect(str(db_path)) as con:
|
||||
record_success_after_openai(
|
||||
con,
|
||||
lic,
|
||||
device_id=x_dev,
|
||||
request_id=request_id,
|
||||
model=bm.TRANSCRIBE_MODEL,
|
||||
operation_type="transcription",
|
||||
input_tokens=0,
|
||||
output_tokens=0,
|
||||
total_tokens=0,
|
||||
audio_seconds=audio_sec,
|
||||
)
|
||||
except Exception as rec_exc:
|
||||
_log.warning("EMPFANG_AI_BUDGET_RECORD ok err=%s", rec_exc)
|
||||
|
||||
return None, text
|
||||
|
||||
|
||||
@router.post("/messages/{msg_id}/transcribe-audio")
|
||||
async def empfang_message_transcribe_audio(msg_id: str, request: Request):
|
||||
"""Transkribiert das erste Audio-Attachment einer Nachricht; speichert ``transcript`` am Attachment."""
|
||||
@@ -7794,7 +7936,11 @@ async def empfang_message_transcribe_audio(msg_id: str, request: Request):
|
||||
raise HTTPException(status_code=413, detail="Audiodatei zu gross")
|
||||
suffix = _audio_suffix_for_attachment(att)
|
||||
try:
|
||||
transcript = _empfang_transcribe_openai_from_bytes(raw, filename_suffix=suffix)
|
||||
budget_resp, transcript = _empfang_transcribe_openai_budgeted(
|
||||
request, raw, practice_id=pid, filename_suffix=suffix
|
||||
)
|
||||
if budget_resp is not None:
|
||||
return budget_resp
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
@@ -7941,7 +8087,11 @@ async def empfang_external_dm_transcribe_audio(msg_id: str, request: Request):
|
||||
att_for_suffix["mime"] = str(att.get("mime_type") or "")
|
||||
suffix = _audio_suffix_for_attachment(att_for_suffix)
|
||||
try:
|
||||
transcript = _empfang_transcribe_openai_from_bytes(raw, filename_suffix=suffix)
|
||||
budget_resp, transcript = _empfang_transcribe_openai_budgeted(
|
||||
request, raw, practice_id=my_pid, filename_suffix=suffix
|
||||
)
|
||||
if budget_resp is not None:
|
||||
return budget_resp
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
@@ -8580,6 +8730,26 @@ async def federation_practices(request: Request):
|
||||
# CLEANUP + PRACTICE INFO
|
||||
# =====================================================================
|
||||
|
||||
@router.get("/retention-info")
|
||||
async def empfang_retention_info():
|
||||
"""Nur Metadaten zur Aufbewahrungsregel (keine Inhalte, keine Loeschen-Aktion)."""
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"chat_message_retention_days": EMPFANG_MESSAGE_RETENTION_DAYS,
|
||||
"auto_prune_internal_messages_on_api_load": True,
|
||||
"auto_prune_external_dm_on_api_load": True,
|
||||
"storage_internal_messages": "empfang_nachrichten.json",
|
||||
"storage_external_dm": "empfang_external_messages.json",
|
||||
"attachment_auto_delete_with_message": False,
|
||||
"not_in_scope_note": (
|
||||
"Briefe, Aufgaben und Notizen liegen in anderen Speichern "
|
||||
"und werden durch diese Chat-Retention nicht geloescht."
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/cleanup")
|
||||
async def empfang_cleanup(request: Request):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user