#!/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'
  • {html.escape(str(x))}
  • ' for x in (p.get('score_reasons') or [])[:8]) or '
  • Keine Score-Reasons vorhanden
  • ' todos=''.join(f"
  • {html.escape(str(t.get('file','')))}:{t.get('line','')} — {html.escape(str(t.get('text',''))[:180])}
  • " for t in (p.get('todo_fixme_snippets') or [])[:6]) or '
  • Keine TODO-Snippets im Intake-Kontext.
  • ' 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'''{name} — Visual Explainer

    ProjectIntakeTower Visual Explainer

    {name}

    {path}

    Tech: {html.escape(tech)} · Score: {p.get('score','?')} · Risk: {html.escape(str(p.get('risk_level','?')))} · Dirty: {dirty}

    Systembild

    Originale IdeeProblem sichtbar machenBuild-SpurenGit · README · Pläne · TODOsHeutiger WertWeiterbauen oder archivierenOffener Loop: Abschlussartefakt → Verifier → Issue/Handoff

    Was war die Idee?

    {idea}

    Warum angefangen?

    {why}

    Warum nicht fertig?

    {not_finished}

    Was ist heute wertvoll?

    {today}

    Evidence

    Score-Reasons

      {reasons}

    TODO/FIXME-Auszug

      {todos}

    Storyboard: {pid}.md · Video: {pid}.mp4

    ''' 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=['Project Explainers

    ProjectIntakeTower — Visual Explainers

    HTML-Seite + MP4 pro Projekt. Keine echten GitHub-Issues, keine Source-Projektänderungen.

    '] for p in projects: pid=p['project_id']; name=html.escape(p.get('name') or pid) index.append(f'

    {name}

    {html.escape(str(p.get("path","")))}

    Website · Video · Storyboard
    ') index.append('
    ') (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()