#!/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(t.get('file','')))}:{t.get('line','')} — {html.escape(str(t.get('text',''))[:180])}ProjectIntakeTower Visual Explainer
{path}
Tech: {html.escape(tech)} · Score: {p.get('score','?')} · Risk: {html.escape(str(p.get('risk_level','?')))} · Dirty: {dirty}
{idea}
{why}
{not_finished}
{today}
HTML-Seite + MP4 pro Projekt. Keine echten GitHub-Issues, keine Source-Projektänderungen.