import json import os import sqlite3 from datetime import datetime from typing import Optional import requests from fastapi import FastAPI, File, Form, HTTPException, UploadFile from fastapi.responses import HTMLResponse, PlainTextResponse, Response API_BASE = os.getenv("API_BASE", "http://gx10.aquantico.lan:8093").rstrip("/") OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://gx10.aquantico.lan:11434").rstrip("/") OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3.5:9b") DB_PATH = os.getenv("DB_PATH", "/data/ui.db") app = FastAPI(title="Diarization UI") def db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def now_iso() -> str: return datetime.utcnow().isoformat() def init_db(): os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) with db() as c: c.execute( """ CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, created_at TEXT NOT NULL ) """ ) c.execute( """ CREATE TABLE IF NOT EXISTS prompts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, prompt TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) """ ) c.execute( """ CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, kind TEXT NOT NULL, -- transcript|analysis title TEXT NOT NULL, content_md TEXT NOT NULL, source_document_id INTEGER, prompt_id INTEGER, raw_json TEXT, created_at TEXT NOT NULL, FOREIGN KEY(project_id) REFERENCES projects(id), FOREIGN KEY(source_document_id) REFERENCES documents(id), FOREIGN KEY(prompt_id) REFERENCES prompts(id) ) """ ) # defaults c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso())) c.execute( "INSERT OR IGNORE INTO prompts(name, prompt, created_at, updated_at) VALUES (?,?,?,?)", ( "Zusammenfassung", "Erstelle eine prägnante Zusammenfassung des Gesprächs in Stichpunkten.", now_iso(), now_iso(), ), ) c.execute( "INSERT OR IGNORE INTO prompts(name, prompt, created_at, updated_at) VALUES (?,?,?,?)", ( "Aufgaben", "Extrahiere alle Aufgaben. Gib pro Aufgabe: Verantwortlich, Aufgabe, Deadline (falls vorhanden), Priorität.", now_iso(), now_iso(), ), ) def layout(title: str, body: str) -> str: return f"""
Audio wird transkribiert + mit Sprechern angereichert und als Dokument gespeichert.
{f"{msg}
" if msg else ""} """ return layout("Upload", body) @app.post("/projects", response_class=HTMLResponse) def add_project(name: str = Form(...)): with db() as c: c.execute("INSERT INTO projects(name, created_at) VALUES (?,?)", (name.strip(), now_iso())) return HTMLResponse("") @app.post("/projects/update", response_class=HTMLResponse) def rename_project(id: int = Form(...), name: str = Form(...)): with db() as c: c.execute("UPDATE projects SET name=? WHERE id=?", (name.strip(), id)) return HTMLResponse("") @app.post("/projects/{project_id}/delete", response_class=HTMLResponse) def delete_project(project_id: int): with db() as c: default = c.execute("SELECT id FROM projects WHERE name='Default'").fetchone() if not default: c.execute("INSERT INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso())) default_id = c.execute("SELECT id FROM projects WHERE name='Default'").fetchone()[0] else: default_id = default[0] if project_id == default_id: return HTMLResponse("") c.execute("UPDATE documents SET project_id=? WHERE project_id=?", (default_id, project_id)) c.execute("DELETE FROM projects WHERE id=?", (project_id,)) return HTMLResponse("") @app.post("/upload", response_class=HTMLResponse) async def upload(project_id: int = Form(...), title: str = Form(""), file: UploadFile = File(...)): data = await file.read() if not data: raise HTTPException(400, "Leere Datei") files = {"file": (file.filename or "audio.bin", data, file.content_type or "application/octet-stream")} r = requests.post(f"{API_BASE}/transcribe-diarize", files=files, timeout=1800) if r.status_code >= 400: raise HTTPException(r.status_code, r.text) payload = r.json() content_md = payload.get("formatted_text", "") doc_title = (title or "").strip() or (file.filename or "Transkript") with db() as c: cur = c.execute( "INSERT INTO documents(project_id, kind, title, content_md, raw_json, created_at) VALUES (?,?,?,?,?,?)", (project_id, "transcript", doc_title, content_md, json.dumps(payload, ensure_ascii=False), now_iso()), ) doc_id = cur.lastrowid return HTMLResponse(f"") @app.get("/library", response_class=HTMLResponse) def library(project_id: Optional[int] = None): with db() as c: projects = c.execute("SELECT id,name FROM projects ORDER BY name").fetchall() if project_id: docs = c.execute( """ SELECT d.id,d.kind,d.title,d.created_at,p.name AS project FROM documents d JOIN projects p ON p.id=d.project_id WHERE d.project_id=? ORDER BY d.id DESC """, (project_id,), ).fetchall() else: docs = c.execute( """ SELECT d.id,d.kind,d.title,d.created_at,p.name AS project FROM documents d JOIN projects p ON p.id=d.project_id ORDER BY d.id DESC LIMIT 200 """ ).fetchall() p_opts = "" + "".join( [f"" for p in projects] ) proj_opts = "".join([f"" for p in projects]) items = "".join( [ f"Keine Einträge.
'} """ return layout("Library", body) @app.get("/document/{doc_id}", response_class=HTMLResponse) def view_document(doc_id: int): with db() as c: d = c.execute( """ SELECT d.*, p.name AS project, pr.name AS prompt_name FROM documents d JOIN projects p ON p.id=d.project_id LEFT JOIN prompts pr ON pr.id=d.prompt_id WHERE d.id=? """, (doc_id,), ).fetchone() if not d: raise HTTPException(404, "not found") body = f"""Projekt: {d['project']} · Typ: {d['kind']} · {d['created_at']}
{(d['content_md'] or '').replace('<','<')}
"""
return layout("Dokument", body)
@app.get("/document/{doc_id}/download.md", response_class=PlainTextResponse)
def download_md(doc_id: int):
with db() as c:
d = c.execute("SELECT title,content_md FROM documents WHERE id=?", (doc_id,)).fetchone()
if not d:
raise HTTPException(404, "not found")
base = (d["title"] or f"document_{doc_id}").strip()
safe = "".join(ch if ch.isalnum() or ch in ("-", "_", " ") else "_" for ch in base).strip()
safe = safe.replace(" ", "_") or f"document_{doc_id}"
filename = f"{safe}.md"
return PlainTextResponse(
d["content_md"],
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
@app.post("/document/{doc_id}/rename", response_class=HTMLResponse)
def rename_document(doc_id: int, title: str = Form(...)):
with db() as c:
c.execute("UPDATE documents SET title=? WHERE id=?", (title.strip(), doc_id))
return HTMLResponse("")
@app.post("/document/{doc_id}/move", response_class=HTMLResponse)
def move_document(doc_id: int, project_id: int = Form(...)):
with db() as c:
c.execute("UPDATE documents SET project_id=? WHERE id=?", (project_id, doc_id))
return HTMLResponse("")
@app.post("/document/{doc_id}/delete", response_class=HTMLResponse)
def delete_document(doc_id: int):
with db() as c:
c.execute("DELETE FROM documents WHERE id=?", (doc_id,))
return HTMLResponse("")
@app.get("/prompts", response_class=HTMLResponse)
def prompts_page():
with db() as c:
prompts = c.execute("SELECT * FROM prompts ORDER BY name").fetchall()
projects = c.execute("SELECT id,name FROM projects ORDER BY name").fetchall()
p_list = "".join(
[
f"{(p['prompt'] or '').replace('<','<')}"
f""
f""
f"