update
This commit is contained in:
9
Makefile
9
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: install dev prod stop logs remove auth
|
||||
.PHONY: install dev prod stop logs remove auth sync
|
||||
|
||||
COMPOSE = docker compose
|
||||
|
||||
@@ -35,3 +35,10 @@ remove: stop
|
||||
@echo "Lösche Datenbank und generierte Dateien..."
|
||||
rm -rf storage/*
|
||||
@echo "Fertig."
|
||||
|
||||
sync:
|
||||
@mkdir -p storage/html storage/pdf
|
||||
rsync -avz --progress root@178.104.67.87:/var/www/guides/storage/guides.db storage/
|
||||
rsync -avz --progress --delete root@178.104.67.87:/var/www/guides/storage/html/ storage/html/
|
||||
rsync -avz --progress --delete root@178.104.67.87:/var/www/guides/storage/pdf/ storage/pdf/
|
||||
@echo "Sync abgeschlossen."
|
||||
|
||||
@@ -27,12 +27,4 @@ FORMAT_META = {
|
||||
AGENT_TIMEOUT = 3600
|
||||
|
||||
MAX_CONCURRENT_GENERATIONS = 6
|
||||
CONTENT_REVIEW_ITERATIONS = {
|
||||
"OnePager": 1,
|
||||
"Cheatsheet": 1,
|
||||
"MiniGuide": 2,
|
||||
"BeginnerGuide": 2,
|
||||
"IntermediateGuide": 2,
|
||||
"ExtendedGuide": 2,
|
||||
}
|
||||
CLAUDE_CLI = "claude"
|
||||
|
||||
@@ -10,7 +10,6 @@ from pathlib import Path
|
||||
from config import (
|
||||
AGENT_TIMEOUT,
|
||||
CLAUDE_CLI,
|
||||
CONTENT_REVIEW_ITERATIONS,
|
||||
TEMPLATES_DIR,
|
||||
MAX_CONCURRENT_GENERATIONS,
|
||||
STORAGE_DIR,
|
||||
@@ -86,19 +85,6 @@ async def _render_pdf(html_path: Path, pdf_path: Path) -> tuple[bool, str]:
|
||||
return True, ""
|
||||
|
||||
|
||||
async def _render_pngs(pdf_path: Path, preview_dir: Path) -> list[Path]:
|
||||
preview_dir.mkdir(parents=True, exist_ok=True)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"python3", "-c",
|
||||
f"from pdf2image import convert_from_path; pages = convert_from_path('{pdf_path}', dpi=120); [p.save('{preview_dir}/page_' + str(i) + '.png') for i, p in enumerate(pages)]; print(len(pages))",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=60)
|
||||
pngs = sorted(preview_dir.glob("page_*.png"))
|
||||
return pngs
|
||||
|
||||
|
||||
def _build_generator_prompt(topic: str, format_name: str, html_path: Path, instructions: str = "") -> str:
|
||||
spec = (TEMPLATES_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8")
|
||||
reference = (TEMPLATES_DIR / "Referenz" / f"{format_name}.md").read_text(encoding="utf-8")
|
||||
@@ -183,43 +169,6 @@ FAIL
|
||||
"""
|
||||
|
||||
|
||||
def _build_review_prompt(format_name: str, png_paths: list[Path], page_count: int) -> str:
|
||||
spec = (TEMPLATES_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8")
|
||||
|
||||
read_instructions = "\n".join(
|
||||
f"- Öffne mit dem Read-Tool: {p}" for p in png_paths
|
||||
)
|
||||
|
||||
return f"""Prüfe visuell einen generierten "{format_name}" Guide.
|
||||
|
||||
SCHRITT 1 — Bilder laden:
|
||||
Das PDF hat {page_count} Seite(n), gerendert als PNG-Screenshots.
|
||||
Nutze das Read-Tool, um JEDE der folgenden Dateien zu öffnen und visuell zu inspizieren:
|
||||
{read_instructions}
|
||||
|
||||
SCHRITT 2 — Visuell prüfen anhand dieser Spezifikation:
|
||||
{spec}
|
||||
|
||||
Prüfkriterien (basierend auf dem, was du in den Bildern SIEHST):
|
||||
- Stimmt die Seitenanzahl? (OnePager/Cheatsheet = exakt 1 Seite)
|
||||
- Ist Text abgeschnitten, überlappt oder läuft aus dem sichtbaren Bereich?
|
||||
- Fehlen Pflicht-Elemente (Cover, TOC, Recall-Boxen, Callouts, etc.)?
|
||||
- Sind Code-Blöcke über Seitenumbrüche zerrissen?
|
||||
- Ist das Layout korrekt (Spalten, Grid, Footer)?
|
||||
|
||||
SCHRITT 3 — Antworte mit GENAU EINEM der folgenden Formate:
|
||||
|
||||
Bei Bestehen:
|
||||
PASS
|
||||
|
||||
Bei Nicht-Bestehen:
|
||||
FAIL
|
||||
- Problem 1
|
||||
- Problem 2
|
||||
- ...
|
||||
"""
|
||||
|
||||
|
||||
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "") -> None:
|
||||
async with _semaphore:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
@@ -227,8 +176,6 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
|
||||
html_path = STORAGE_DIR / "html" / f"{guide_id}.html"
|
||||
pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf"
|
||||
preview_dir = STORAGE_DIR / "preview" / guide_id
|
||||
content_iters = CONTENT_REVIEW_ITERATIONS.get(format_name, 2)
|
||||
|
||||
try:
|
||||
if guide_id in _cancelled:
|
||||
@@ -252,31 +199,28 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
await _fail(guide_id, "HTML-Datei wurde nicht erstellt")
|
||||
return
|
||||
|
||||
# Phase 1: Inhalts-Review (HTML lesen + Websuche, kein PDF)
|
||||
for iteration in range(1, content_iters + 1):
|
||||
# Step 2: Inhalts-Review (1x, kein Loop)
|
||||
if guide_id in _cancelled:
|
||||
return
|
||||
|
||||
await _set_progress(guide_id, "Prüfe Inhalt…")
|
||||
current_step = "Inhalts-Review"
|
||||
current_timeout = AGENT_TIMEOUT
|
||||
content_prompt = _build_content_review_prompt(topic, format_name, html_path)
|
||||
returncode, review_out, review_err = await _run_claude(guide_id, content_prompt, AGENT_TIMEOUT)
|
||||
|
||||
if returncode != 0:
|
||||
await _fail(guide_id, f"Inhalts-Review-Fehler: {review_err[:1000]}")
|
||||
return
|
||||
|
||||
review_text = review_out.strip()
|
||||
if not review_text.startswith("PASS"):
|
||||
if guide_id in _cancelled:
|
||||
return
|
||||
|
||||
await _set_progress(guide_id, f"Prüfe Inhalt… ({iteration}/{content_iters})")
|
||||
current_step = f"Inhalts-Review ({iteration}/{content_iters})"
|
||||
current_timeout = AGENT_TIMEOUT
|
||||
content_prompt = _build_content_review_prompt(topic, format_name, html_path)
|
||||
returncode, review_out, review_err = await _run_claude(guide_id, content_prompt, AGENT_TIMEOUT)
|
||||
|
||||
if returncode != 0:
|
||||
await _fail(guide_id, f"Inhalts-Review-Fehler: {review_err[:1000]}")
|
||||
return
|
||||
|
||||
review_text = review_out.strip()
|
||||
if review_text.startswith("PASS"):
|
||||
break
|
||||
|
||||
if iteration == content_iters:
|
||||
break
|
||||
|
||||
feedback = review_text.replace("FAIL", "").strip()
|
||||
await _set_progress(guide_id, f"Korrigiere Inhalt… ({iteration}/{content_iters})")
|
||||
current_step = f"Inhalts-Korrektur ({iteration}/{content_iters})"
|
||||
await _set_progress(guide_id, "Korrigiere Inhalt…")
|
||||
current_step = "Inhalts-Korrektur"
|
||||
current_timeout = AGENT_TIMEOUT
|
||||
fix_prompt = _build_fix_prompt(topic, format_name, html_path, feedback)
|
||||
returncode, _, fix_err = await _run_claude(guide_id, fix_prompt, AGENT_TIMEOUT)
|
||||
@@ -285,7 +229,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
await _fail(guide_id, f"Fix-Fehler: {fix_err[:1000]}")
|
||||
return
|
||||
|
||||
# Phase 2: Stil-Review (1x PDF rendern + PNGs prüfen)
|
||||
# Step 3: PDF rendern
|
||||
if guide_id in _cancelled:
|
||||
return
|
||||
|
||||
@@ -295,39 +239,6 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
await _fail(guide_id, f"WeasyPrint-Fehler: {err}")
|
||||
return
|
||||
|
||||
await _set_progress(guide_id, "Prüfe Layout…")
|
||||
pngs = await _render_pngs(pdf_path, preview_dir)
|
||||
page_count = len(pngs)
|
||||
|
||||
current_step = "Stil-Review"
|
||||
current_timeout = AGENT_TIMEOUT
|
||||
review_prompt = _build_review_prompt(format_name, pngs, page_count)
|
||||
returncode, review_out, review_err = await _run_claude(guide_id, review_prompt, AGENT_TIMEOUT)
|
||||
|
||||
if returncode != 0:
|
||||
await _fail(guide_id, f"Stil-Review-Fehler: {review_err[:1000]}")
|
||||
return
|
||||
|
||||
review_text = review_out.strip()
|
||||
if not review_text.startswith("PASS"):
|
||||
feedback = review_text.replace("FAIL", "").strip()
|
||||
await _set_progress(guide_id, "Korrigiere Layout…")
|
||||
current_step = "Stil-Korrektur"
|
||||
current_timeout = AGENT_TIMEOUT
|
||||
fix_prompt = _build_fix_prompt(topic, format_name, html_path, feedback)
|
||||
returncode, _, fix_err = await _run_claude(guide_id, fix_prompt, AGENT_TIMEOUT)
|
||||
|
||||
if returncode != 0:
|
||||
await _fail(guide_id, f"Fix-Fehler: {fix_err[:1000]}")
|
||||
return
|
||||
|
||||
# Finales PDF nach Stil-Fix
|
||||
await _set_progress(guide_id, "Rendere finales PDF…")
|
||||
ok, err = await _render_pdf(html_path, pdf_path)
|
||||
if not ok:
|
||||
await _fail(guide_id, f"WeasyPrint-Fehler: {err}")
|
||||
return
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
await update_guide(
|
||||
guide_id,
|
||||
@@ -345,11 +256,6 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
finally:
|
||||
_active_processes.pop(guide_id, None)
|
||||
_cancelled.discard(guide_id)
|
||||
# Preview-PNGs aufräumen
|
||||
if preview_dir.exists():
|
||||
for f in preview_dir.glob("*"):
|
||||
f.unlink()
|
||||
preview_dir.rmdir()
|
||||
|
||||
|
||||
async def rework_guide(guide_id: str, topic: str, format_name: str, instructions: str) -> None:
|
||||
|
||||
@@ -12,7 +12,6 @@ from routes import router
|
||||
async def lifespan(app: FastAPI):
|
||||
(STORAGE_DIR / "html").mkdir(parents=True, exist_ok=True)
|
||||
(STORAGE_DIR / "pdf").mkdir(parents=True, exist_ok=True)
|
||||
(STORAGE_DIR / "preview").mkdir(parents=True, exist_ok=True)
|
||||
await init_db()
|
||||
yield
|
||||
await close_db()
|
||||
|
||||
Reference in New Issue
Block a user