feat(diarization-ui): add project-batch execution and user_prompt to run page

- New tab layout on /run: single-document vs. project-batch mode
- POST /run/project enqueues one analysis job per transcript in a project
- Optional user_prompt field on both modes for extra context to the LLM
- DB migration: adds user_prompt column to jobs table
- LLM prompt builder includes ZUSATZINFOS block when user_prompt is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 09:56:37 +01:00
parent ed3c616676
commit 53836b136f

83
app.py
View File

@@ -95,6 +95,12 @@ def init_db():
""" """
) )
# migrations
try:
c.execute("ALTER TABLE jobs ADD COLUMN user_prompt TEXT")
except Exception:
pass
# defaults # defaults
c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso())) c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso()))
c.execute( c.execute(
@@ -342,10 +348,12 @@ def _process_analysis_job(job_id: int):
if not doc or not prm: if not doc or not prm:
raise RuntimeError("Dokument oder Prompt nicht gefunden") raise RuntimeError("Dokument oder Prompt nicht gefunden")
user_extra = (j.get("user_prompt") or "").strip()
llm_prompt = ( llm_prompt = (
"Du bist ein präziser Assistent. Antworte auf Deutsch.\\n" "Du bist ein präziser Assistent. Antworte auf Deutsch.\\n"
f"AUFTRAG:\\n{prm['prompt']}\\n\\n" f"AUFTRAG:\\n{prm['prompt']}\\n"
f"TEXT:\\n{doc['content_md']}\\n" + (f"\\nZUSATZINFOS:\\n{user_extra}\\n" if user_extra else "")
+ f"\\nTEXT:\\n{doc['content_md']}\\n"
) )
r = requests.post( r = requests.post(
@@ -389,8 +397,8 @@ def enqueue_job(kind: str, **kwargs) -> int:
with db() as c: with db() as c:
cur = c.execute( cur = c.execute(
""" """
INSERT INTO jobs(kind,status,project_id,document_id,prompt_id,title,file_path,created_at) INSERT INTO jobs(kind,status,project_id,document_id,prompt_id,title,file_path,user_prompt,created_at)
VALUES (?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?)
""", """,
( (
kind, kind,
@@ -400,6 +408,7 @@ def enqueue_job(kind: str, **kwargs) -> int:
kwargs.get("prompt_id"), kwargs.get("prompt_id"),
kwargs.get("title"), kwargs.get("title"),
kwargs.get("file_path"), kwargs.get("file_path"),
kwargs.get("user_prompt") or None,
now_iso(), now_iso(),
), ),
) )
@@ -1253,34 +1262,90 @@ def run_page():
with db() as c: with db() as c:
docs = c.execute("SELECT id,title,kind,created_at FROM documents ORDER BY id DESC LIMIT 200").fetchall() 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() 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"<option value='{d['id']}'>#{d['id']} [{d['kind']}] {d['title']}</option>" for d in docs]) d_opts = "".join([f"<option value='{d['id']}'>#{d['id']} [{d['kind']}] {d['title']}</option>" for d in docs])
p_opts = "".join([f"<option value='{p['id']}'>{p['name']}</option>" for p in prompts]) p_opts = "".join([f"<option value='{p['id']}'>{p['name']}</option>" for p in prompts])
proj_opts = "".join([f"<option value='{p['id']}'>{p['name']}</option>" for p in projects])
body = f""" body = f"""
<h2 class='h4 mb-3'>Prompt ausführen</h2> <h2 class='h4 mb-3'>Prompt ausführen</h2>
<ul class='nav nav-tabs mb-3' id='runTabs' role='tablist'>
<li class='nav-item' role='presentation'>
<button class='nav-link active' data-bs-toggle='tab' data-bs-target='#pane-single' type='button' role='tab'><i class='bi bi-file-earmark-text'></i> Einzeldokument</button>
</li>
<li class='nav-item' role='presentation'>
<button class='nav-link' data-bs-toggle='tab' data-bs-target='#pane-project' type='button' role='tab'><i class='bi bi-folder2-open'></i> Projekt-Batch</button>
</li>
</ul>
<div class='tab-content'>
<div class='tab-pane fade show active' id='pane-single' role='tabpanel'>
<form method='post' action='/run' class='card'> <form method='post' action='/run' class='card'>
<div class='mb-2'> <div class='mb-3'>
<label class='form-label'>Dokument</label> <label class='form-label'>Dokument</label>
<select class='form-select' name='document_id'>{d_opts}</select> <select class='form-select' name='document_id'>{d_opts}</select>
</div> </div>
<div class='mb-2'> <div class='mb-3'>
<label class='form-label'>Prompt</label> <label class='form-label'>Prompt</label>
<select class='form-select' name='prompt_id'>{p_opts}</select> <select class='form-select' name='prompt_id'>{p_opts}</select>
</div> </div>
<button class='btn btn-primary' type='submit'>Ausführen (Qwen)</button> <div class='mb-3'>
<label class='form-label'>Zusatzinfos <span class='text-secondary fw-normal'>(optional — wird dem LLM zusätzlich mitgegeben)</span></label>
<textarea class='form-control' name='user_prompt' rows='3' placeholder='z. B. Fokus auf Entscheidungen, Kontext zum Meeting …'></textarea>
</div>
<button class='btn btn-primary' type='submit'><i class='bi bi-play-circle'></i> Ausführen</button>
</form> </form>
</div>
<div class='tab-pane fade' id='pane-project' role='tabpanel'>
<form method='post' action='/run/project' class='card'>
<div class='text-secondary small mb-3'>Führt den gewählten Prompt für <strong>alle Transkripte</strong> des Projekts aus — je Transkript ein Job.</div>
<div class='mb-3'>
<label class='form-label'>Projekt</label>
<select class='form-select' name='project_id'>{proj_opts}</select>
</div>
<div class='mb-3'>
<label class='form-label'>Prompt</label>
<select class='form-select' name='prompt_id'>{p_opts}</select>
</div>
<div class='mb-3'>
<label class='form-label'>Zusatzinfos <span class='text-secondary fw-normal'>(optional — wird für alle Jobs mitgegeben)</span></label>
<textarea class='form-control' name='user_prompt' rows='3' placeholder='z. B. Fokus auf Entscheidungen, Kontext zum Meeting …'></textarea>
</div>
<button class='btn btn-primary' type='submit'><i class='bi bi-play-fill'></i> Alle Transkripte verarbeiten</button>
</form>
</div>
</div>
""" """
return layout("Run", body) return layout("Run", body)
@app.post("/run", response_class=HTMLResponse) @app.post("/run", response_class=HTMLResponse)
def run_prompt(document_id: int = Form(...), prompt_id: int = Form(...)): def run_prompt(document_id: int = Form(...), prompt_id: int = Form(...), user_prompt: str = Form("")):
with db() as c: with db() as c:
doc = c.execute("SELECT id FROM documents WHERE id=?", (document_id,)).fetchone() doc = c.execute("SELECT id FROM documents WHERE id=?", (document_id,)).fetchone()
prm = c.execute("SELECT id FROM prompts WHERE id=?", (prompt_id,)).fetchone() prm = c.execute("SELECT id FROM prompts WHERE id=?", (prompt_id,)).fetchone()
if not doc or not prm: if not doc or not prm:
raise HTTPException(404, "Dokument oder Prompt nicht gefunden") raise HTTPException(404, "Dokument oder Prompt nicht gefunden")
job_id = enqueue_job("analysis", document_id=document_id, prompt_id=prompt_id) job_id = enqueue_job("analysis", document_id=document_id, prompt_id=prompt_id, user_prompt=user_prompt.strip() or None)
return HTMLResponse(f"<meta http-equiv='refresh' content='0; url=/jobs?queued={job_id}'>") return HTMLResponse(f"<meta http-equiv='refresh' content='0; url=/jobs?queued={job_id}'>")
@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("<meta http-equiv='refresh' content='0; url=/jobs'>")