update
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import aiosqlite
|
import aiosqlite
|
||||||
from config import DB_PATH
|
from config import DB_PATH, STORAGE_DIR
|
||||||
|
from paths import final_paths
|
||||||
|
|
||||||
CREATE_GUIDES = """
|
CREATE_GUIDES = """
|
||||||
CREATE TABLE IF NOT EXISTS guides (
|
CREATE TABLE IF NOT EXISTS guides (
|
||||||
@@ -10,8 +11,6 @@ CREATE TABLE IF NOT EXISTS guides (
|
|||||||
status TEXT NOT NULL DEFAULT 'queued',
|
status TEXT NOT NULL DEFAULT 'queued',
|
||||||
progress TEXT,
|
progress TEXT,
|
||||||
error_msg TEXT,
|
error_msg TEXT,
|
||||||
html_path TEXT,
|
|
||||||
pdf_path TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
)
|
)
|
||||||
@@ -68,6 +67,20 @@ async def init_db():
|
|||||||
"WHERE status IN ('queued', 'generating')"
|
"WHERE status IN ('queued', 'generating')"
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
await _migrate_uuid_filenames(db)
|
||||||
|
|
||||||
|
|
||||||
|
async def _migrate_uuid_filenames(db: aiosqlite.Connection) -> None:
|
||||||
|
cursor = await db.execute("SELECT id, topic, format FROM guides WHERE status = 'done'")
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
for guide_id, topic, format_name in rows:
|
||||||
|
final_html, final_pdf = final_paths(topic, format_name)
|
||||||
|
old_html = STORAGE_DIR / "html" / f"{guide_id}.html"
|
||||||
|
old_pdf = STORAGE_DIR / "pdf" / f"{guide_id}.pdf"
|
||||||
|
if old_html.exists() and not final_html.exists():
|
||||||
|
old_html.rename(final_html)
|
||||||
|
if old_pdf.exists() and not final_pdf.exists():
|
||||||
|
old_pdf.rename(final_pdf)
|
||||||
|
|
||||||
|
|
||||||
async def close_db():
|
async def close_db():
|
||||||
@@ -85,8 +98,8 @@ def _row_to_dict(row, cursor):
|
|||||||
async def create_guide(guide: dict) -> dict:
|
async def create_guide(guide: dict) -> dict:
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""INSERT INTO guides (id, topic, format, instructions, status, progress, html_path, pdf_path, created_at, updated_at)
|
"""INSERT INTO guides (id, topic, format, instructions, status, progress, created_at, updated_at)
|
||||||
VALUES (:id, :topic, :format, :instructions, :status, :progress, :html_path, :pdf_path, :created_at, :updated_at)""",
|
VALUES (:id, :topic, :format, :instructions, :status, :progress, :created_at, :updated_at)""",
|
||||||
guide,
|
guide,
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
@@ -22,6 +23,7 @@ from database import (
|
|||||||
list_bausteine,
|
list_bausteine,
|
||||||
update_baustein,
|
update_baustein,
|
||||||
)
|
)
|
||||||
|
from paths import final_paths, temp_paths
|
||||||
|
|
||||||
_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS)
|
_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS)
|
||||||
_active_processes: dict[str, asyncio.subprocess.Process] = {}
|
_active_processes: dict[str, asyncio.subprocess.Process] = {}
|
||||||
@@ -174,8 +176,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(guide_id, status="generating", progress="Recherche…", updated_at=now)
|
await update_guide(guide_id, status="generating", progress="Recherche…", updated_at=now)
|
||||||
|
|
||||||
html_path = STORAGE_DIR / "html" / f"{guide_id}.html"
|
html_path, pdf_path = final_paths(topic, format_name)
|
||||||
pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if guide_id in _cancelled:
|
if guide_id in _cancelled:
|
||||||
@@ -241,12 +242,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
|
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(
|
await update_guide(
|
||||||
guide_id,
|
guide_id, status="done", progress=None, updated_at=now,
|
||||||
status="done",
|
|
||||||
progress=None,
|
|
||||||
html_path=str(html_path),
|
|
||||||
pdf_path=str(pdf_path),
|
|
||||||
updated_at=now,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
@@ -263,16 +259,23 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions
|
|||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(guide_id, status="generating", progress="Überarbeite…", updated_at=now)
|
await update_guide(guide_id, status="generating", progress="Überarbeite…", updated_at=now)
|
||||||
|
|
||||||
html_path = STORAGE_DIR / "html" / f"{guide_id}.html"
|
final_html, final_pdf = final_paths(topic, format_name)
|
||||||
pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf"
|
tmp_html, tmp_pdf = temp_paths(guide_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if guide_id in _cancelled:
|
if guide_id in _cancelled:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not final_html.exists():
|
||||||
|
await _fail(guide_id, "Original-HTML nicht gefunden")
|
||||||
|
return
|
||||||
|
|
||||||
|
shutil.copy2(final_html, tmp_html)
|
||||||
|
|
||||||
current_step = "Überarbeitung"
|
current_step = "Überarbeitung"
|
||||||
current_timeout = AGENT_TIMEOUT
|
current_timeout = AGENT_TIMEOUT
|
||||||
|
|
||||||
rework_prompt = _build_rework_prompt(topic, format_name, html_path, instructions)
|
rework_prompt = _build_rework_prompt(topic, format_name, tmp_html, instructions)
|
||||||
returncode, stdout, stderr = await _run_claude(guide_id, rework_prompt, AGENT_TIMEOUT)
|
returncode, stdout, stderr = await _run_claude(guide_id, rework_prompt, AGENT_TIMEOUT)
|
||||||
|
|
||||||
if guide_id in _cancelled:
|
if guide_id in _cancelled:
|
||||||
@@ -281,20 +284,23 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions
|
|||||||
await _fail(guide_id, f"Rework-Fehler: {stderr[:1000]}")
|
await _fail(guide_id, f"Rework-Fehler: {stderr[:1000]}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not html_path.exists():
|
if not tmp_html.exists():
|
||||||
await _fail(guide_id, "HTML-Datei wurde nicht erstellt")
|
await _fail(guide_id, "HTML-Datei wurde nicht erstellt")
|
||||||
return
|
return
|
||||||
|
|
||||||
await _set_progress(guide_id, "Rendere PDF…")
|
await _set_progress(guide_id, "Rendere PDF…")
|
||||||
ok, err = await _render_pdf(html_path, pdf_path)
|
ok, err = await _render_pdf(tmp_html, tmp_pdf)
|
||||||
if not ok:
|
if not ok:
|
||||||
await _fail(guide_id, f"WeasyPrint-Fehler: {err}")
|
await _fail(guide_id, f"WeasyPrint-Fehler: {err}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Atomar: Temp → Final umbenennen
|
||||||
|
tmp_html.replace(final_html)
|
||||||
|
tmp_pdf.replace(final_pdf)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(
|
await update_guide(
|
||||||
guide_id, status="done", progress=None,
|
guide_id, status="done", progress=None, updated_at=now,
|
||||||
html_path=str(html_path), pdf_path=str(pdf_path), updated_at=now,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
@@ -304,6 +310,8 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions
|
|||||||
finally:
|
finally:
|
||||||
_active_processes.pop(guide_id, None)
|
_active_processes.pop(guide_id, None)
|
||||||
_cancelled.discard(guide_id)
|
_cancelled.discard(guide_id)
|
||||||
|
tmp_html.unlink(missing_ok=True)
|
||||||
|
tmp_pdf.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
async def _fail(guide_id: str, msg: str) -> None:
|
async def _fail(guide_id: str, msg: str) -> None:
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ class GuideResponse(BaseModel):
|
|||||||
status: str
|
status: str
|
||||||
progress: str | None = None
|
progress: str | None = None
|
||||||
error_msg: str | None = None
|
error_msg: str | None = None
|
||||||
html_path: str | None = None
|
|
||||||
pdf_path: str | None = None
|
|
||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
|
||||||
|
|||||||
17
backend/paths.py
Normal file
17
backend/paths.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from config import STORAGE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def safe_basename(topic: str, format_name: str) -> str:
|
||||||
|
clean = topic.replace("/", "_").replace("\x00", "")
|
||||||
|
return f"{clean} - {format_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def final_paths(topic: str, format_name: str) -> tuple[Path, Path]:
|
||||||
|
base = safe_basename(topic, format_name)
|
||||||
|
return STORAGE_DIR / "html" / f"{base}.html", STORAGE_DIR / "pdf" / f"{base}.pdf"
|
||||||
|
|
||||||
|
|
||||||
|
def temp_paths(guide_id: str) -> tuple[Path, Path]:
|
||||||
|
return STORAGE_DIR / "html" / f"{guide_id}.tmp.html", STORAGE_DIR / "pdf" / f"{guide_id}.tmp.pdf"
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from config import FORMAT_META, STORAGE_DIR
|
from config import FORMAT_META
|
||||||
from database import (
|
from database import (
|
||||||
create_guide, delete_guide, get_guide, list_guides,
|
create_guide, delete_guide, get_guide, list_guides,
|
||||||
create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein,
|
create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein,
|
||||||
@@ -17,6 +16,7 @@ from models import (
|
|||||||
GuideCreateRequest, GuideReworkRequest, GuideResponse,
|
GuideCreateRequest, GuideReworkRequest, GuideResponse,
|
||||||
BausteinCreateRequest, BausteinResponse, SuggestionResponse,
|
BausteinCreateRequest, BausteinResponse, SuggestionResponse,
|
||||||
)
|
)
|
||||||
|
from paths import final_paths
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
@@ -36,8 +36,6 @@ async def create(req: GuideCreateRequest):
|
|||||||
"instructions": req.instructions.strip(),
|
"instructions": req.instructions.strip(),
|
||||||
"status": "queued",
|
"status": "queued",
|
||||||
"progress": None,
|
"progress": None,
|
||||||
"html_path": None,
|
|
||||||
"pdf_path": None,
|
|
||||||
"created_at": now,
|
"created_at": now,
|
||||||
"updated_at": now,
|
"updated_at": now,
|
||||||
}
|
}
|
||||||
@@ -64,12 +62,12 @@ async def download_html(guide_id: str):
|
|||||||
guide = await get_guide(guide_id)
|
guide = await get_guide(guide_id)
|
||||||
if guide is None:
|
if guide is None:
|
||||||
raise HTTPException(404, "Guide nicht gefunden")
|
raise HTTPException(404, "Guide nicht gefunden")
|
||||||
if guide["status"] != "done" or not guide["html_path"]:
|
if guide["status"] != "done":
|
||||||
raise HTTPException(404, "HTML nicht verfügbar")
|
raise HTTPException(404, "HTML nicht verfügbar")
|
||||||
path = Path(guide["html_path"])
|
html_path, _ = final_paths(guide["topic"], guide["format"])
|
||||||
if not path.exists():
|
if not html_path.exists():
|
||||||
raise HTTPException(404, "Datei nicht gefunden")
|
raise HTTPException(404, "Datei nicht gefunden")
|
||||||
return FileResponse(path, filename=f"{guide['topic']}-{guide['format']}.html", media_type="text/html")
|
return FileResponse(html_path, filename=html_path.name, media_type="text/html")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/guides/{guide_id}/rework")
|
@router.post("/guides/{guide_id}/rework")
|
||||||
@@ -96,12 +94,12 @@ async def download_pdf(guide_id: str):
|
|||||||
guide = await get_guide(guide_id)
|
guide = await get_guide(guide_id)
|
||||||
if guide is None:
|
if guide is None:
|
||||||
raise HTTPException(404, "Guide nicht gefunden")
|
raise HTTPException(404, "Guide nicht gefunden")
|
||||||
if guide["status"] != "done" or not guide["pdf_path"]:
|
if guide["status"] != "done":
|
||||||
raise HTTPException(404, "PDF nicht verfügbar")
|
raise HTTPException(404, "PDF nicht verfügbar")
|
||||||
path = Path(guide["pdf_path"])
|
_, pdf_path = final_paths(guide["topic"], guide["format"])
|
||||||
if not path.exists():
|
if not pdf_path.exists():
|
||||||
raise HTTPException(404, "Datei nicht gefunden")
|
raise HTTPException(404, "Datei nicht gefunden")
|
||||||
return FileResponse(path, filename=f"{guide['topic']}-{guide['format']}.pdf", media_type="application/pdf")
|
return FileResponse(pdf_path, filename=pdf_path.name, media_type="application/pdf")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/guides/{guide_id}")
|
@router.delete("/guides/{guide_id}")
|
||||||
@@ -109,10 +107,9 @@ async def remove(guide_id: str):
|
|||||||
guide = await get_guide(guide_id)
|
guide = await get_guide(guide_id)
|
||||||
if guide is None:
|
if guide is None:
|
||||||
raise HTTPException(404, "Guide nicht gefunden")
|
raise HTTPException(404, "Guide nicht gefunden")
|
||||||
if guide["html_path"]:
|
html_path, pdf_path = final_paths(guide["topic"], guide["format"])
|
||||||
Path(guide["html_path"]).unlink(missing_ok=True)
|
html_path.unlink(missing_ok=True)
|
||||||
if guide["pdf_path"]:
|
pdf_path.unlink(missing_ok=True)
|
||||||
Path(guide["pdf_path"]).unlink(missing_ok=True)
|
|
||||||
await delete_guide(guide_id)
|
await delete_guide(guide_id)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
@@ -165,8 +162,10 @@ async def trigger_suggestions(topic: str):
|
|||||||
guides = await list_guides()
|
guides = await list_guides()
|
||||||
html_paths = []
|
html_paths = []
|
||||||
for g in guides:
|
for g in guides:
|
||||||
if g["topic"] == topic and g["status"] == "done" and g["html_path"]:
|
if g["topic"] == topic and g["status"] == "done":
|
||||||
html_paths.append(Path(g["html_path"]))
|
html_path, _ = final_paths(g["topic"], g["format"])
|
||||||
|
if html_path.exists():
|
||||||
|
html_paths.append(html_path)
|
||||||
asyncio.create_task(generate_suggestions(topic, html_paths))
|
asyncio.create_task(generate_suggestions(topic, html_paths))
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user