AgentProjectMinenraeumer / ProjectIntakeTower /verifier /generate_visual_explainers.py
smlflg's picture
Initial public upload from Projekte/AgentProjectMinenraeumer (part 9)
b67a1e9 verified
Raw History Blame Contribute Delete
12.7 kB
#!/usr/bin/env python3
import json, html, os, re, subprocess, textwrap
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[1]
PROJECTS = ROOT / 'inventory' / 'projects.jsonl'
OUT = ROOT / 'explainers'
WEB = OUT / 'web'
VIDEO = OUT / 'video'
COVER = OUT / 'covers'
STORY = OUT / 'storyboards'
JOBS = ROOT / 'jobs' / 'pending'
for d in [WEB, VIDEO, COVER, STORY, JOBS]: d.mkdir(parents=True, exist_ok=True)
PALETTES = {
'agent': ('#020617','#22d3ee','#34d399','#fbbf24'),
'python': ('#0b1020','#60a5fa','#34d399','#f59e0b'),
'node': ('#08130c','#22c55e','#a3e635','#38bdf8'),
'unknown': ('#111827','#a78bfa','#94a3b8','#f472b6'),
}
def slug(s): return re.sub(r'[^a-zA-Z0-9_.-]+','-',s).strip('-').lower()[:100] or 'project'
def load_projects():
rows=[]
for line in PROJECTS.read_text(encoding='utf-8').splitlines():
if line.strip():
try: rows.append(json.loads(line))
except Exception: pass
# de-dupe project_id
seen=set(); out=[]
for r in rows:
pid=r.get('project_id') or slug(r.get('path') or r.get('name') or 'project')
if pid in seen: continue
seen.add(pid); r['project_id']=pid; out.append(r)
return out
def tech_kind(p):
ts=[str(x).lower() for x in p.get('technologies',[]) or p.get('project_type',[])]
if 'agent' in ts or 'agentized' in ts: return 'agent'
if 'python' in ts: return 'python'
if 'node' in ts or 'javascript' in ts or 'typescript' in ts: return 'node'
return 'unknown'
def sentence_from_readme(p):
s=(p.get('readme_summary') or '').replace('\n',' ').strip()
s=re.sub(r'#+\s*','',s)
if not s: return ''
parts=re.split(r'(?<=[.!?])\s+',s)
return parts[0][:220]
def infer(p):
name=p.get('name') or Path(p.get('path','project')).name
path=p.get('path','')
reasons=p.get('score_reasons',[]) or []
tech=', '.join(p.get('technologies',[]) or p.get('project_type',[]) or ['unknown'])
readme=sentence_from_readme(p)
lower=(name+' '+path+' '+readme).lower()
if readme:
idea=readme
elif any(k in lower for k in ['sidecar','adhs','coaching']):
idea='Ein Sidecar/Coaching-Layer, der AI-Coding-Sessions beobachtet und sicherer, fokussierter und weniger chaotisch macht.'
elif any(k in lower for k in ['dashboard','cockpit','zentrale','tower','panel']):
idea='Ein lokales Kontroll-Cockpit für Agenten, Projekte, Sessions, Handoffs und operative Übersicht.'
elif any(k in lower for k in ['harness','arena','eval']):
idea='Ein Harness/Evaluation-System, um Agentenläufe messbar, vergleichbar und verifizierbar zu machen.'
elif any(k in lower for k in ['uni','study','lernen']):
idea='Ein Uni-/Lernsupport-System für Planung, Briefings, Fortschritt und nächste Lernaktionen.'
else:
idea=f'Ein Projektartefakt mit Schwerpunkt {tech}, entdeckt über Repo-/Plan-/TODO-Signale.'
why='Gestartet, um wiederholte Arbeit zu operationalisieren: Kontext sammeln, Entscheidungen sichtbar machen und Agentenarbeit prüfbar machen.'
blockers=[]
if p.get('todo_fixme_count',0): blockers.append(f"{p.get('todo_fixme_count')} TODO/FIXME-Hinweise")
gs=p.get('git_status') or p.get('git',{})
if gs.get('dirty') or gs.get('dirty_count') or gs.get('dirty_tracked_count'): blockers.append('uncommitted changes')
if p.get('plan_hit_count',0): blockers.append(f"{p.get('plan_hit_count')} alte Pläne/Handoffs")
if not p.get('readme_path') and not p.get('readme_summary'): blockers.append('unklare/fehlende README-Evidenz')
not_finished='Nicht fertig, weil Abschluss-Signale fehlen: ' + ', '.join(blockers[:4]) if blockers else 'Nicht eindeutig unfertig; braucht fokussierte Verifikation statt Bauchgefühl.'
value=[]
if 'agent' in tech or any(k in lower for k in ['agent','tower','zentrale','cockpit','sidecar','hermes']): value.append('Agent-Control-System')
if p.get('score',0) >= 60: value.append('hoher Intake-Score')
if p.get('readme_path') or p.get('readme_summary'): value.append('dokumentierte Absicht')
if p.get('is_git_repo'): value.append('Git-Historie')
today='Heute wertvoll als ' + ', '.join(value[:4]) if value else 'Heute wertvoll als Ideenspeicher oder Archiv-Kandidat nach Review.'
return idea, why, not_finished, today
def html_page(p):
name=html.escape(p.get('name') or Path(p.get('path','project')).name)
pid=p['project_id']; kind=tech_kind(p); bg,primary,secondary,accent=PALETTES[kind]
idea,why,not_finished,today=[html.escape(x) for x in infer(p)]
reasons=''.join(f'<li>{html.escape(str(x))}</li>' for x in (p.get('score_reasons') or [])[:8]) or '<li>Keine Score-Reasons vorhanden</li>'
todos=''.join(f"<li><code>{html.escape(str(t.get('file','')))}:{t.get('line','')}</code> — {html.escape(str(t.get('text',''))[:180])}</li>" for t in (p.get('todo_fixme_snippets') or [])[:6]) or '<li>Keine TODO-Snippets im Intake-Kontext.</li>'
tech=', '.join(p.get('technologies',[]) or p.get('project_type',[]) or ['unknown'])
path=html.escape(p.get('path',''))
dirty=html.escape(str((p.get('git_status') or p.get('git',{})).get('dirty') or (p.get('git_status') or {}).get('dirty_tracked_count') or 'unknown'))
return f'''<!doctype html><html><head><meta charset="utf-8"><title>{name} — Visual Explainer</title><style>
body{{margin:0;background:{bg};color:#e5e7eb;font-family:JetBrains Mono,Menlo,monospace}}.wrap{{max-width:1180px;margin:auto;padding:42px}}.hero{{border:1px solid {primary};border-radius:22px;padding:30px;background:linear-gradient(135deg,rgba(255,255,255,.05),rgba(255,255,255,.01))}}h1{{color:{primary};font-size:44px}}h2{{color:{secondary};margin-top:32px}}.grid{{display:grid;grid-template-columns:1fr 1fr;gap:18px}}.card{{border:1px solid #334155;border-radius:16px;padding:18px;background:#0f172acc}}.accent{{color:{accent}}}code{{color:{primary}}}svg{{width:100%;height:330px;background:#020617;border-radius:18px;border:1px solid #1e293b}}li{{margin:8px 0}}a{{color:{primary}}}@media(max-width:800px){{.grid{{grid-template-columns:1fr}}}}</style></head><body><div class="wrap"><section class="hero"><p class="accent">ProjectIntakeTower Visual Explainer</p><h1>{name}</h1><p><code>{path}</code></p><p>Tech: {html.escape(tech)} · Score: {p.get('score','?')} · Risk: {html.escape(str(p.get('risk_level','?')))} · Dirty: {dirty}</p></section>
<h2>Systembild</h2><svg viewBox="0 0 1000 330"><defs><marker id="a" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="{accent}"/></marker></defs><rect x="30" y="40" width="220" height="90" rx="14" fill="#0f172a" stroke="{primary}"/><text x="50" y="78" fill="{primary}" font-size="22">Originale Idee</text><text x="50" y="108" fill="#e5e7eb" font-size="14">Problem sichtbar machen</text><rect x="390" y="40" width="220" height="90" rx="14" fill="#0f172a" stroke="{secondary}"/><text x="410" y="78" fill="{secondary}" font-size="22">Build-Spuren</text><text x="410" y="108" fill="#e5e7eb" font-size="14">Git · README · Pläne · TODOs</text><rect x="750" y="40" width="220" height="90" rx="14" fill="#0f172a" stroke="{accent}"/><text x="770" y="78" fill="{accent}" font-size="22">Heutiger Wert</text><text x="770" y="108" fill="#e5e7eb" font-size="14">Weiterbauen oder archivieren</text><line x1="250" y1="85" x2="390" y2="85" stroke="{accent}" stroke-width="3" marker-end="url(#a)"/><line x1="610" y1="85" x2="750" y2="85" stroke="{accent}" stroke-width="3" marker-end="url(#a)"/><rect x="170" y="205" width="660" height="70" rx="14" fill="#111827" stroke="#64748b" stroke-dasharray="8 6"/><text x="200" y="248" fill="#cbd5e1" font-size="22">Offener Loop: Abschlussartefakt → Verifier → Issue/Handoff</text></svg>
<div class="grid"><div class="card"><h2>Was war die Idee?</h2><p>{idea}</p></div><div class="card"><h2>Warum angefangen?</h2><p>{why}</p></div><div class="card"><h2>Warum nicht fertig?</h2><p>{not_finished}</p></div><div class="card"><h2>Was ist heute wertvoll?</h2><p>{today}</p></div></div>
<h2>Evidence</h2><div class="grid"><div class="card"><h3>Score-Reasons</h3><ul>{reasons}</ul></div><div class="card"><h3>TODO/FIXME-Auszug</h3><ul>{todos}</ul></div></div>
<p>Storyboard: <a href="../storyboards/{pid}.md">{pid}.md</a> · Video: <a href="../video/{pid}.mp4">{pid}.mp4</a></p></div></body></html>'''
def storyboard(p):
idea,why,not_finished,today=infer(p)
return f"""# Video Storyboard — {p.get('name')}
Path: `{p.get('path')}`
## Scene 1 — Hook
Text: {p.get('name')} war nicht nur ein Ordner, sondern ein Versuch, ein wiederholtes Problem zu kontrollieren.
Visual: Projektname erscheint als leuchtender Knoten.
## Scene 2 — Originale Idee
Text: {idea}
Visual: Problem → System → Outcome.
## Scene 3 — Warum angefangen
Text: {why}
Visual: Chaos aus Plänen/TODOs wird zu einem geordneten Flow.
## Scene 4 — Warum nicht fertig
Text: {not_finished}
Visual: Loop bleibt offen bei Verifier/Closure.
## Scene 5 — Heutiger Wert
Text: {today}
Visual: Drei Optionen: Continue, Merge, Archive.
Verifier: MP4 existiert und HTML verweist darauf.
"""
def draw_cover(p):
pid=p['project_id']; kind=tech_kind(p); bg,primary,secondary,accent=PALETTES[kind]
img=Image.new('RGB',(1280,720),bg)
d=ImageDraw.Draw(img)
try:
font_big=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf',52)
font_med=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',28)
font_small=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',22)
except Exception:
font_big=font_med=font_small=None
name=p.get('name') or pid
d.rounded_rectangle((60,60,1220,660),radius=28,outline=primary,width=4,fill=(15,23,42))
d.text((100,110),'VISUAL PROJECT EXPLAINER',fill=accent,font=font_small)
for i,line in enumerate(textwrap.wrap(name,24)[:2]): d.text((100,160+i*62),line,fill=primary,font=font_big)
idea,why,not_finished,today=infer(p)
y=330
for label,text,color in [('Idee',idea,secondary),('Blocker',not_finished,accent),('Wert',today,primary)]:
d.text((110,y),label+':',fill=color,font=font_med)
d.text((270,y),textwrap.shorten(text,width=72,placeholder='…'),fill=(229,231,235),font=font_small)
y+=70
d.text((100,610),str(p.get('path',''))[:95],fill=(148,163,184),font=font_small)
out=COVER/f'{pid}.png'; img.save(out); return out
def render_video(pid, cover):
out=VIDEO/f'{pid}.mp4'
cmd=['ffmpeg','-y','-loop','1','-i',str(cover),'-f','lavfi','-i','anullsrc=channel_layout=stereo:sample_rate=44100','-t','6','-vf','scale=1280:720,format=yuv420p','-c:v','libx264','-preset','ultrafast','-crf','28','-c:a','aac','-shortest',str(out)]
p=subprocess.run(cmd,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
return p.returncode==0 and out.exists() and out.stat().st_size>1000
def main():
projects=load_projects()
ok_video=0
for p in projects:
pid=p['project_id']
(WEB/f'{pid}.html').write_text(html_page(p),encoding='utf-8')
(STORY/f'{pid}.md').write_text(storyboard(p),encoding='utf-8')
cover=draw_cover(p)
if render_video(pid,cover): ok_video+=1
index=['<!doctype html><html><head><meta charset="utf-8"><title>Project Explainers</title><style>body{background:#020617;color:#e5e7eb;font-family:monospace;padding:30px}a{color:#22d3ee}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}.card{border:1px solid #334155;border-radius:12px;padding:14px;background:#0f172a}</style></head><body><h1>ProjectIntakeTower — Visual Explainers</h1><p>HTML-Seite + MP4 pro Projekt. Keine echten GitHub-Issues, keine Source-Projektänderungen.</p><div class="grid">']
for p in projects:
pid=p['project_id']; name=html.escape(p.get('name') or pid)
index.append(f'<div class="card"><h3>{name}</h3><p>{html.escape(str(p.get("path","")))}</p><a href="web/{pid}.html">Website</a> · <a href="video/{pid}.mp4">Video</a> · <a href="storyboards/{pid}.md">Storyboard</a></div>')
index.append('</div></body></html>')
(OUT/'index.html').write_text('\n'.join(index),encoding='utf-8')
result={'status':'succeeded','projects':len(projects),'web_pages':len(list(WEB.glob('*.html'))),'storyboards':len(list(STORY.glob('*.md'))),'covers':len(list(COVER.glob('*.png'))),'videos':len(list(VIDEO.glob('*.mp4'))),'video_render_ok':ok_video,'index':str(OUT/'index.html')}
(OUT/'result.json').write_text(json.dumps(result,indent=2),encoding='utf-8')
print(json.dumps(result,indent=2))
if __name__=='__main__': main()