mirror of
https://github.com/R0m1k3/LiveFlow.git
synced 2026-10-22 14:38:17 +02:00
Fix pause, mobile transcription, and diarization
This commit is contained in:
1 parent
e9f1dd07e2
commit
5a9c2a6239
1 file changed
+45
-10
+45
-10
@@ -19,18 +19,19 @@ ASR_BASE_URL = os.environ.get("ASR_BASE_URL", "http://asr:8000/v1").rstrip("/")
|
|||||||
ASR_MODEL = os.environ.get("ASR_MODEL", "Qwen/Qwen3-ASR-1.7B")
|
ASR_MODEL = os.environ.get("ASR_MODEL", "Qwen/Qwen3-ASR-1.7B")
|
||||||
ASR_API_KEY = os.environ.get("ASR_API_KEY", "sk-local")
|
ASR_API_KEY = os.environ.get("ASR_API_KEY", "sk-local")
|
||||||
ASR_LANGUAGE = os.environ.get("ASR_LANGUAGE", "").strip() or "fr"
|
ASR_LANGUAGE = os.environ.get("ASR_LANGUAGE", "").strip() or "fr"
|
||||||
DIARIZATION = os.environ.get("DIARIZATION", "off").strip().lower() == "on"
|
DIARIZATION = os.environ.get("DIARIZATION", "on").strip().lower() == "on"
|
||||||
DIARIZATION_THRESHOLD = float(os.environ.get("DIARIZATION_THRESHOLD", "0.70"))
|
DIARIZATION_THRESHOLD = float(os.environ.get("DIARIZATION_THRESHOLD", "0.70"))
|
||||||
DIARIZATION_MAX_SPEAKERS = int(os.environ.get("DIARIZATION_MAX_SPEAKERS", "8"))
|
DIARIZATION_MAX_SPEAKERS = int(os.environ.get("DIARIZATION_MAX_SPEAKERS", "8"))
|
||||||
|
|
||||||
db: aiosqlite.Connection | None = None
|
db: aiosqlite.Connection | None = None
|
||||||
http: httpx.AsyncClient | None = None
|
http: httpx.AsyncClient | None = None
|
||||||
embedding_model = None # EmbeddingModel chargé au lifespan si DIARIZATION=True
|
embedding_model = None # EmbeddingModel chargé au lifespan si DIARIZATION=True
|
||||||
|
diarization_error = ""
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
global db, http, embedding_model
|
global db, http, embedding_model, diarization_error
|
||||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
db = await aiosqlite.connect(DB_PATH)
|
db = await aiosqlite.connect(DB_PATH)
|
||||||
db.row_factory = aiosqlite.Row
|
db.row_factory = aiosqlite.Row
|
||||||
@@ -62,10 +63,16 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# Charger le modèle de diarisation (lourd, ~30s au premier lancement)
|
# Charger le modèle de diarisation (lourd, ~30s au premier lancement)
|
||||||
if DIARIZATION:
|
if DIARIZATION:
|
||||||
print("Chargement du modèle de diarisation ECAPA-TDNN…", flush=True)
|
try:
|
||||||
from diarizer import EmbeddingModel
|
print("Chargement du modèle de diarisation ECAPA-TDNN…", flush=True)
|
||||||
embedding_model = await asyncio.to_thread(EmbeddingModel)
|
from diarizer import EmbeddingModel
|
||||||
print("Modèle de diarisation prêt.", flush=True)
|
embedding_model = await asyncio.to_thread(EmbeddingModel)
|
||||||
|
diarization_error = ""
|
||||||
|
print("Modèle de diarisation prêt.", flush=True)
|
||||||
|
except Exception as exc:
|
||||||
|
embedding_model = None
|
||||||
|
diarization_error = str(exc)
|
||||||
|
print(f"Diarisation désactivée : {exc}", flush=True)
|
||||||
|
|
||||||
http = httpx.AsyncClient(timeout=120)
|
http = httpx.AsyncClient(timeout=120)
|
||||||
yield
|
yield
|
||||||
@@ -129,17 +136,27 @@ async def ws_transcribe(ws: WebSocket):
|
|||||||
|
|
||||||
title = (start.get("title") or "").strip() or datetime.now().strftime("Réunion du %d/%m/%Y %H:%M")
|
title = (start.get("title") or "").strip() or datetime.now().strftime("Réunion du %d/%m/%Y %H:%M")
|
||||||
# La diarisation est activée si le serveur la supporte ET le client la demande
|
# La diarisation est activée si le serveur la supporte ET le client la demande
|
||||||
session_diarization = DIARIZATION and start.get("diarization", True)
|
requested_diarization = bool(start.get("diarization", True))
|
||||||
|
session_diarization = DIARIZATION and embedding_model is not None and requested_diarization
|
||||||
cur = await db.execute(
|
cur = await db.execute(
|
||||||
"INSERT INTO meetings (title, created_at) VALUES (?, ?)",
|
"INSERT INTO meetings (title, created_at) VALUES (?, ?)",
|
||||||
(title, datetime.now(timezone.utc).isoformat()),
|
(title, datetime.now(timezone.utc).isoformat()),
|
||||||
)
|
)
|
||||||
meeting_id = cur.lastrowid
|
meeting_id = cur.lastrowid
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await ws.send_json({"type": "ready", "meeting_id": meeting_id, "title": title})
|
await ws.send_json({
|
||||||
|
"type": "ready",
|
||||||
|
"meeting_id": meeting_id,
|
||||||
|
"title": title,
|
||||||
|
"diarization": session_diarization,
|
||||||
|
"diarization_available": DIARIZATION and embedding_model is not None,
|
||||||
|
"diarization_requested": requested_diarization,
|
||||||
|
"diarization_error": diarization_error,
|
||||||
|
})
|
||||||
|
|
||||||
segmenter = SpeechSegmenter()
|
segmenter = SpeechSegmenter()
|
||||||
queue: asyncio.Queue[Segment | None] = asyncio.Queue()
|
queue: asyncio.Queue[Segment | None] = asyncio.Queue()
|
||||||
|
paused = False
|
||||||
|
|
||||||
async def worker():
|
async def worker():
|
||||||
"""Identifie le locuteur puis transcrit, dans l'ordre."""
|
"""Identifie le locuteur puis transcrit, dans l'ordre."""
|
||||||
@@ -193,6 +210,8 @@ async def ws_transcribe(ws: WebSocket):
|
|||||||
if msg["type"] == "websocket.disconnect":
|
if msg["type"] == "websocket.disconnect":
|
||||||
break
|
break
|
||||||
if msg.get("bytes") is not None:
|
if msg.get("bytes") is not None:
|
||||||
|
if paused:
|
||||||
|
continue
|
||||||
for seg in segmenter.feed(msg["bytes"]):
|
for seg in segmenter.feed(msg["bytes"]):
|
||||||
queue.put_nowait(seg)
|
queue.put_nowait(seg)
|
||||||
elif msg.get("text"):
|
elif msg.get("text"):
|
||||||
@@ -207,11 +226,12 @@ async def ws_transcribe(ws: WebSocket):
|
|||||||
await ws.send_json({"type": "done", "meeting_id": meeting_id})
|
await ws.send_json({"type": "done", "meeting_id": meeting_id})
|
||||||
break
|
break
|
||||||
elif cmd == "pause":
|
elif cmd == "pause":
|
||||||
|
paused = True
|
||||||
# Mettre en pause : forcer la finalisation du segment audio en cours
|
# Mettre en pause : forcer la finalisation du segment audio en cours
|
||||||
if (last := segmenter.flush()) is not None:
|
if (last := segmenter.flush()) is not None:
|
||||||
queue.put_nowait(last)
|
queue.put_nowait(last)
|
||||||
elif cmd == "resume":
|
elif cmd == "resume":
|
||||||
pass
|
paused = False
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
@@ -228,6 +248,20 @@ async def ws_transcribe(ws: WebSocket):
|
|||||||
|
|
||||||
# --------------------------------------------------------------------- API
|
# --------------------------------------------------------------------- API
|
||||||
|
|
||||||
|
@app.get("/api/capabilities")
|
||||||
|
async def capabilities():
|
||||||
|
return {
|
||||||
|
"diarization": {
|
||||||
|
"enabled": DIARIZATION,
|
||||||
|
"ready": embedding_model is not None,
|
||||||
|
"available": DIARIZATION and embedding_model is not None,
|
||||||
|
"error": diarization_error,
|
||||||
|
"threshold": DIARIZATION_THRESHOLD,
|
||||||
|
"max_speakers": DIARIZATION_MAX_SPEAKERS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/meetings")
|
@app.get("/api/meetings")
|
||||||
async def list_meetings():
|
async def list_meetings():
|
||||||
rows = await db.execute_fetchall(
|
rows = await db.execute_fetchall(
|
||||||
@@ -309,7 +343,8 @@ async def export_meeting(meeting_id: int, format: str = "txt"):
|
|||||||
return f"[{s['speaker']}] {s['text']}" if s.get("speaker") else s["text"]
|
return f"[{s['speaker']}] {s['text']}" if s.get("speaker") else s["text"]
|
||||||
body, mime, ext = "\n".join(_txt_line(s) for s in segs) + "\n", "text/plain", "txt"
|
body, mime, ext = "\n".join(_txt_line(s) for s in segs) + "\n", "text/plain", "txt"
|
||||||
else:
|
else:
|
||||||
raise HTTPException(400, "Format inconnu (txt, md, srt, json)")
|
raise HTTPException(400, "Format inconnu (txt, md, srt, json)
|
||||||
|
")
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
content=body,
|
content=body,
|
||||||
|
|||||||
Reference in new issue
Block a user