Download ProjectIntakeTower/verifier/generate_visual_explainers.py from smlflg/AgentProjectMinenraeumer: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/smlflg/AgentProjectMinenraeumer/resolve/main/ProjectIntakeTower/verifier/generate_visual_explainers.py
- Command line
-
hf download hf://smlflg/AgentProjectMinenraeumer/ProjectIntakeTower/verifier/generate_visual_explainers.py
-
curl -L -o generate_visual_explainers.py https://huggingface.co/smlflg/AgentProjectMinenraeumer/resolve/main/ProjectIntakeTower/verifier/generate_visual_explainers.py
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() | |