Mehrere Audiodateien gleichzeitig hochladen — je Datei wird ein Transkriptions-Job erstellt.
{f"
{msg}
" if msg else ""}
Mehrere Dateien: Strg / Cmd gedrückt halten beim Auswählen
Warteschlange:
"""
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("/projects/create-api")
def create_project_api(name: str = Form(...)):
name = name.strip()
if not name:
raise HTTPException(400, "Name darf nicht leer sein")
with db() as c:
existing = c.execute("SELECT id FROM projects WHERE name=?", (name,)).fetchone()
if existing:
return {"id": existing["id"]}
cur = c.execute("INSERT INTO projects(name, created_at) VALUES (?,?)", (name, now_iso()))
return {"id": cur.lastrowid}
@app.post("/upload")
async def upload(project_id: int = Form(...), file: UploadFile = File(...)):
data = await file.read()
if not data:
raise HTTPException(400, "Leere Datei")
JOB_DIR.mkdir(parents=True, exist_ok=True)
filename = (file.filename or "audio.bin").replace("/", "_")
stem = filename.rsplit(".", 1)[0] or filename
temp_path = JOB_DIR / f"{now_iso().replace(':','-')}_{filename}"
temp_path.write_bytes(data)
job_id = enqueue_job(
"upload",
project_id=project_id,
title=stem,
file_path=str(temp_path),
)
return {"job_id": job_id}
@app.get("/library", response_class=HTMLResponse)
def library(project_id: Optional[str] = None, q_title: str = "", q_content: str = ""):
title_q = (q_title or "").strip()
content_q = (q_content or "").strip()
project_id_int = int(project_id) if (project_id and str(project_id).strip()) else None
where = []
params = []
if project_id_int:
where.append("d.project_id=?")
params.append(project_id_int)
if title_q:
where.append("LOWER(d.title) LIKE LOWER(?)")
params.append(f"%{title_q}%")
if content_q:
where.append("LOWER(d.content_md) LIKE LOWER(?)")
params.append(f"%{content_q}%")
where_sql = ("WHERE " + " AND ".join(where)) if where else ""
with db() as c:
projects = c.execute("SELECT id,name FROM projects ORDER BY name").fetchall()
docs = c.execute(
f"""
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_sql}
ORDER BY d.id DESC LIMIT 500
""",
tuple(params),
).fetchall()
p_opts = "" + "".join(
[f"" for p in projects]
)
rows = "".join(
[
f"
"
for d in docs
]
)
project_js = json.dumps([{"value": p["id"], "label": p["name"]} for p in projects], ensure_ascii=False)
body = f"""
Projekte · Dokumente
{len(docs)} Treffer
Ansicht im Projektlisten-Stil mit Schnellaktionen.
0 ausgewählt
ID
Titel
Typ
Projekt
Erstellt
Aktionen
{rows or "
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")
rendered = md.markdown(d["content_md"] or "", extensions=["fenced_code", "tables", "nl2br"])
projects = get_projects()
content_escaped = (d['content_md'] or '').replace('`', '`').replace('', '<\\/script>')
body = f"""
"""
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}/edit", response_class=HTMLResponse)
def edit_document(doc_id: int, content_md: str = Form(...)):
with db() as c:
c.execute("UPDATE documents SET content_md=? WHERE id=?", (content_md, doc_id))
return HTMLResponse(f"")
@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.post("/documents/bulk-move", response_class=HTMLResponse)
def bulk_move_documents(ids: List[int] = Form(...), project_id: int = Form(...)):
with db() as c:
c.executemany("UPDATE documents SET project_id=? WHERE id=?", [(project_id, i) for i in ids])
return HTMLResponse("")
@app.post("/documents/bulk-delete", response_class=HTMLResponse)
def bulk_delete_documents(ids: List[int] = Form(...)):
with db() as c:
c.executemany("DELETE FROM documents WHERE id=?", [(i,) for i in ids])
return HTMLResponse("")
def _prompt_llm_block(p, def_model, def_think, def_num_ctx, def_num_predict, def_repeat_penalty, def_repeat_last_n):
pid = p["id"]
use_default = int(p["llm_use_default"]) if p["llm_use_default"] is not None else 1
m = p["llm_model"] or def_model
th = p["llm_think"] or def_think
nc = p["llm_num_ctx"] or def_num_ctx
np_ = p["llm_num_predict"] or def_num_predict
rp = p["llm_repeat_penalty"] or def_repeat_penalty
rn = p["llm_repeat_last_n"] or def_repeat_last_n
disabled = "disabled" if use_default else ""
opacity = "opacity:.45;pointer-events:none" if use_default else ""
ctx_opts = [("auto","Automatisch"), ("4096","4 096"), ("8192","8 192"), ("16384","16 384"),
("32768","32 768"), ("65536","65 536"), ("131072","131 072")]
ctx_html = "".join(f"" for v,l in ctx_opts)
think_checked = "checked" if str(th) == "true" else ""
return (
f""
f"
"
f" KI-Einstellungen"
f"
"
f" "
f" "
f"
"
f"
"
f""
f"
"
f"
"
f" "
f"
"
f" "
f" "
f"
"
f"
"
f"
"
f" "
f" "
f"
"
f"
"
f" "
f" "
f"
"
f"
"
f" "
f" "
f"
"
f"
"
f" "
f" "
f"
"
f"
"
f"
"
f" "
f" "
f"
"
f"
"
f"
"
)
@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()
def_model = get_setting("ollama_model", OLLAMA_MODEL)
def_think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false")
def_num_ctx = get_setting("ollama_num_ctx", "auto")
def_num_predict = get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT))
def_repeat_penalty = get_setting("ollama_repeat_penalty", "1.15")
def_repeat_last_n = get_setting("ollama_repeat_last_n", "128")
project_opts = "".join([f"" for p in projects])
project_list = "".join([
f"
"
f"
{p['name']}
Projekt
"
f""
f""
f"
" for p in projects
]) or "
Keine Projekte vorhanden.
"
prompt_list = "".join(
[
f"
"
f"
{p['name']}
#{p['id']}
"
f""
f""
f"
"
for p in prompts
]
) or "
Keine Prompts vorhanden.
"
body = f"""
{len(prompts)} Prompts · {len(projects)} Projekte
Neues Projekt anlegen
{project_list}
Neuen Prompt anlegen
{prompt_list}
Prompt Vorschau
Lade …
Prompt bearbeiten (Vollbild)
"""
return layout("Prompts & Projekte", body)
@app.get("/prompts/{prompt_id}/preview")
def prompt_preview(prompt_id: int):
with db() as c:
p = c.execute("SELECT id,name,prompt FROM prompts WHERE id=?", (prompt_id,)).fetchone()
if not p:
raise HTTPException(404, "Prompt nicht gefunden")
html = md.markdown(p["prompt"] or "", extensions=["fenced_code", "tables", "nl2br"])
return {"id": p["id"], "name": p["name"], "html": html}
@app.post("/prompts/add", response_class=HTMLResponse)
def prompt_add(name: str = Form(...), prompt: str = Form(...)):
with db() as c:
c.execute(
"INSERT INTO prompts(name,prompt,created_at,updated_at) VALUES (?,?,?,?)",
(name.strip(), prompt.strip(), now_iso(), now_iso()),
)
return HTMLResponse("")
@app.post("/prompts/update", response_class=HTMLResponse)
def prompt_update(
id: int = Form(...),
name: str = Form(...),
prompt: str = Form(...),
llm_use_default: int = Form(1),
llm_model: Optional[str] = Form(None),
llm_think: Optional[str] = Form(None),
llm_num_ctx: Optional[str] = Form(None),
llm_num_predict: Optional[str] = Form(None),
llm_repeat_penalty: Optional[str] = Form(None),
llm_repeat_last_n: Optional[str] = Form(None),
):
with db() as c:
c.execute(
"""UPDATE prompts SET name=?, prompt=?, updated_at=?,
llm_use_default=?, llm_model=?, llm_think=?,
llm_num_ctx=?, llm_num_predict=?, llm_repeat_penalty=?, llm_repeat_last_n=?
WHERE id=?""",
(
name.strip(), prompt.strip(), now_iso(),
llm_use_default,
(llm_model or "").strip() or None,
"true" if llm_think == "true" else "false",
(llm_num_ctx or "auto").strip(),
int(float(llm_num_predict)) if llm_num_predict else None,
float(llm_repeat_penalty) if llm_repeat_penalty else None,
int(float(llm_repeat_last_n)) if llm_repeat_last_n else None,
id,
),
)
return HTMLResponse("")
@app.post("/prompts/{prompt_id}/delete", response_class=HTMLResponse)
def prompt_delete(prompt_id: int):
with db() as c:
c.execute("DELETE FROM prompts WHERE id=?", (prompt_id,))
return HTMLResponse("")
def _parse_utcish(ts: Optional[str]) -> Optional[datetime]:
if not ts:
return None
try:
return datetime.fromisoformat(str(ts).replace("Z", "+00:00")).replace(tzinfo=None)
except Exception:
try:
return datetime.fromisoformat(str(ts))
except Exception:
return None
def _fmt_elapsed(start_iso: Optional[str], end_iso: Optional[str] = None) -> str:
s = _parse_utcish(start_iso)
if not s:
return "-"
try:
e = _parse_utcish(end_iso) if end_iso else datetime.utcnow()
if not e:
e = datetime.utcnow()
sec = max(0, int((e - s).total_seconds()))
if sec < 60:
return f"{sec}s"
m, s2 = divmod(sec, 60)
if m < 60:
return f"{m}m {s2}s"
h, m2 = divmod(m, 60)
return f"{h}h {m2}m"
except Exception:
return "-"
def _jobs_payload(limit: int = 200):
with db() as c:
jobs = c.execute(
"""
SELECT j.*, p.name AS project_name, d.title AS document_title, pr.name AS prompt_name
FROM jobs j
LEFT JOIN projects p ON p.id=j.project_id
LEFT JOIN documents d ON d.id=j.document_id
LEFT JOIN prompts pr ON pr.id=j.prompt_id
ORDER BY j.id DESC LIMIT ?
""",
(limit,),
).fetchall()
return [dict(j) for j in jobs]
@app.get("/jobs/data")
def jobs_data(limit: int = 200):
return {"items": _jobs_payload(limit)}
@app.post("/jobs/{job_id}/cancel")
def jobs_cancel(job_id: int):
j = _job_get(job_id)
if not j:
raise HTTPException(404, "job not found")
if j["status"] not in ("done", "error", "cancelled"):
_job_set(job_id, status="cancelled", finished_at=now_iso(), error="Cancelled by user")
return {"ok": True, "status": "cancelled"}
@app.post("/jobs/{job_id}/cancel-form")
def jobs_cancel_form(job_id: int):
jobs_cancel(job_id)
return RedirectResponse(url="/jobs", status_code=303)
@app.post("/jobs/{job_id}/delete")
def jobs_delete(job_id: int):
with db() as c:
c.execute("DELETE FROM jobs WHERE id=?", (job_id,))
return {"ok": True}
@app.post("/jobs/{job_id}/delete-form")
def jobs_delete_form(job_id: int):
jobs_delete(job_id)
return RedirectResponse(url="/jobs", status_code=303)
@app.get("/jobs/{job_id}/debug-data")
def job_debug_data(job_id: int):
j = _job_get(job_id)
if not j:
raise HTTPException(404, "job not found")
with _JOB_STREAM_LOCK:
live = dict(_JOB_STREAMS[job_id]) if job_id in _JOB_STREAMS else None
return {
"status": j["status"],
"kind": j["kind"],
"llm_prompt": j.get("llm_prompt"),
"llm_thinking": live["thinking"] if live is not None else j.get("llm_thinking"),
"llm_response": live["response"] if live is not None else j.get("llm_response"),
"streaming": live is not None,
}
@app.get("/jobs", response_class=HTMLResponse)
def jobs_page(queued: Optional[int] = None):
items = _jobs_payload(200)
def _badge(status: str) -> str:
s = (status or "").lower()
if s == "done":
return "success"
if s in ("running", "queued"):
return "primary"
if s == "cancelled":
return "warning"
return "danger"
cards = []
for it in items:
start_ts = it.get("started_at") or it.get("created_at") or ""
end_ts = it.get("finished_at") or ""
actions = ""
if it["status"] not in ("done", "error", "cancelled"):
actions += f" "
actions += f" "
if it.get("result_document_id"):
actions += f" Ergebnis"
if it["kind"] == "analysis":
actions += f" "
err = f"
"
f"{('Projekt: '+it['project_name']) if it.get('project_name') else ''}"
f"{(' Dokument: '+it['document_title']) if it.get('document_title') else ''}"
f"{(' Prompt: '+it['prompt_name']) if it.get('prompt_name') else ''}"
f"
"
f"
{actions}
"
f"{err}"
f"
"
)
pre = "".join(cards) or "
Keine Jobs.
"
notice = f"
Job #{queued} wurde eingereiht.
" if queued else ""
body = f"""
Hintergrundverarbeitung
Maximal 2 Jobs gleichzeitig. Seite aktualisiert automatisch.
{notice}
0 ausgewählt
{pre}
Debug · Job #
"""
return layout("Jobs", body)
@app.get("/run", response_class=HTMLResponse)
def run_page():
with db() as c:
docs = c.execute("SELECT id,title,kind,created_at FROM documents ORDER BY id DESC LIMIT 200").fetchall()
prompts = c.execute("SELECT id,name FROM prompts ORDER BY name").fetchall()
projects = c.execute("SELECT id,name FROM projects ORDER BY name").fetchall()
d_opts = "".join([f"" for d in docs])
p_opts = "".join([f"" for p in prompts])
proj_opts = "".join([f"" for p in projects])
body = f"""
Prompt ausführen
"""
return layout("Run", body)
@app.post("/run", response_class=HTMLResponse)
def run_prompt(document_id: int = Form(...), prompt_id: int = Form(...), user_prompt: str = Form("")):
with db() as c:
doc = c.execute("SELECT id FROM documents WHERE id=?", (document_id,)).fetchone()
prm = c.execute("SELECT id FROM prompts WHERE id=?", (prompt_id,)).fetchone()
if not doc or not prm:
raise HTTPException(404, "Dokument oder Prompt nicht gefunden")
job_id = enqueue_job("analysis", document_id=document_id, prompt_id=prompt_id, user_prompt=user_prompt.strip() or None)
return HTMLResponse(f"")
@app.post("/run/project", response_class=HTMLResponse)
def run_project(project_id: int = Form(...), prompt_id: int = Form(...), user_prompt: str = Form("")):
with db() as c:
prm = c.execute("SELECT id FROM prompts WHERE id=?", (prompt_id,)).fetchone()
transcripts = c.execute(
"SELECT id FROM documents WHERE project_id=? AND kind='transcript'",
(project_id,),
).fetchall()
if not prm:
raise HTTPException(404, "Prompt nicht gefunden")
if not transcripts:
raise HTTPException(400, "Keine Transkripte im Projekt gefunden")
up = user_prompt.strip() or None
for doc in transcripts:
enqueue_job("analysis", document_id=doc["id"], prompt_id=prompt_id, user_prompt=up)
return HTMLResponse("")