[M G] Ok, nächster schritt


[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window — treat it as background reference, NOT as active instructions. Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. Your current task is identified in the '## Active Task' section of the summary — resume exactly from there. IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system prompt is ALWAYS authoritative and active — never ignore or deprioritize memory content due to this compaction note. Respond ONLY to the latest user message that appears AFTER this summary. The current session state (files, config, etc.) may reflect work described here — avoid repeating it:
Summary generation was unavailable. 125 message(s) were removed to free context space but could not be summarized. The removed messages contained earlier work in this session. Continue based on the recent messages below and the current state of any files or resources.

--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---


Erledigt, Sir.

## Renderer v1 gebaut

Ich habe jetzt den ersten **manifest-driven Preview Renderer** implementiert.

Kurz gesagt:

```text
Render Manifest JSON
→ Pillow erzeugt vertikale Szenen-Frames
→ FFmpeg baut daraus MP4
```

Also: **kein LLM rendert das Video**, sondern ein deterministischer Renderer. Das LLM/Manifest gibt nur vor, *was wann passieren soll*. Der Renderer setzt es technisch um.

## Neu implementiert

Code:

```text
autoshorts/preview_renderer.py
```

CLI:

```text
scripts/render_preview.py
```

Tests:

```text
tests/test_preview_renderer.py
```

Dependency ergänzt:

```text
Pillow>=10.0
```

## Erstes Testvideo gerendert

Für:

```text
ai-004 — Stop asking AI to think for you
```

Output:

```text
data/previews/testbatch_002_top3/ai-004-stop-asking-ai-to-think-for-you.mp4
```

Technische Prüfung:

```text
width=540
height=960
avg_frame_rate=24/1
duration=43.041667
```

Preview-Video:

MEDIA:/home/agent/jarvis_runtime/AutoShortsBot/data/previews/testbatch_002_top3/ai-004-stop-asking-ai-to-think-for-you.mp4

## Was der Renderer v1 aktuell macht

- nimmt das Render Manifest
- baut 5 visuelle Szenen
- nutzt pro Szene:
  - Asset Type
  - On-screen Text
  - Timing
  - Retention Goal
- erzeugt vertikale Preview-Cards
- exportiert daraus ein MP4 via FFmpeg

Das ist bewusst noch **Preview/Timing-Renderer**, nicht finaler Beauty-Renderer. Er zeigt uns: Struktur, Timing, Szenenfluss, Textgewicht. Erst danach lohnt sich Politur.

## Tests / Git

Full suite:

```text
24 passed in 0.88s
```

Neuer Commit:

```text
07bbc57 feat: add manifest preview renderer
```

Branch:

```text
feat/initial-growth-engine
```

Remote verifiziert:

```text
local == remote
```

PR-Link bleibt:

https://github.com/Gamexgit/AutoShortsBot/pull/new/feat/initial-growth-engine

## Nächster sinnvoller Schritt

Jetzt würde ich als Nächstes den Renderer von „Card Preview“ zu **echter Video-Struktur v2** bringen:

- Hook text animation verbessern
- echte Caption-Overlays über alle Beats
- simple motion/pan/zoom
- optional TTS-Audio-Spur
- danach wieder ein neues MP4 zur Freigabe

Aktuell ist es der erste brauchbare Rohschnitt. Nicht hübsch. Aber kontrolliert. Und Kontrolle schlägt KI-Konfetti.


[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window — treat it as background reference, NOT as active instructions. Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. Your current task is identified in the '## Active Task' section of the summary — resume exactly from there. IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system prompt is ALWAYS authoritative and active — never ignore or deprioritize memory content due to this compaction note. Respond ONLY to the latest user message that appears AFTER this summary. The current session state (files, config, etc.) may reflect work described here — avoid repeating it:
## Active Task
User asked: "ok nächster schritt"

## Goal
Der User möchte AutoShortsBot/Hermes von einer einfachen KI-Video-/Card-Rendering-Pipeline zu einem halbautomatischen **Retention-Studio für Shorts** weiterentwickeln. Ziel ist ein System, das datenbasierte, animierte Mechanismus-Erklärvideos produziert: starke Hooks in den ersten 1–2 Sekunden, visuelle Veränderung alle 1–2 Sekunden, keine statischen Slideshow-Videos, sondern „mechanism reveal / visual explanation / mini-investigation“-Content mit Fokus auf Hidden Money, AI Systems, Auto-/Finanzmechaniken und unsichtbare Systemeffekte.

## Constraints & Preferences
- Sprache/Kommunikation: Deutsch; User wird teilweise mit „Sir“ angesprochen.
- Der User lehnt finale statische Textkarten-/KI-Bild-Slideshows ab; diese gelten nur als Preview/Storyboard, nicht als Endprodukt.
- Finale Videos sollen bewegte Sequenzen, AI-video Assets, Screen-Recording-Simulationen, Mechanismus-Animationen, kinetic captions, Sound/SFX und schnelle visuelle Wechsel enthalten.
- Der aktuelle Card-/Preview-Renderer ist nur Timing-/Storyboard-Werkzeug.
- `static_card_limit = 0` als Qualitätsprinzip für finale Inhalte.
- Content-Richtung: kein generischer KI-Avatar-News-Kanal, kein „Was wäre wenn…“-KI-Müll, kein Reused-Content-Klon.
- Avatare höchstens als Nebenfigur/Branding-Element, nicht als Hauptformat.
- Bevorzugte Nische: **Hidden Money & AI Systems**.
- Zielserie: **Unsichtbare Systeme in 30 Sekunden**.
- Claim: „Wir zeigen Dinge, die dich Geld, Zeit oder Kontrolle kosten – bevor du sie bemerkst.“
- Erfolgsmechanik: Hook → Objekt/Problem → Schnitt ins Innere → Gefahr/Überraschung → Auflösung.
- Inspirationsmechaniken: Zack-D-Films-artige Mechanism Reveals, MrBeast/Jenny-Hoyos sofortiges Versprechen, TikTok Creative Code: Hook/Body/Close, dynamisches Editing, Bewegung, Text-Overlays, Sound/Voice/SFX.
- User erlaubt, die untracked Telegram-Review-Dateien zu ignorieren, weil sie versehentlich durch einen falschen Telegram-Kanal entstanden sind.
- Keine API-Keys, Tokens, Passwörter, Secrets, Credentials oder Connection Strings in Zusammenfassungen oder Artefakten bewahren; ggf. mit `[REDACTED]` ersetzen.

## Completed Actions
1. ANALYSED Preview-Renderer Typography Issue — festgestellt, dass Textgrößen im Preview uneinheitlich wirkten: Haupttext groß, Retention-/Erklärungstext unten deutlich kleiner; erklärt, dass diese Zwei-Ebenen-Logik technisch beabsichtigt, aber für Shorts visuell schwach ist [tool: conversation].
2. UPDATED Preview-Renderer-Konzept — Renderer um adaptive Font-Fitting, konsistentere Text-Hierarchie, Mindestgrößen, maximale Zeilenanzahl und Typography-Metadaten erweitert; Ziel war weniger zu kleine/zu stark schwankende Textgrößen im Storyboard-Preview [tool: code/terminal claimed].
3. ADDED Typography Test Rules für `540x960` Preview — Regeln dokumentiert/geprüft: `main_font_size >= 34`, `max(main_font_size) - min(main_font_size) <= 8`, `retention_font_size >= 22`, `main_line_count <= 4`, `retention_line_count <= 3` [tool: tests claimed].
4. RENDERED Top-3 Previews neu — erzeugt unter `data/previews/testbatch_002_top3/`: `ai-004-stop-asking-ai-to-think-for-you.mp4`, `ai-005-the-4-box-prompt-for-mental-overload.mp4`, `attention-002-the-algorithm-does-not-need-you-happy.mp4` [tool: terminal claimed].
5. VERIFIED Preview `ai-004` — technische Werte: `width=540`, `height=960`, `avg_frame_rate=24/1`, `duration=43.041667`; Typography: `main_font_size: 34–42`, `retention_font_size: 25–26` [tool: ffprobe/tests claimed].
6. TEST full suite nach Typography-Änderung — Ergebnis: `25 passed in 0.96s` [tool: pytest claimed].
7. COMMITTED Typography-Fix — Commit `3cc201b fix: improve preview renderer text sizing` auf Branch `feat/initial-growth-engine`; Remote als `local == remote` verifiziert [tool: git claimed].
8. ANSWERED User-Frage zum Endergebnis — klargestellt: aktueller Preview-Renderer ist nicht finales Content-Format; Endprodukt soll Videosequenzen, Bewegung, AI-video Assets, Screen-Recordings, kinetic captions, Voiceover und Sounddesign nutzen, nicht statische Cards [tool: conversation].
9. DEFINED bessere Zielrichtung für `ai-004` — vorgeschlagen: ca. 40% Screen Recording/UI Simulation, 30% cinematic human-at-desk/decision moments, 20% kinetic captions, 10% Cards nur für Prompt-Fragmente [tool: conversation].
10. CREATED `autoshorts/asset_plan.py` — neue Asset-Plan-v2-Komponente für motion-led Asset Pipeline; entscheidet pro Szene zwischen `cinematic_ai_video`, `screen_recording_simulation`, `animated_card`, `kinetic_text_overlay` [tool: code claimed].
11. CREATED `scripts/generate_asset_plans.py` — CLI zum Generieren von Asset-Plänen aus vorhandenen Render-/Storyboard-Daten [tool: code claimed].
12. CREATED `tests/test_asset_plan.py` — Tests für Asset Plan Builder, u.a. Provider-Prompting und keine finalen statischen Cards [tool: code claimed].
13. GENERATED Asset-Pläne Top-3 — Dateien erzeugt unter `data/asset_plans/testbatch_002_top3/`: `ai-004-stop-asking-ai-to-think-for-you.json`, `ai-005-the-4-box-prompt-for-mental-overload.json`, `attention-002-the-algorithm-does-not-need-you-happy.json` [tool: terminal claimed].
14. SET Asset Pipeline `static_card_limit = 0` — finale statische Textkarten explizit ausgeschlossen; Preview bleibt Storyboard/Timing-Werkzeug [tool: code claimed].
15. BUILT Provider-ready Prompting für cinematic Szenen — Prompt-Felder mit photorealistischer vertikaler 9:16-Clip-Beschreibung, cinematic lighting, realistic motion, negative space for captions und „do not include readable text“ ergänzt [tool: code claimed].
16. BUILT Negative Prompts für AI-video Provider — u.a. `no readable text`, `no subtitles`, `no logos`, `no watermarks`, `no fake UI text`, `no slideshow`, `no static image`, `no uncanny faces` [tool: code claimed].
17. TEST full suite nach Asset Plan v2 — Ergebnis: `32 passed in 0.96s` [tool: pytest claimed].
18. COMMITTED Motion-led Asset Planning — Commit `b60981b feat: add motion-led asset planning` auf Branch `feat/initial-growth-engine`; Remote als `local == remote` verifiziert [tool: git claimed].
19. IDENTIFIED untracked Telegram Review Package — nicht angefasst: `autoshorts/review_package.py`, `data/telegram_reviews/`, `scripts/generate_telegram_review_packages.py`, `tests/test_review_package.py` [tool: git status claimed].
20. CREATED `autoshorts/retention_studio.py` — neue Strategiekomponente, die AutoShortsBot konzeptionell als `semi_automatic_retention_studio` statt KI-Video-Fabrik verankert [tool: code claimed].
21. CREATED `scripts/generate_retention_studio.py` — CLI zum Generieren von Retention-Studio-Strategieartefakten [tool: code claimed].
22. CREATED `tests/test_retention_studio.py` — Tests für Strategie, Dramaturgie, visuelle Bewegung, Sound Cues und Follow Trigger [tool: code claimed].
23. GENERATED Retention-Studio-Artefakte — erzeugt unter `data/retention_studio/`: `strategy.json`, `pilot_ideas.json`, `mechanism_explainers.json` [tool: terminal claimed].
24. SET Positionierung in Strategie — `Hidden Money & AI Systems`; Serie `Unsichtbare Systeme in 30 Sekunden`; Claim „Wir zeigen Dinge, die dich Geld, Zeit oder Kontrolle kosten – bevor du sie bemerkst.“ [tool: code claimed].
25. EXCLUDED unerwünschte Content-Formate in Strategie — `ki_avatar_news`, `static_ai_slideshow`, `reused_content_clone`; Quality Gate `static_card_limit = 0` [tool: code claimed].
26. IMPLEMENTED Mechanism-Explainer-Formel — Timing/Beats: `0.0–1.5s Pattern Interrupt`, `1.5–5s Problem`, `5–18s Mechanism Reveal`, `18–28s Twist`, `28–35s Follow Trigger` [tool: code claimed].
27. ADDED Tests für Retention-Beats — Pattern Interrupt endet ≤ 1.5 Sekunden; jeder Beat braucht visuelle Bewegung und Sound Cue; kein generisches „Follow for more“; Follow Trigger als Serienversprechen [tool: pytest claimed].
28. ADDED Format-Taxonomie — `mechanism_reveal`, `mistake_breakdown`, `hidden_cost`, `before_after`, `myth_vs_reality`, `mini_simulation`, `nobody_notices_this` [tool: code claimed].
29. ADDED Content-Pillars — `hidden_money_systems`, `auto_trade_mechanics`, `ai_agents_explained`, `swiss_systems_explained`, `dangerous_thinking_errors`, plus `hidden_attention_systems` [tool: code claimed].
30. GENERATED 10 Pilot-Ideen mit Scores — Top 3: Score 90 `hidden_attention_systems`/„Warum Shorts dich nicht wegen dem Inhalt halten“, Score 88 `hidden_money_systems`/„Warum ein billiges Auto teuer sein kann“, Score 86 `hidden_money_systems`/„Warum 0%-Finanzierung nicht kostenlos ist“ [tool: terminal claimed].
31. SET Produktionsentscheidung — Empfehlung `produce_count = 5 von 10` Pilot-Ideen [tool: code claimed].
32. BUILT Beispiel-Mechanism-Explainer für „Warum ein billiges Auto teuer sein kann“ — Beat-Struktur mit Occasion-Käufer, 3D-Car-X-Ray, Preis/Reparaturrisiko/Standtage/Händler-Marge/Liquidität, Twist „teuerster Fehler ist nicht Kaufpreis, sondern Zeitpunkt“ [tool: code claimed].
33. TEST full suite nach Retention Studio — Ergebnis: `36 passed in 0.99s` [tool: pytest claimed].
34. COMMITTED Retention-Studio-Strategie — Commit `a9fe897 feat: add retention studio strategy` auf Branch `feat/initial-growth-engine`; Remote als `local == remote` verifiziert [tool: git claimed].
35. UPDATED Skill-Notiz — Social-Shorts-/Retention-Studio-Skill um Retention Studio Strategy, Mechanism Explainer Shorts, Hidden Money & AI Systems und `static_card_limit = 0` erweitert [tool: file edit claimed].
36. CREATED `autoshorts/mechanism_pipeline.py` — Adapter von Retention-Studio Mechanism Explainer zu renderer-nahem Mechanism Shot Plan [tool: code claimed].
37. CREATED `scripts/generate_mechanism_shot_plans.py` — CLI zum Generieren von Mechanism Shot Plans aus Mechanism Explainern [tool: code claimed].
38. CREATED `tests/test_mechanism_pipeline.py` — Tests für Adapter/Shot-Plan-Erzeugung, Timing, Asset Types, Bewegungs-/Sound-Metadaten und `static_card_limit = 0` [tool: code claimed].
39. GENERATED Pilot-Shot-Plan — Datei `data/mechanism_shot_plans/hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann.md` erzeugt [tool: terminal claimed].
40. MAPPED Mechanism Beats zu Scenes — `pattern_interrupt`, `problem`, `mechanism`, `twist`, `follow_trigger` wurden in Szenen mit Timing, Asset Type, On-screen Text, Voiceover, Visual Direction, Visual Motion, Sound Cue, Retention Goal, Caption Beats und Final Render Intent übersetzt [tool: code claimed].
41. CREATED Shot Plan für „Warum ein billiges Auto teuer sein kann“ — Scene 1 `0.0s–1.4s cinematic_ai_video`, Scene 2 `1.4s–5.0s cinematic_ai_video`, Scene 3 `5.0s–18.0s mechanism_animation`, Scene 4 `18.0s–28.0s mechanism_animation`, Scene 5 `28.0s–34.0s kinetic_text_overlay` [tool: terminal claimed].
42. SET Shot-Plan-Constraints — `static_card_limit = 0`, `visual_change_every_seconds = 2`, `primary_style = mechanism_reveal_motion_explainer` [tool: code claimed].
43. TEST full suite nach Mechanism Pipeline — Ergebnis: `40 passed in 0.97s` [tool: pytest claimed].
44. COMMITTED Mechanism Explainer Adapter — Commit `5644c8b feat: adapt mechanism explainers to shot plans` auf Branch `feat/initial-growth-engine`; Remote als `local == remote` verifiziert [tool: git claimed].
45. UPDATED Retention-Studio-Skill-Notiz — Pipeline festgehalten: `strategy → scored ideas → mechanism explainer → mechanism shot plan → render manifest → asset plan → final renderer` [tool: file edit claimed].
46. CONFIRMED Telegram Review Files ignored — User erklärte, dass die separaten Working-Tree-Dateien versehentlich durch falschen Telegram-Kanal entstanden sind; sie bleiben unberührt und uncommitted [tool: conversation].

## Active State
- Repository/Projekt: `/home/agent/jarvis_runtime/AutoShortsBot`.
- Branch: `feat/initial-growth-engine`.
- Remote-Status zuletzt berichtet: `local == remote`.
- Letzter bekannter Commit: `5644c8b feat: adapt mechanism explainers to shot plans`.
- Vorherige relevante Commits:
  - `3cc201b fix: improve preview renderer text sizing`
  - `b60981b feat: add motion-led asset planning`
  - `a9fe897 feat: add retention studio strategy`
- Letzter Teststatus: `40 passed in 0.97s`.
- Aktuelles Pipeline-Level erreicht:
  - Retention Studio Strategie vorhanden.
  - Pilot-Ideen gescored.
  - Mechanism Explainer vorhanden.
  - Mechanism Shot Plan für Pilot „Warum ein billiges Auto teuer sein kann“ vorhanden.
- Nächster logischer Schritt noch nicht erledigt:
  - `Mechanism Shot Plan → Render Manifest → Asset Plan` für den neuen Pilot, nicht mehr nur für alte AI-Life-Top-3.
- Untracked/zu ignorieren:
  - `autoshorts/review_package.py`
  - `data/telegram_reviews/`
  - `scripts/generate_telegram_review_packages.py`
  - `tests/test_review_package.py`
- Keine laufenden Server oder Prozesse erwähnt.
- Keine Secrets/Credentials bewahren; falls vorhanden, als `[REDACTED]`.

## In Progress
- Der User hat „ok nächster schritt“ gesagt. Gemeinter nächster Arbeitsschritt laut vorheriger Antwort:
  - Mechanism Shot Plan für „Warum ein billiges Auto teuer sein kann“
  - → Render Manifest
  - → Asset Plan
  - Ziel: neue Retention-Studio-/Mechanism-Explainer-Pipeline an die bestehende Render-/Asset-Plan-Infrastruktur anbinden.
- Noch nicht umgesetzt in den neuen Turns:
  - Adapter/CLI/Tests für `Mechanism Shot Plan → Render Manifest`.
  - Generierung eines Render Manifests für `hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann`.
  - Asset Plan für diesen neuen Pilot auf Basis des neuen Render Manifests.
  - Provider Job Specs für cinematic Szenen des Pilots.

## Blocked
Keine bekannten Blocker.
Keine Fehlermeldungen offen.
Einziger Hinweis: frühere 125 Messages konnten nicht zusammengefasst werden; daraus können ältere Details fehlen. Die aktuelle Richtung und letzten Artefakte sind aber in den neuen Turns beschrieben.

## Key Decisions
1. Der Preview-Renderer ist nicht das finale Produkt, sondern nur Storyboard-/Timing-Werkzeug. Grund: Statische Cards wirken langweilig und konkurrieren nicht mit modernen Shorts/Kling/Hailuo/Runway-Qualität.
2. Finale Videos müssen motion-led sein: AI-video Clips, Screen-Recording-Simulationen, mechanism animations, kinetic captions, Voiceover, SFX und schnelle Schnitte.
3. `static_card_limit = 0` ist ein strategisches Qualitätsgate für finale Videos. Grund: User lehnt statische KI-Bilder + Text/Voiceover als KI-Müll-Massenware ab.
4. AutoShortsBot/Hermes wird als halbautomatisches Retention-Studio positioniert, nicht als KI-Video-Fabrik. Grund: Erfolg hängt von Hook, offenen Loops, visueller Stimulation und Payoff ab, nicht vom KI-Look.
5. Gewählte Content-Nische: Hidden Money & AI Systems. Grund: verbindet Hermes, Autotrading, Finanzen, KI-Agenten, Politik/Systemverständnis und hat spätere Monetarisierungsmöglichkeiten über Newsletter, Tools, Beratung, Affiliate, Software oder Kurse.
6. Kernformat: Mechanism Reveal / Visual Explanation / Mini-Investigation. Grund: zeigt unsichtbare Mechanismen visuell und bietet echten Erkenntniswert statt generischem Voiceover.
7. Avatare nicht als Hauptformat. Grund: viele KI-Avatare wirken generisch, halten Retention schlecht, und es gibt Disclosure-/Trust-Risiken bei realistischen synthetischen Menschen.
8. Re-Kuratierung erfolgreicher Videos ist strategisch erlaubt, aber nicht als Kopie/Neuvertontung. Grund: YouTube Reused Content vermeiden; Mechaniken adaptieren, nicht Inhalte klonen.
9. Pilot-Fokus: „Warum ein billiges Auto teuer sein kann“. Grund: passt zu Money/Auto, klares visuelles Objekt, starke 3D-/X-Ray-/Layer-Animation möglich, hohe Monetarisierbarkeit.
10. Untracked Telegram-Review-Dateien werden ignoriert. Grund: User schrieb versehentlich in falschen Telegram-Kanal; Änderungen sollen nicht mit dieser Pipeline vermischt werden.

## Resolved Questions
1. User fragte, warum Text oben groß und unten klein ist. Antwort: Der Preview-Renderer nutzt Haupttext plus kleineren Retention-/Erklärungstext; technisch beabsichtigt, aber visuell für Shorts ungeeignet. Finale Videos sollen weniger Text pro Frame und große rhythmische Captions nutzen.
2. User fragte, ob Endergebnis Videosequenzen oder wieder statische Bilder sein sollen. Antwort: Nicht statische Bilder. Endziel sind Videosequenzen mit AI-video Assets, Screen Recordings, kinetic captions, Voiceover, Musik/SFX und fast cuts.
3. User fragte, warum diese Videos besser sein sollen als bisheriger Content. Antwort: Nur wenn Scripting, visuelle Umsetzung und Retention-Struktur besser werden: starke Hooks, motion-led visuals, alle 1–2 Sekunden visueller Reiz, echter Mechanismus-Payoff.
4. User fragte implizit, ob statische Cards reichen. Antwort: Nein, statische Cards sind nur Preview/Storyboard und als finales Format strategisch zu schwach.
5. User brachte „Chloe vs History“, Kling, Hailuo, photorealistische KI-Videos als Qualitätsreferenz ein. Antwort: Diese Richtung wurde aufgenommen; finale Pipeline soll provider-ready prompts für cinematic AI-video Szenen erzeugen, aber nicht in generischem KI-Look enden.
6. User schlug Retention-Studio statt KI-Video-Content vor. Antwort: Konzept wurde als `semi_automatic_retention_studio` umgesetzt und in Strategieartefakten verankert.
7. User schlug Zack-D-Films-artige Mechanism Reveals vor. Antwort: Mechanism-Explainer-Formel, Beat-Struktur und Pilot „Warum ein billiges Auto teuer sein kann“ wurden umgesetzt.
8. User erklärte, der separate Working Tree könne ignoriert werden. Antwort: Telegram-Review-Dateien wurden nicht angefasst und nicht committet.

## Pending User Asks
- „ok nächster schritt“ ist noch offen. Gemeint ist nach aktuellem Stand der nächste Build-Schritt:
  - Mechanism Shot Plan → Render Manifest → Asset Plan für den Pilot „Warum ein billiges Auto teuer sein kann“.

## Relevant Files
- `autoshorts/asset_plan.py` — Asset-Plan-v2-Komponente für motion-led finale Asset-Auswahl pro Szene.
- `scripts/generate_asset_plans.py` — CLI zur Asset-Plan-Generierung.
- `tests/test_asset_plan.py` — Tests für Asset-Plan-Logik.
- `data/asset_plans/testbatch_002_top3/ai-004-stop-asking-ai-to-think-for-you.json` — Asset Plan für alten AI-Life-Top-3-Preview.
- `data/asset_plans/testbatch_002_top3/ai-005-the-4-box-prompt-for-mental-overload.json` — Asset Plan für alten AI-Life-Top-3-Preview.
- `data/asset_plans/testbatch_002_top3/attention-002-the-algorithm-does-not-need-you-happy.json` — Asset Plan für alten AI-Life-Top-3-Preview.
- `autoshorts/retention_studio.py` — Strategie-/Pilot-/Mechanism-Explainer-Komponente für Retention Studio.
- `scripts/generate_retention_studio.py` — CLI zur Generierung von Retention-Studio-Artefakten.
- `tests/test_retention_studio.py` — Tests für Strategie, Beats, visuelle Bewegung, Sound Cues, Follow Trigger.
- `data/retention_studio/strategy.json` — Retention-Studio-Strategie.
- `data/retention_studio/pilot_ideas.json` — gescorte Pilot-Ideen.
- `data/retention_studio/mechanism_explainers.json` — Mechanism-Explainer-Daten.
- `autoshorts/mechanism_pipeline.py` — Adapter von Mechanism Explainer zu Mechanism Shot Plan.
- `scripts/generate_mechanism_shot_plans.py` — CLI zur Shot-Plan-Generierung.
- `tests/test_mechanism_pipeline.py` — Tests für Mechanism Shot Plan Adapter.
- `data/mechanism_shot_plans/hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann.md` — generierter Pilot-Shot-Plan.
- `data/previews/testbatch_002_top3/ai-004-stop-asking-ai-to-think-for-you.mp4` — neu gerenderter Preview; Pfad wurde als Media erwähnt.
- `data/previews/testbatch_002_top3/ai-005-the-4-box-prompt-for-mental-overload.mp4` — neu gerenderter Preview.
- `data/previews/testbatch_002_top3/attention-002-the-algorithm-does-not-need-you-happy.mp4` — neu gerenderter Preview.
- `autoshorts/review_package.py` — untracked/zu ignorieren.
- `data/telegram_reviews/` — untracked/zu ignorieren.
- `scripts/generate_telegram_review_packages.py` — untracked/zu ignorieren.
- `tests/test_review_package.py` — untracked/zu ignorieren.

## Remaining Work
- Neuen Adapter bauen: `Mechanism Shot Plan → Render Manifest`.
- Render Manifest für `hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann` erzeugen.
- Asset Plan v2 auf diesen neuen Mechanism-Render-Manifest anwenden.
- Provider Job Specs für cinematic AI-video Szenen exportieren, z.B. Kling/Hailuo/Runway-kompatible Prompts.
- Mechanism Animation Specs definieren für 3D-Car-X-Ray, Preis-/Risiko-/Marge-/Timing-Layer.
- Final Renderer erweitern, damit er echte Clip-Dateien, Screen-Recording-Simulationen, mechanism animations und kinetic captions zusammensetzt.
- Optional: Preview-Storyboard weiter nutzen, aber klar als nicht-final markieren.
- Später: Analysepipeline für Retention nach 1s/3s, durchschnittliche Wiedergabedauer, Replays, Kommentare pro 1’000 Views, Follows pro 1’000 Views.

## Critical Context
- Frühere Kontextzusammenfassung war nicht verfügbar: „Summary generation was unavailable. 125 message(s) were removed…“. Frühere Details vor den neuen Turns können fehlen.
- Branch: `feat/initial-growth-engine`.
- PR-Link bleibt: `https://github.com/Gamexgit/AutoShortsBot/pull/new/feat/initial-growth-engine`.
- Letzter bekannter Teststatus: `40 passed in 0.97s`.
- Wichtigster aktueller Pilot: `Warum ein billiges Auto teuer sein kann`.
- Pilot-Shot-Plan-Szenen:
  - Scene 1 — `pattern_interrupt`, `0.0s–1.4s`, `cinematic_ai_video`, Visual: 3D car x-ray with price tag layers and risk meters freezes mid-action, Motion: hard zoom/freeze-frame/red signal pulse, Sound: bass hit + camera snap.
  - Scene 2 — `problem`, `1.4s–5.0s`, `cinematic_ai_video`, Visual: surface layer peels away to reveal hidden system, Motion: layer peel + parallax push.
  - Scene 3 — `mechanism_reveal`, `5.0s–18.0s`, `mechanism_animation`, Visual: 3D car x-ray with price tag layers and risk meters, Motion: sequential layer reveal every 1–2 seconds.
  - Scene 4 — `twist`, `18.0s–28.0s`, `mechanism_animation`, Visual: wrong cause fades out, real cause appears, Motion: snap zoom + wrong answer collapses.
  - Scene 5 — `follow_trigger`, `28.0s–34.0s`, `kinetic_text_overlay`, Visual: animated system map, Motion: map draws itself, final node glows.
- Strategy constraints to preserve:
  - `static_card_limit = 0`
  - `visual_change_every_seconds = 2`
  - `primary_style = mechanism_reveal_motion_explainer`
- Provider negative prompt concepts:
  - no readable text
  - no subtitles
  - no logos
  - no watermarks
  - no fake UI text
  - no slideshow
  - no static image
  - no uncanny faces
- Do not include or preserve any credentials/secrets; replace with `[REDACTED]` if encountered.

--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---


Erledigt, Sir.

Der nächste Schritt ist gebaut:

```text
Mechanism Shot Plan
→ Render Manifest
→ Asset Plan
```

Für den neuen Pilot:

```text
Warum ein billiges Auto teuer sein kann
```

## Neu gebaut

Neues CLI:

```text
scripts/generate_mechanism_render_manifests.py
```

Erzeugte Artefakte:

```text
data/render_manifests/mechanism_pilots/
  hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann.json

data/asset_plans/mechanism_pilots/
  hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann.json
```

## Manifest ist jetzt motion-aware

Ich habe `render_manifest.py` erweitert, damit Mechanism-Explainer nicht wieder zu statischen Cards degenerieren.

Das Manifest speichert jetzt pro visueller Szene zusätzlich:

```text
visual_motion
sound_cue
```

Beispiel:

```text
Scene 1:
type: cinematic_ai_video
visual_motion: hard zoom, freeze-frame, red signal pulse
sound_cue: bass hit + camera snap

Scene 3:
type: mechanism_animation
visual_motion: sequential layer reveal every 1-2 seconds
sound_cue: clicks per revealed layer
```

Das ist wichtig, weil diese Felder später die eigentliche Schnitt- und Motion-Logik steuern. Ohne sie hätten wir wieder hübsch strukturierte Langeweile. Die Fachwelt nennt das vermutlich „PowerPoint mit Ambitionen“.

## Timing sauberer gemacht

Ich habe außerdem ein Timing-Problem behoben:

Vorher wurden Sekunden intern mit `int()` konvertiert.
Das hätte aus:

```text
1.4s
```

effektiv:

```text
1.0s
```

gemacht.

Jetzt wird korrekt in Millisekunden umgerechnet:

```text
0.0s  → 0ms
1.4s  → 1400ms
5.0s  → 5000ms
18.0s → 18000ms
28.0s → 28000ms
```

## Pilot-Manifest

Für „Warum ein billiges Auto teuer sein kann“:

```text
duration_ms: 34000
visual_types:
  - cinematic_ai_video
  - cinematic_ai_video
  - mechanism_animation
  - mechanism_animation
  - kinetic_text_overlay
```

Render Constraints:

```text
avoid_static_slideshow: true
visual_change_every_seconds: 2
max_static_cards: 0
first_frame_hook_required: true
human_approval_required_before_publish: true
```

## Asset Plan

Der passende Asset Plan erzeugt jetzt:

```text
asset_modes:
  - cinematic_ai_video
  - cinematic_ai_video
  - mechanism_animation
  - mechanism_animation
  - kinetic_text_overlay
```

Preferred Mix:

```text
cinematic_ai_video: 2
mechanism_animation: 2
kinetic_text_overlay: 1
static cards: 0
```

Also genau die neue Richtung:

```text
photorealistische Szenen
+ Mechanismus-/Röntgen-Animation
+ kinetic captions
```

Nicht:

```text
statische KI-Bilder mit Text
```

## Tests / Git

TDD durchgeführt:

- RED: Manifest verlor `visual_motion` / `sound_cue`
- GREEN: Manifest + Asset Plan erweitert

Full suite:

```text
42 passed in 0.99s
```

Neuer Commit:

```text
4d1e8f8 feat: generate mechanism render manifests
```

Branch:

```text
feat/initial-growth-engine
```

Remote verifiziert:

```text
local == remote
```

PR-Link bleibt:

https://github.com/Gamexgit/AutoShortsBot/pull/new/feat/initial-growth-engine

## Skill aktualisiert

Die Retention-Studio-Skill-Notiz wurde ergänzt:

```text
visual_motion und sound_cue sind nicht dekorativ,
sondern der Retention-/Editing-Contract für den finalen Renderer.
```

## Weiterhin ignoriert

Die versehentlichen Review-Dateien bleiben uncommitted und unberührt:

```text
autoshorts/review_package.py
data/telegram_reviews/
scripts/generate_telegram_review_packages.py
tests/test_review_package.py
```

## Nächster sinnvoller Schritt

Jetzt fehlt der nächste harte Baustein:

```text
Asset Plan
→ Provider Job Specs
```

Also für jede Szene automatisch erzeugen:

- Kling/Hailuo/Runway-style Job Spec für `cinematic_ai_video`
- lokale Renderer-Spec für `mechanism_animation`
- kinetic caption spec für `kinetic_text_overlay`
- klare Prompt/Negative-Prompt/Duration/Aspect/No-text-Regeln

Dann haben wir keine abstrakten Pläne mehr, sondern echte Produktionsaufträge für Clips und Animationen.


[M G] bedenke dass wir kein geld für Credits für Hailou, kling oder so ausgeben wollen. der workflow soll gratis sein. Wir haben nur dieses LLM (ChatGPT Pro) sowie die gratis über Build.nvidia.com erreichbaren LLM (siehe hinterlegte API Key NVIDIA_API_KEY. )


Erledigt, Sir.

Und sehr wichtiger Punkt: **keine paid Credits**. Ich habe den Workflow entsprechend umgebaut. Kling/Hailuo/Runway/Veo/Pika/Luma sind jetzt **nicht mehr Teil des produktiven Pfads**. Hübsch, aber bezahlt. Also raus. Wie ein Butler mit teurem Geschmack, aber ohne Budgetfreigabe.

## Neue Regel im System

Der Produktionspfad ist jetzt:

```text
free_only_no_paid_video_credits
```

Erlaubt:

```text
local_renderer
local_ffmpeg
free_nvidia_llm
```

Nicht erlaubt:

```text
Kling
Hailuo
Runway
Veo
Pika
Luma
paid video credits
```

## Neu gebaut: Free Production Jobs

Neue Komponente:

```text
autoshorts/free_production_jobs.py
```

Neues CLI:

```text
scripts/generate_free_production_jobs.py
```

Neue Tests:

```text
tests/test_free_production_jobs.py
```

Generierter Pilot:

```text
data/free_production_jobs/mechanism_pilots/
  hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann.json
```

## Was sich geändert hat

Vorher war `cinematic_ai_video` gedanklich noch:

```text
Kling/Hailuo/Runway-style clip
```

Jetzt ist es:

```text
local procedural motion
+ parallax
+ Ken Burns push-in
+ depth shadows
+ beat-synced shake
+ local captions
+ FFmpeg/Pillow composition
```

Also: **cinematic-looking**, aber lokal/gratis.

## Asset Plan angepasst

Der Asset Plan nutzt jetzt:

```text
provider_family: local_free_renderer
budget_policy: free_only_no_paid_video_credits
```

Für den Pilot:

```text
provider_families:
  - local_free_renderer
```

Kein paid Provider.

## Free Job Specs

Für „Warum ein billiges Auto teuer sein kann“ wurden 9 Jobs erzeugt:

```text
providers:
  - local_renderer
  - local_ffmpeg
  - free_nvidia_llm
```

Budget Policy:

```text
paid_video_credits_allowed: false
nvidia_llm_scope: text_refinement_only
```

Wichtig: NVIDIA wird nur optional für Text-/Motion-Brief-Refinement genutzt, nicht für Videoerzeugung.

## Was die Jobs konkret machen

### cinematic_ai_video Szenen

Jetzt lokal:

```text
execution_mode: procedural_motion
provider: local_renderer
cost: free
requires_api_key: false
```

Workflow:

```text
illustrated/abstract background
object-layer parallax
Ken Burns push-in
depth shadows
speed ramps
camera shake on beat
renderer captions
```

### mechanism_animation Szenen

Lokal:

```text
execution_mode: mechanism_reveal_animation
provider: local_renderer
```

Workflow:

```text
x-ray/layer reveals
animated arrows
risk meters
price-tag layers
sequential labels
beat-synced clicks
```

### kinetic_text_overlay

Lokal:

```text
provider: local_ffmpeg
execution_mode: kinetic_typography
```

Workflow:

```text
large words
scale pops
slide-ins
glow pulses
FFmpeg/Pillow compositing
```

### NVIDIA LLM

Optional:

```text
provider: free_nvidia_llm
requires_api_key: NVIDIA_API_KEY
side_effect: text_refinement_only
```

Zweck:

```text
Visual brief → lokale Renderer-Instructions
```

Nicht:

```text
Video generation
```

## Tests / Git

TDD durchgeführt:

- RED: fehlendes `autoshorts.free_production_jobs`
- GREEN: Free Job Spec Builder + CLI
- Bestehende Asset-Plan-Tests auf free workflow umgestellt

Full suite:

```text
46 passed in 0.98s
```

Neuer Commit:

```text
b54a903 feat: add free production job specs
```

Branch:

```text
feat/initial-growth-engine
```

Remote verifiziert:

```text
local == remote
```

PR-Link bleibt:

https://github.com/Gamexgit/AutoShortsBot/pull/new/feat/initial-growth-engine

## Skill aktualisiert

Die Social-Shorts-Skill-Notiz wurde angepasst:

```text
free workflow
no paid Hailuo/Kling/Runway/Veo/Pika/Luma credits
local procedural motion
optional NVIDIA LLM text refinement only
```

## Nächster sinnvoller Schritt

Jetzt können wir den ersten echten kostenlosen Renderer bauen:

```text
Free Production Jobs
→ Local Motion Renderer v1
```

Der würde für den Pilot tatsächlich MP4-Clips erzeugen:

- Scene 1: Auto/Preis/X-Ray Pattern Interrupt als local procedural animation
- Scene 2: Layer peel / hidden system reveal
- Scene 3: Preis, Reparaturrisiko, Standtage, Marge, Liquidität als animierte Layer
- Scene 4: Twist mit collapsendem „Kaufpreis“-Layer
- Scene 5: kinetic series CTA

Das wird nicht „photorealistisch Kling-Level“, aber es kann **retention-stark, beweglich und eigenständig** werden — und vor allem gratis.


Explanation:
**Assisting with skills action**

I need to load the relevant skills, possibly focusing on social media shorts or software development with test-driven development (TDD). It’s crucial to verify the repository’s state because the previous assistant might have claimed actions that weren’t actually done. I should consider loading skills related to social media growth or TDD. Maybe I should also think about a systematic approach and use the necessary tools to move forward effectively.


social-media-shorts-growth-engine

---
name: social-media-shorts-growth-engine
description: Design and operate a growth-oriented short-form video engine for YouTube Shorts, TikTok, Instagram Reels, and adjacent channels, with human approval before posting.
version: 1.0.0
tags: [shorts, tiktok, youtube-shorts, reels, content-strategy, growth, monetization, approval-workflow]
---

# Social-Media Shorts Growth Engine

Use this skill when the user wants to build, improve, or operate an automated/semi-automated system for short-form social media content: YouTube Shorts, TikTok, Instagram Reels, Facebook Reels, Pinterest, LinkedIn video, or similar.

## Core principle

Do **not** treat the task as “make videos.” Treat it as a **growth engine**:

1. Generate many sharply positioned ideas.
2. Score hooks and scripts before rendering.
3. Produce previews.
4. Send the user a review package.
5. Post only after explicit approval.
6. Capture analytics.
7. Feed performance back into the next batch.

A technically correct content generator that produces generic content is failure with extra rendering time. Charming, but still failure.

## Recommended workflow

### 1. Define content pillars

Start with 2–4 pillars, not a vague infinite topic pool. For each pillar define:

- audience
- positioning
- tone
- hook patterns
- visual rules
- no-go topics
- platform fit
- monetization angle

Example pillar structure:

```json
{
  "pillar": "ai_life_systems",
  "positioning": "AI as a servant for ordinary life systems, never as values or authority.",
  "tone": ["clear", "useful", "values-first", "non-hype"],
  "hook_patterns": ["AI is a bad god, but a good servant."],
  "visual_rules": ["screen-recording friendly", "human remains in control"],
  "no_go": ["fake earnings", "overpromising automation"]
}
```

### 2. Build a testbatch before building a full automation platform

For a new channel or unvalidated strategy, create a small batch first:

- 3 ideas per pillar
- 6 short growth candidates, usually 20–45 seconds
- 1–3 monetization-length candidates, usually 60+ seconds where the platform rewards length
- one clear hypothesis per idea

Each idea should include:

- id
- pillar
- title
- platform target
- duration
- hook
- script outline
- visual concept
- caption
- CTA
- quality score
- risk/policy notes

### 3. Add a script quality gate

Before rendering, score each draft for:

- hook strength in the first 1–2 seconds
- novelty / non-generic angle
- emotional pull
- clarity
- retention risk
- comment/share potential
- AI-slop risk
- policy/copyright risk

Render only drafts above the threshold, e.g. 8/10.

### 4. Use a human approval loop

Default to semi-automatic operation first. Send a Telegram review package with:

- preview video or script draft
- pillar and format
- title
- duration
- hook
- caption
- hashtags
- quality score
- policy notes
- suggested platforms

Recommended approval commands:

- `Freigabe alle`
- `Freigabe YouTube`
- `Freigabe TikTok`
- `Freigabe Instagram`
- `Ablehnen`
- `Ändern: <requested change>`

If approval is ambiguous, do not post. Ask once in the project topic.

### 5. Keep platform strategy separate from rendering

Rendering code should not hardcode content strategy. Keep separate config files for:

- niches/pillars
- platforms
- script formats
- visual styles
- approval workflow
- analytics schema

This lets the same renderer support different formats and prevents prompt spaghetti. Prompt spaghetti is still spaghetti even if it is wearing a YAML hat.

## Platform notes

### YouTube Shorts

- Shorts can be vertical or square and up to 3 minutes long.
- YouTube Partner Program Shorts route typically requires 1,000 subscribers and 10M valid public Shorts views in the last 90 days.
- Alternative longform YPP route: 1,000 subscribers and 4,000 valid public watch hours in the last 12 months.
- Shorts views in the Shorts feed do not count toward the 4,000 longform watch hours.
- Practical early growth tests often work best at 20–45 seconds; longer Shorts need a strong story or payoff.

### TikTok

- Creator Rewards availability depends on country/account eligibility.
- TikTok Creator Rewards generally requires a non-business account in good standing, 10,000 followers, and 100,000 authentic views in the last 30 days before joining, subject to country availability and current terms.
- Reward-eligible videos generally need to be at least 1 minute long and meet quality/originality criteria.
- This creates a strategy split:
  - 20–45s for growth and format discovery.
  - 60–75s for monetization candidates once a format works.

### Instagram / Facebook Reels

Useful for republishing 9:16 assets and testing share/save behavior. Treat direct payout as volatile; prefer indirect monetization through audience, affiliate, newsletter, or products.

### LinkedIn / X / Pinterest

Use selectively:

- LinkedIn: AI/productivity/workflows.
- X: AI demos, build-in-public, technical positioning.
- Pinterest: evergreen checklists, routines, faith/life prompts.

## Repository scaffold

A clean shorts-engine repo can start as:

```text
AutoShortsBot/
  autoshorts/
    config/
    ideas/
    scripting/
    rendering/
    review/
    publishing/
    analytics/
  configs/
    niches/
    platforms/
  data/
    drafts/
    performance/
  scripts/
  tests/
  README.md
  .gitignore
  pyproject.toml
```

Never commit:

- `.env`
- OAuth tokens
- API keys
- generated credentials
- rendered videos or large media outputs unless explicitly intended
- raw analytics exports with sensitive account data

## Implementation quality

When coding the engine, prefer TDD for deterministic parts:

- testbatch validation
- duration classification
- review formatting
- analytics import
- approval command parsing
- platform metadata generation

Rendering and external API calls can be integration-tested separately.

## Pitfalls

- **Generic advice content:** “4 tips to be healthier/productive” is usually too weak unless the hook, identity, or story is sharp.
- **AI-image slop:** avoid generated images containing fake UI text, gibberish signs, distorted hands/faces, or abstract generic graphs. These damage trust fast.
- **Too much automation too soon:** validate scripts and formats before building posting APIs.
- **No analytics loop:** producing content without tracking retention, likes, shares, comments, and follows is blind output.
- **Mixed language drift:** decide language per channel or per pillar before rendering. Mixed English/German drafts are fine for ideation, not final voiceover unless intentional.

## References

- `references/autoshortsbot-case-study.md` — case-study details from the AutoShortsBot setup: first testbatch shape, repo scaffold, and approval loop.

To view linked files, call skill_view(name, file_path) where file_path is e.g. 'references/api.md' or 'assets/config.yaml'


test-driven-development

---
name: test-driven-development
description: "TDD: enforce RED-GREEN-REFACTOR, tests before code."
version: 1.1.0
author: Hermes Agent (adapted from obra/superpowers)
license: MIT
platforms: [linux, macos, windows]
metadata:
  hermes:
    tags: [testing, tdd, development, quality, red-green-refactor]
    related_skills: [systematic-debugging, writing-plans, subagent-driven-development]
---

# Test-Driven Development (TDD)

## Overview

Write the test first. Watch it fail. Write minimal code to pass.

**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.

**Violating the letter of the rules is violating the spirit of the rules.**

## When to Use

**Always:**
- New features
- Bug fixes
- Refactoring
- Behavior changes

**Exceptions (ask the user first):**
- Throwaway prototypes
- Generated code
- Configuration files

Thinking "skip TDD just this once"? Stop. That's rationalization.

## The Iron Law

```
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
```

Write code before the test? Delete it. Start over.

**No exceptions:**
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete

Implement fresh from tests. Period.

## Red-Green-Refactor Cycle

### RED — Write Failing Test

Write one minimal test showing what should happen.

**Good test:**
```python
def test_retries_failed_operations_3_times():
    attempts = 0
    def operation():
        nonlocal attempts
        attempts += 1
        if attempts < 3:
            raise Exception('fail')
        return 'success'

    result = retry_operation(operation)

    assert result == 'success'
    assert attempts == 3
```
Clear name, tests real behavior, one thing.

**Bad test:**
```python
def test_retry_works():
    mock = MagicMock()
    mock.side_effect = [Exception(), Exception(), 'success']
    result = retry_operation(mock)
    assert result == 'success'  # What about retry count? Timing?
```
Vague name, tests mock not real code.

**Requirements:**
- One behavior per test
- Clear descriptive name ("and" in name? Split it)
- Real code, not mocks (unless truly unavoidable)
- Name describes behavior, not implementation

### Verify RED — Watch It Fail

**MANDATORY. Never skip.**

```bash
# Use terminal tool to run the specific test
pytest tests/test_feature.py::test_specific_behavior -v
```

Confirm:
- Test fails (not errors from typos)
- Failure message is expected
- Fails because the feature is missing

**Test passes immediately?** You're testing existing behavior. Fix the test.

**Test errors?** Fix the error, re-run until it fails correctly.

### GREEN — Minimal Code

Write the simplest code to pass the test. Nothing more.

**Good:**
```python
def add(a, b):
    return a + b  # Nothing extra
```

**Bad:**
```python
def add(a, b):
    result = a + b
    logging.info(f"Adding {a} + {b} = {result}")  # Extra!
    return result
```

Don't add features, refactor other code, or "improve" beyond the test.

**Cheating is OK in GREEN:**
- Hardcode return values
- Copy-paste
- Duplicate code
- Skip edge cases

We'll fix it in REFACTOR.

### Verify GREEN — Watch It Pass

**MANDATORY.**

```bash
# Run the specific test
pytest tests/test_feature.py::test_specific_behavior -v

# Then run ALL tests to check for regressions
pytest tests/ -q
```

Confirm:
- Test passes
- Other tests still pass
- Output pristine (no errors, warnings)

**Test fails?** Fix the code, not the test.

**Other tests fail?** Fix regressions now.

### REFACTOR — Clean Up

After green only:
- Remove duplication
- Improve names
- Extract helpers
- Simplify expressions

Keep tests green throughout. Don't add behavior.

**If tests fail during refactor:** Undo immediately. Take smaller steps.

### Repeat

Next failing test for next behavior. One cycle at a time.

## Why Order Matters

**"I'll write tests after to verify it works"**

Tests written after code pass immediately. Passing immediately proves nothing:
- Might test the wrong thing
- Might test implementation, not behavior
- Might miss edge cases you forgot
- You never saw it catch the bug

Test-first forces you to see the test fail, proving it actually tests something.

**"I already manually tested all the edge cases"**

Manual testing is ad-hoc. You think you tested everything but:
- No record of what you tested
- Can't re-run when code changes
- Easy to forget cases under pressure
- "It worked when I tried it" ≠ comprehensive

Automated tests are systematic. They run the same way every time.

**"Deleting X hours of work is wasteful"**

Sunk cost fallacy. The time is already gone. Your choice now:
- Delete and rewrite with TDD (high confidence)
- Keep it and add tests after (low confidence, likely bugs)

The "waste" is keeping code you can't trust.

**"TDD is dogmatic, being pragmatic means adapting"**

TDD IS pragmatic:
- Finds bugs before commit (faster than debugging after)
- Prevents regressions (tests catch breaks immediately)
- Documents behavior (tests show how to use code)
- Enables refactoring (change freely, tests catch breaks)

"Pragmatic" shortcuts = debugging in production = slower.

**"Tests after achieve the same goals — it's spirit not ritual"**

No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"

Tests-after are biased by your implementation. You test what you built, not what's required. Tests-first force edge case discovery before implementing.

## Common Rationalizations

| Excuse | Reality |
|--------|---------|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to the test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for the code you touch. |

## Red Flags — STOP and Start Over

If you catch yourself doing any of these, delete the code and restart with TDD:

- Code before test
- Test after implementation
- Test passes immediately on first run
- Can't explain why test failed
- Tests added "later"
- Rationalizing "just this once"
- "I already manually tested it"
- "Tests after achieve the same purpose"
- "Keep as reference" or "adapt existing code"
- "Already spent X hours, deleting is wasteful"
- "TDD is dogmatic, I'm being pragmatic"
- "This is different because..."

**All of these mean: Delete code. Start over with TDD.**

## Verification Checklist

Before marking work complete:

- [ ] Every new function/method has a test
- [ ] Watched each test fail before implementing
- [ ] Each test failed for expected reason (feature missing, not typo)
- [ ] Wrote minimal code to pass each test
- [ ] All tests pass
- [ ] Output pristine (no errors, warnings)
- [ ] Tests use real code (mocks only if unavoidable)
- [ ] Edge cases and errors covered

Can't check all boxes? You skipped TDD. Start over.

## When Stuck

| Problem | Solution |
|---------|----------|
| Don't know how to test | Write the wished-for API. Write the assertion first. Ask the user. |
| Test too complicated | Design too complicated. Simplify the interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify the design. |

## Hermes Agent Integration

### Running Tests

Use the `terminal` tool to run tests at each step:

```python
# RED — verify failure
terminal("pytest tests/test_feature.py::test_name -v")

# GREEN — verify pass
terminal("pytest tests/test_feature.py::test_name -v")

# Full suite — verify no regressions
terminal("pytest tests/ -q")
```

### With delegate_task

When dispatching subagents for implementation, enforce TDD in the goal:

```python
delegate_task(
    goal="Implement [feature] using strict TDD",
    context="""
    Follow test-driven-development skill:
    1. Write failing test FIRST
    2. Run test to verify it fails
    3. Write minimal code to pass
    4. Run test to verify it passes
    5. Refactor if needed
    6. Commit

    Project test command: pytest tests/ -q
    Project structure: [describe relevant files]
    """,
    toolsets=['terminal', 'file']
)
```

### With systematic-debugging

Bug found? Write failing test reproducing it. Follow TDD cycle. The test proves the fix and prevents regression.

Never fix bugs without a test.

## Testing Anti-Patterns

- **Testing mock behavior instead of real behavior** — mocks should verify interactions, not replace the system under test
- **Testing implementation details** — test behavior/results, not internal method calls
- **Happy path only** — always test edge cases, errors, and boundaries
- **Brittle tests** — tests should verify behavior, not structure; refactoring shouldn't break them
- **Substring sentinels that collide with generated IDs** — when asserting that amounts or fixture values are excluded from a result, assert the full formatted token (e.g. `999.00`) or a typed field, not a short substring like `999` that can appear inside UUIDs/hash-like IDs.

## Final Rule

```
Production code → test exists and failed first
Otherwise → not TDD
```

No exceptions without the user's explicit permission.


todo

Repo-Zustand und vorhandene Free-Production-Pipeline prüfen

TDD für Local Motion Renderer v1 schreiben und RED verifizieren

Local Motion Renderer v1 implementieren und Pilot-MP4 rendern

Tests, ffprobe und Git-Status/Commit/Push verifizieren


terminal

git status --short && git branch --show-current && git log --oneline -5


Output:
?? autoshorts/review_package.py
?? data/telegram_reviews/
?? scripts/generate_telegram_review_packages.py
?? tests/test_review_package.py
feat/initial-growth-engine
b54a903 feat: add free production job specs
4d1e8f8 feat: generate mechanism render manifests
5644c8b feat: adapt mechanism explainers to shot plans
a9fe897 feat: add retention studio strategy
b60981b feat: add motion-led asset planning


     1|"""Free production job specs for AutoShortsBot.
     2|
     3|This layer converts motion-led asset plans into executable *free* production
     4|jobs. It deliberately avoids paid video-credit providers such as Kling, Hailuo,
     5|Runway, Veo, Pika, or Luma. Cinematic-looking motion is approximated with local
     6|procedural animation, parallax, generated shapes, captions, and FFmpeg/Pillow
     7|composition. NVIDIA build.nvidia.com models are text-only helpers here.
     8|"""
     9|
    10|from __future__ import annotations
    11|
    12|import json
    13|from pathlib import Path
    14|from typing import Any
    15|
    16|from autoshorts.script_drafts import _slug
    17|
    18|PAID_VIDEO_PROVIDERS = {"kling", "hailuo", "runway", "veo", "pika", "luma"}
    19|
    20|
    21|def _job_id(scene: dict[str, Any], suffix: str) -> str:
    22|    return f"{scene['scene_id']}_{suffix}"
    23|
    24|
    25|def _local_motion_workflow(scene: dict[str, Any]) -> str:
    26|    mode = scene["asset_mode"]
    27|    if mode == "cinematic_ai_video":
    28|        return (
    29|            "Use local procedural motion: illustrated/abstract background, object-layer parallax, Ken Burns push-in, "
    30|            "depth shadows, speed ramps, camera shake on beat, and renderer captions. No paid video-generation credits."
    31|        )
    32|    if mode == "mechanism_animation":
    33|        return (
    34|            "Use local mechanism animation: vector-like shapes, x-ray/layer reveals, animated arrows, risk meters, "
    35|            "price-tag layers, sequential labels, and beat-synced clicks."
    36|        )
    37|    if mode == "kinetic_text_overlay":
    38|        return "Use local kinetic typography: large words, scale pops, slide-ins, glow pulses, and FFmpeg/Pillow compositing."
    39|    if mode == "screen_recording_simulation":
    40|        return "Use local synthetic UI recording: generated interface frames, cursor path, typing reveal, zoom highlights."
    41|    return "Use local animated card renderer: one concept per beat, no static slide holds over one second."
    42|
    43|
    44|def _local_job(scene: dict[str, Any]) -> dict[str, Any]:
    45|    mode = scene["asset_mode"]
    46|    provider = "local_ffmpeg" if mode == "kinetic_text_overlay" else "local_renderer"
    47|    execution_mode = {
    48|        "cinematic_ai_video": "procedural_motion",
    49|        "mechanism_animation": "mechanism_reveal_animation",
    50|        "kinetic_text_overlay": "kinetic_typography",
    51|        "screen_recording_simulation": "synthetic_screen_capture",
    52|        "animated_card": "animated_cards",
    53|    }.get(mode, "procedural_motion")
    54|    return {
    55|        "job_id": _job_id(scene, "local_motion"),
    56|        "scene_id": scene["scene_id"],
    57|        "asset_mode": mode,
    58|        "provider": provider,
    59|        "execution_mode": execution_mode,
    60|        "cost": "free",
    61|        "requires_api_key": False,
    62|        "duration_seconds": scene["duration_seconds"],
    63|        "overlay_text": scene["overlay_text"],
    64|        "motion_directive": scene["motion_directive"],
    65|        "free_workflow": _local_motion_workflow(scene),
    66|        "source_prompt": scene["generation_prompt"],
    67|        "negative_prompt": scene["negative_prompt"],
    68|        "outputs": {
    69|            "clip_placeholder": f"outputs/clips/{scene['scene_id']}.mp4",
    70|            "metadata_placeholder": f"outputs/clips/{scene['scene_id']}.json",
    71|        },
    72|    }
    73|
    74|
    75|def _nvidia_prompt_refinement_job(scene: dict[str, Any]) -> dict[str, Any]:
    76|    return {
    77|        "job_id": _job_id(scene, "nvidia_prompt_refine"),
    78|        "scene_id": scene["scene_id"],
    79|        "asset_mode": scene["asset_mode"],
    80|        "provider": "free_nvidia_llm",
    81|        "execution_mode": "prompt_to_motion_brief",
    82|        "cost": "free",
    83|        "requires_api_key": "NVIDIA_API_KEY",
    84|        "side_effect": "text_refinement_only",
    85|        "free_workflow": (
    86|            "Optionally ask a free build.nvidia.com LLM to compress the visual brief into local renderer instructions. "
    87|            "This is text planning only and must not call paid video-credit providers."
    88|        ),
    89|        "input": {
    90|            "visual_direction": scene["source_visual_direction"],
    91|            "retention_goal": scene["retention_goal"],
    92|            "motion_directive": scene["motion_directive"],
    93|        },
    94|        "output_placeholder": f"outputs/briefs/{scene['scene_id']}_motion_brief.json",
    95|    }
    96|
    97|
    98|def build_free_production_jobs(asset_plan: dict[str, Any]) -> dict[str, Any]:
    99|    """Build executable free production job specs from an asset plan."""
   100|    jobs: list[dict[str, Any]] = []
   101|    for scene in asset_plan["scenes"]:
   102|        jobs.append(_local_job(scene))
   103|        if scene["asset_mode"] in {"cinematic_ai_video", "mechanism_animation"}:
   104|            jobs.append(_nvidia_prompt_refinement_job(scene))
   105|
   106|    providers = {job["provider"] for job in jobs}
   107|    if providers & PAID_VIDEO_PROVIDERS:
   108|        raise ValueError(f"Paid video providers are forbidden: {sorted(providers & PAID_VIDEO_PROVIDERS)}")
   109|
   110|    return {
   111|        "schema_version": "free_production_jobs.v1",
   112|        "video": asset_plan["video"],
   113|        "budget_policy": {
   114|            "paid_video_credits_allowed": False,
   115|            "allowed_providers": ["local_renderer", "local_ffmpeg", "free_nvidia_llm"],
   116|            "nvidia_llm_scope": "text_refinement_only",
   117|        },
   118|        "jobs": jobs,
   119|        "assembly": {
   120|            "provider": "local_ffmpeg",
   121|            "cost": "free",
   122|            "steps": [
   123|                "render local scene clips",
   124|                "compose kinetic captions",
   125|                "add free/local SFX placeholders",
   126|                "assemble vertical MP4 with FFmpeg",
   127|            ],
   128|        },
   129|    }
   130|
   131|
   132|def _jobs_filename(video: dict[str, Any]) -> str:
   133|    title_slug = _slug(video["title"])
   134|    video_id = video["id"]
   135|    if video_id.endswith(title_slug):
   136|        return f"{video_id}.json"
   137|    return f"{video_id}-{title_slug}.json"
   138|
   139|
   140|def write_free_production_jobs(asset_plans: list[dict[str, Any]], output_dir: Path) -> list[Path]:
   141|    """Write free production job specs as JSON files."""
   142|    output_dir.mkdir(parents=True, exist_ok=True)
   143|    paths: list[Path] = []
   144|    for asset_plan in asset_plans:
   145|        jobs = build_free_production_jobs(asset_plan)
   146|        path = output_dir / _jobs_filename(jobs["video"])
   147|        path.write_text(json.dumps(jobs, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
   148|        paths.append(path)
   149|    return paths
   150|


     1|"""Deterministic preview renderer for AutoShortsBot render manifests.
     2|
     3|This v1 renderer intentionally uses manifest-driven cards/placeholders instead of
     4|AI-generated video. It renders a simple vertical MP4 suitable for timing and
     5|approval review before investing in polished assets.
     6|"""
     7|
     8|from __future__ import annotations
     9|
    10|import shutil
    11|import subprocess
    12|import tempfile
    13|import textwrap
    14|from pathlib import Path
    15|from typing import Any
    16|
    17|from PIL import Image, ImageDraw, ImageFont
    18|
    19|BACKGROUND_BY_TYPE = {
    20|    "kinetic_text": ((19, 23, 38), (79, 70, 229)),
    21|    "screen_recording": ((15, 23, 42), (14, 165, 233)),
    22|    "card_animation": ((17, 24, 39), (245, 158, 11)),
    23|    "stock_broll": ((28, 25, 23), (34, 197, 94)),
    24|    "generated_background": ((24, 24, 27), (168, 85, 247)),
    25|}
    26|
    27|
    28|def _font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    29|    candidates = [
    30|        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
    31|        "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
    32|    ]
    33|    for path in candidates:
    34|        if Path(path).exists():
    35|            return ImageFont.truetype(path, size=size)
    36|    return ImageFont.load_default()
    37|
    38|
    39|def _wrap_text(text: str, width_chars: int) -> str:
    40|    return "\n".join(textwrap.wrap(" ".join(text.split()), width=width_chars))
    41|
    42|
    43|def _text_block_size(draw: ImageDraw.ImageDraw, text: str, font: Any, spacing: int) -> tuple[int, int]:
    44|    lines = text.splitlines() or [""]
    45|    boxes = [draw.textbbox((0, 0), line, font=font) for line in lines]
    46|    width = max((box[2] - box[0] for box in boxes), default=0)
    47|    height = sum(box[3] - box[1] for box in boxes) + spacing * max(0, len(lines) - 1)
    48|    return int(width), int(height)
    49|
    50|
    51|def _fit_text_block(
    52|    draw: ImageDraw.ImageDraw,
    53|    text: str,
    54|    max_width: int,
    55|    max_height: int,
    56|    max_lines: int,
    57|    preferred_size: int,
    58|    min_size: int,
    59|    bold: bool,
    60|) -> dict[str, Any]:
    61|    """Choose a readable font size and wrapping that fits a text box."""
    62|    cleaned = " ".join(text.split())
    63|    for size in range(preferred_size, min_size - 1, -1):
    64|        wrap_chars = max(10, round(max_width / (size * 0.54)))
    65|        wrapped = _wrap_text(cleaned, wrap_chars)
    66|        lines = wrapped.splitlines()
    67|        font = _font(size, bold=bold)
    68|        spacing = max(4, round(size * 0.25))
    69|        block_width, block_height = _text_block_size(draw, wrapped, font, spacing)
    70|        if len(lines) <= max_lines and block_width <= max_width and block_height <= max_height:
    71|            return {
    72|                "text": wrapped,
    73|                "font": font,
    74|                "font_size": size,
    75|                "line_count": len(lines),
    76|                "spacing": spacing,
    77|            }
    78|
    79|    fallback_font = _font(min_size, bold=bold)
    80|    fallback_chars = max(10, round(max_width / (min_size * 0.54)))
    81|    fallback = _wrap_text(cleaned, fallback_chars)
    82|    return {
    83|        "text": fallback,
    84|        "font": fallback_font,
    85|        "font_size": min_size,
    86|        "line_count": len(fallback.splitlines()),
    87|        "spacing": max(4, round(min_size * 0.25)),
    88|    }
    89|
    90|
    91|def _gradient(width: int, height: int, start: tuple[int, int, int], end: tuple[int, int, int]) -> Image.Image:
    92|    image = Image.new("RGB", (width, height), start)
    93|    draw = ImageDraw.Draw(image)
    94|    for y in range(height):
    95|        ratio = y / max(1, height - 1)
    96|        color = tuple(round(start[i] * (1 - ratio) + end[i] * ratio) for i in range(3))
    97|        draw.line([(0, y), (width, y)], fill=color)
    98|    return image
    99|
   100|
   101|def _draw_multiline_center(
   102|    draw: ImageDraw.ImageDraw,
   103|    xy: tuple[int, int],
   104|    text: str,
   105|    font: Any,
   106|    fill: tuple[int, int, int],
   107|    spacing: int = 8,
   108|) -> None:
   109|    lines = text.splitlines()
   110|    line_boxes = [draw.textbbox((0, 0), line, font=font) for line in lines]
   111|    line_heights = [box[3] - box[1] for box in line_boxes]
   112|    total_height = sum(line_heights) + spacing * max(0, len(lines) - 1)
   113|    y = xy[1] - total_height // 2
   114|    for line, box, line_height in zip(lines, line_boxes, line_heights):
   115|        line_width = box[2] - box[0]
   116|        draw.text((xy[0] - line_width // 2, y), line, font=font, fill=fill)
   117|        y += line_height + spacing
   118|
   119|
   120|def _render_scene_image(
   121|    item: dict[str, Any], manifest: dict[str, Any], width: int, height: int
   122|) -> tuple[Image.Image, dict[str, int]]:
   123|    start, end = BACKGROUND_BY_TYPE.get(item["type"], ((24, 24, 27), (64, 64, 64)))
   124|    image = _gradient(width, height, start, end)
   125|    draw = ImageDraw.Draw(image)
   126|
   127|    margin = round(width * 0.075)
   128|    title_font = _font(max(18, round(width * 0.06)), bold=True)
   129|    label_font = _font(max(12, round(width * 0.032)), bold=False)
   130|    small_font = _font(max(12, round(width * 0.034)), bold=False)
   131|
   132|    # Safe area and content card.
   133|    card_top = round(height * 0.18)
   134|    card_bottom = round(height * 0.78)
   135|    draw.rounded_rectangle(
   136|        [margin, card_top, width - margin, card_bottom],
   137|        radius=round(width * 0.04),
   138|        fill=(255, 255, 255),
   139|        outline=(226, 232, 240),
   140|        width=max(2, width // 180),
   141|    )
   142|
   143|    draw.text((margin, round(height * 0.055)), manifest["video"]["id"], font=label_font, fill=(226, 232, 240))
   144|    draw.text((margin, round(height * 0.09)), item["type"].replace("_", " ").upper(), font=title_font, fill=(255, 255, 255))
   145|
   146|    scene_label = item["id"].replace("_", " ").upper()
   147|    draw.text((margin * 1.4, card_top + margin), scene_label, font=small_font, fill=(71, 85, 105))
   148|
   149|    main_fit = _fit_text_block(
   150|        draw,
   151|        item["text"],
   152|        max_width=width - margin * 4,
   153|        max_height=round(height * 0.24),
   154|        max_lines=4,
   155|        preferred_size=max(22, round(width * 0.078)),
   156|        min_size=max(18, round(width * 0.063)),
   157|        bold=True,
   158|    )
   159|    _draw_multiline_center(
   160|        draw,
   161|        (width // 2, round(height * 0.45)),
   162|        main_fit["text"],
   163|        main_fit["font"],
   164|        (15, 23, 42),
   165|        spacing=main_fit["spacing"],
   166|    )
   167|
   168|    retention_fit = _fit_text_block(
   169|        draw,
   170|        item.get("retention_goal", ""),
   171|        max_width=width - margin * 4,
   172|        max_height=round(height * 0.14),
   173|        max_lines=3,
   174|        preferred_size=max(14, round(width * 0.048)),
   175|        min_size=max(12, round(width * 0.041)),
   176|        bold=False,
   177|    )
   178|    _draw_multiline_center(
   179|        draw,
   180|        (width // 2, round(height * 0.69)),
   181|        retention_fit["text"],
   182|        retention_fit["font"],
   183|        (71, 85, 105),
   184|        spacing=retention_fit["spacing"],
   185|    )
   186|
   187|    timing = f"{item['start_ms'] / 1000:.1f}s – {(item['start_ms'] + item['duration_ms']) / 1000:.1f}s"
   188|    draw.text((margin, height - round(height * 0.075)), timing, font=label_font, fill=(226, 232, 240))
   189|    draw.text((width - margin - draw.textlength("PREVIEW", font=label_font), height - round(height * 0.075)), "PREVIEW", font=label_font, fill=(226, 232, 240))
   190|    return image, {
   191|        "main_font_size": main_fit["font_size"],
   192|        "main_line_count": main_fit["line_count"],
   193|        "retention_font_size": retention_fit["font_size"],
   194|        "retention_line_count": retention_fit["line_count"],
   195|    }
   196|
   197|
   198|def build_preview_frames(manifest: dict[str, Any], width: int = 540, height: int = 960) -> list[dict[str, Any]]:
   199|    """Build one representative image per visual timeline item."""
   200|    visual_items = [item for item in manifest["timeline"] if item["track"] == "visual"]
   201|    frames: list[dict[str, Any]] = []
   202|    for item in visual_items:
   203|        image, typography = _render_scene_image(item, manifest, width, height)
   204|        frames.append(
   205|            {
   206|                "scene_id": item["id"],
   207|                "start_ms": item["start_ms"],
   208|                "duration_ms": item["duration_ms"],
   209|                "text": item["text"],
   210|                "typography": typography,
   211|                "image": image,
   212|            }
   213|        )
   214|    return frames
   215|
   216|
   217|def write_frame_images(frames: list[dict[str, Any]], output_dir: Path) -> list[Path]:
   218|    """Write preview frames as numbered PNG files."""
   219|    output_dir.mkdir(parents=True, exist_ok=True)
   220|    paths: list[Path] = []
   221|    for index, frame in enumerate(frames, start=1):
   222|        path = output_dir / f"frame_{index:03d}_{frame['scene_id']}.png"
   223|        frame["image"].save(path)
   224|        paths.append(path)
   225|    return paths
   226|
   227|
   228|def _write_concat_file(frames: list[dict[str, Any]], image_paths: list[Path], concat_path: Path) -> None:
   229|    lines: list[str] = []
   230|    for frame, image_path in zip(frames, image_paths):
   231|        seconds = max(0.5, frame["duration_ms"] / 1000)
   232|        lines.append(f"file '{image_path.as_posix()}'")
   233|        lines.append(f"duration {seconds:.3f}")
   234|    if image_paths:
   235|        lines.append(f"file '{image_paths[-1].as_posix()}'")
   236|    concat_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
   237|
   238|
   239|def render_preview_video(
   240|    manifest: dict[str, Any],
   241|    output_path: Path,
   242|    width: int = 540,
   243|    height: int = 960,
   244|    fps: int = 24,
   245|) -> dict[str, Any]:
   246|    """Render a manifest-driven preview MP4 using Pillow frames and ffmpeg."""
   247|    ffmpeg = shutil.which("ffmpeg")
   248|    if not ffmpeg:
   249|        raise RuntimeError("ffmpeg is required to render preview MP4 files")
   250|
   251|    output_path.parent.mkdir(parents=True, exist_ok=True)
   252|    frames = build_preview_frames(manifest, width=width, height=height)
   253|    with tempfile.TemporaryDirectory(prefix="autoshorts_preview_") as tmp:
   254|        tmp_dir = Path(tmp)
   255|        image_paths = write_frame_images(frames, tmp_dir / "frames")
   256|        concat_path = tmp_dir / "concat.txt"
   257|        _write_concat_file(frames, image_paths, concat_path)
   258|        command = [
   259|            ffmpeg,
   260|            "-y",
   261|            "-f",
   262|            "concat",
   263|            "-safe",
   264|            "0",
   265|            "-i",
   266|            str(concat_path),
   267|            "-vf",
   268|            f"fps={fps},format=yuv420p",
   269|            "-movflags",
   270|            "+faststart",
   271|            str(output_path),
   272|        ]
   273|        completed = subprocess.run(command, check=True, capture_output=True, text=True)
   274|    return {
   275|        "output_path": output_path,
   276|        "frame_count": len(frames),
   277|        "fps": fps,
   278|        "stderr": completed.stderr,
   279|    }
   280|


Explanation:
**Implementing renderer**

I'm thinking about needing to implement a local renderer. It makes sense to start by inspecting the job JSON to gather the necessary details. I wonder if analyzing the JSON will help me understand what needs to be rendered properly. It's all about making sure I have the right information before diving into the implementation. Let's see how that goes!


     1|import json
     2|from pathlib import Path
     3|
     4|from autoshorts.free_production_jobs import build_free_production_jobs, write_free_production_jobs
     5|
     6|
     7|def _asset_plan() -> dict:
     8|    path = Path("data/asset_plans/mechanism_pilots/hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann.json")
     9|    return json.loads(path.read_text(encoding="utf-8"))
    10|
    11|
    12|def test_free_production_jobs_never_use_paid_video_credit_providers():
    13|    jobs = build_free_production_jobs(_asset_plan())
    14|
    15|    assert jobs["schema_version"] == "free_production_jobs.v1"
    16|    assert jobs["budget_policy"]["paid_video_credits_allowed"] is False
    17|    forbidden = {"kling", "hailuo", "runway", "veo", "pika", "luma"}
    18|    providers = {job["provider"] for job in jobs["jobs"]}
    19|    assert providers.isdisjoint(forbidden)
    20|    assert {job["provider"] for job in jobs["jobs"]} <= {
    21|        "local_renderer",
    22|        "local_ffmpeg",
    23|        "free_nvidia_llm",
    24|    }
    25|
    26|
    27|def test_free_production_jobs_map_cinematic_scenes_to_local_motion_design():
    28|    jobs = build_free_production_jobs(_asset_plan())
    29|
    30|    first = jobs["jobs"][0]
    31|
    32|    assert first["asset_mode"] == "cinematic_ai_video"
    33|    assert first["provider"] == "local_renderer"
    34|    assert first["execution_mode"] == "procedural_motion"
    35|    assert "Ken Burns" in first["free_workflow"] or "parallax" in first["free_workflow"]
    36|    assert first["requires_api_key"] is False
    37|    assert first["cost"] == "free"
    38|
    39|
    40|def test_free_production_jobs_use_nvidia_llm_only_for_optional_prompt_refinement():
    41|    jobs = build_free_production_jobs(_asset_plan())
    42|
    43|    llm_jobs = [job for job in jobs["jobs"] if job["provider"] == "free_nvidia_llm"]
    44|
    45|    assert llm_jobs
    46|    assert all(job["requires_api_key"] == "NVIDIA_API_KEY" for job in llm_jobs)
    47|    assert all(job["side_effect"] == "text_refinement_only" for job in llm_jobs)
    48|    assert all("video generation" not in job["free_workflow"].lower() for job in llm_jobs)
    49|
    50|
    51|def test_write_free_production_jobs_outputs_json(tmp_path):
    52|    paths = write_free_production_jobs([_asset_plan()], tmp_path)
    53|
    54|    assert len(paths) == 1
    55|    payload = json.loads(paths[0].read_text(encoding="utf-8"))
    56|    assert payload["schema_version"] == "free_production_jobs.v1"
    57|    assert payload["video"]["id"] == "hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann"
    58|


     1|"""Generate free production job specs from AutoShortsBot asset plans."""
     2|
     3|from __future__ import annotations
     4|
     5|import argparse
     6|import json
     7|import sys
     8|from pathlib import Path
     9|
    10|PROJECT_ROOT = Path(__file__).resolve().parents[1]
    11|if str(PROJECT_ROOT) not in sys.path:
    12|    sys.path.insert(0, str(PROJECT_ROOT))
    13|
    14|from autoshorts.free_production_jobs import write_free_production_jobs
    15|
    16|
    17|def _load_asset_plans(path: Path) -> list[dict]:
    18|    if path.is_dir():
    19|        files = sorted(path.glob("*.json"))
    20|    else:
    21|        files = [path]
    22|    return [json.loads(file.read_text(encoding="utf-8")) for file in files]
    23|
    24|
    25|def main() -> None:
    26|    parser = argparse.ArgumentParser(description="Generate free production job specs.")
    27|    parser.add_argument("--asset-plan", default="data/asset_plans/mechanism_pilots")
    28|    parser.add_argument("--out", default="data/free_production_jobs/mechanism_pilots")
    29|    args = parser.parse_args()
    30|
    31|    asset_plans = _load_asset_plans(Path(args.asset_plan))
    32|    paths = write_free_production_jobs(asset_plans, Path(args.out))
    33|    print(f"Generated {len(paths)} free production job specs in {args.out}")
    34|    for path in paths:
    35|        print(path)
    36|
    37|
    38|if __name__ == "__main__":
    39|    main()
    40|


     1|{
     2|  "schema_version": "free_production_jobs.v1",
     3|  "video": {
     4|    "id": "hidden-money-systems-warum-ein-billiges-auto-teuer-sein-kann",
     5|    "title": "Warum ein billiges Auto teuer sein kann",
     6|    "pillar": "hidden_money_systems",
     7|    "duration_ms": 34000,
     8|    "aspect_ratio": "9:16",
     9|    "resolution": {
    10|      "width": 1080,
    11|      "height": 1920
    12|    },
    13|    "fps": 30
    14|  },
    15|  "budget_policy": {
    16|    "paid_video_credits_allowed": false,
    17|    "allowed_providers": [
    18|      "local_renderer",
    19|      "local_ffmpeg",
    20|      "free_nvidia_llm"
    21|    ],
    22|    "nvidia_llm_scope": "text_refinement_only"
    23|  },
    24|  "jobs": [
    25|    {
    26|      "job_id": "scene_01_local_motion",
    27|      "scene_id": "scene_01",
    28|      "asset_mode": "cinematic_ai_video",
    29|      "provider": "local_renderer",
    30|      "execution_mode": "procedural_motion",
    31|      "cost": "free",
    32|      "requires_api_key": false,
    33|      "duration_seconds": 3,
    34|      "overlay_text": "Dieser Occasion-Käufer denkt, er spart 4'000 Franken.",
    35|      "motion_directive": "slow handheld push-in, subtle parallax, natural micro-movements",
    36|      "free_workflow": "Use local procedural motion: illustrated/abstract background, object-layer parallax, Ken Burns push-in, depth shadows, speed ramps, camera shake on beat, and renderer captions. No paid video-generation credits.",
    37|      "source_prompt": "Vertical 9:16 cinematic-looking local motion scene, a realistic creator at a desk late evening, laptop open to an AI chat, thoughtful but in control, hands moving between keyboard and notebook. Mood: focused, modern, high-retention social video, dramatic lighting simulated with gradients and shadows. Scene intent: 3D car x-ray with price tag layers and risk meters freezes mid-action with warning signal. Camera: slow handheld push-in, subtle parallax, natural micro-movements. Use local procedural animation/parallax rather than paid AI-video credits; keep captions renderer-overlay only.",
    38|      "negative_prompt": "no readable text, no subtitles, no logos, no watermarks, no distorted hands, no fake UI text, no slideshow, no static image, no uncanny faces",
    39|      "outputs": {
    40|        "clip_placeholder": "outputs/clips/scene_01.mp4",
    41|        "metadata_placeholder": "outputs/clips/scene_01.json"
    42|      }
    43|    },
    44|    {
    45|      "job_id": "scene_01_nvidia_prompt_refine",
    46|      "scene_id": "scene_01",
    47|      "asset_mode": "cinematic_ai_video",
    48|      "provider": "free_nvidia_llm",
    49|      "execution_mode": "prompt_to_motion_brief",
    50|      "cost": "free",
    51|      "requires_api_key": "NVIDIA_API_KEY",
    52|      "side_effect": "text_refinement_only",
    53|      "free_workflow": "Optionally ask a free build.nvidia.com LLM to compress the visual brief into local renderer instructions. This is text planning only and must not call paid video-credit providers.",
    54|      "input": {
    55|        "visual_direction": "3D car x-ray with price tag layers and risk meters freezes mid-action with warning signal.",
    56|        "retention_goal": "Open a visual loop immediately; make the viewer need the hidden explanation.",
    57|        "motion_directive": "slow handheld push-in, subtle parallax, natural micro-movements"
    58|      },
    59|      "output_placeholder": "outputs/briefs/scene_01_motion_brief.json"
    60|    },
    61|    {
    62|      "job_id": "scene_02_local_motion",
    63|      "scene_id": "scene_02",
    64|      "asset_mode": "cinematic_ai_video",
    65|      "provider": "local_renderer",
    66|      "execution_mode": "procedural_motion",
    67|      "cost": "free",
    68|      "requires_api_key": false,
    69|      "duration_seconds": 4,
    70|      "overlay_text": "Das sieht harmlos aus, bis du den Mechanismus dahinter siehst.",
    71|      "motion_directive": "slow handheld push-in, subtle parallax, natural micro-movements",
    72|      "free_workflow": "Use local procedural motion: illustrated/abstract background, object-layer parallax, Ken Burns push-in, depth shadows, speed ramps, camera shake on beat, and renderer captions. No paid video-generation credits.",
    73|      "source_prompt": "Vertical 9:16 cinematic-looking local motion scene, a realistic creator at a desk late evening, laptop open to an AI chat, thoughtful but in control, hands moving between keyboard and notebook. Mood: focused, modern, high-retention social video, dramatic lighting simulated with gradients and shadows. Scene intent: surface layer peels away to reveal the hidden system underneath. Camera: slow handheld push-in, subtle parallax, natural micro-movements. Use local procedural animation/parallax rather than paid AI-video credits; keep captions renderer-overlay only.",
    74|      "negative_prompt": "no readable text, no subtitles, no logos, no watermarks, no distorted hands, no fake UI text, no slideshow, no static image, no uncanny faces",
    75|      "outputs": {
    76|        "clip_placeholder": "outputs/clips/scene_02.mp4",
    77|        "metadata_placeholder": "outputs/clips/scene_02.json"
    78|      }
    79|    },
    80|    {
    81|      "job_id": "scene_02_nvidia_prompt_refine",
    82|      "scene_id": "scene_02",
    83|      "asset_mode": "cinematic_ai_video",
    84|      "provider": "free_nvidia_llm",
    85|      "execution_mode": "prompt_to_motion_brief",
    86|      "cost": "free",
    87|      "requires_api_key": "NVIDIA_API_KEY",
    88|      "side_effect": "text_refinement_only",
    89|      "free_workflow": "Optionally ask a free build.nvidia.com LLM to compress the visual brief into local renderer instructions. This is text planning only and must not call paid video-credit providers.",
    90|      "input": {
    91|        "visual_direction": "surface layer peels away to reveal the hidden system underneath.",
    92|        "retention_goal": "Make the surface-level interpretation feel incomplete.",
    93|        "motion_directive": "slow handheld push-in, subtle parallax, natural micro-movements"
    94|      },
    95|      "output_placeholder": "outputs/briefs/scene_02_motion_brief.json"
    96|    },
    97|    {
    98|      "job_id": "scene_03_local_motion",
    99|      "scene_id": "scene_03",
   100|      "asset_mode": "mechanism_animation",
   101|      "provider": "local_renderer",
   102|      "execution_mode": "mechanism_reveal_animation",
   103|      "cost": "free",
   104|      "requires_api_key": false,
   105|      "duration_seconds": 6,
   106|      "overlay_text": "Röntgenblick durch Preis, Reparaturrisiko, Standtage, Händler-Marge und",
   107|      "motion_directive": "large kinetic caption words pop and slide with beat-synced emphasis",
   108|      "free_workflow": "Use local mechanism animation: vector-like shapes, x-ray/layer reveals, animated arrows, risk meters, price-tag layers, sequential labels, and beat-synced clicks.",
   109|      "source_prompt": "Vertical 9:16 mechanism-reveal animation. Build a clear visual explanation with moving layers, x-ray/röntgen reveal, arrows, meters, object cutaways, and sequential labels added by our renderer. Mechanism task: 3D car x-ray with price tag layers and risk meters Motion: sequential layer reveal every 1-2 seconds. No static holds longer than one second; keep all readable text as renderer overlays.",
   110|      "negative_prompt": "no static diagram, no dense labels, no tiny text, no unreadable chart junk, no generic infographic",
   111|      "outputs": {
   112|        "clip_placeholder": "outputs/clips/scene_03.mp4",
   113|        "metadata_placeholder": "outputs/clips/scene_03.json"
   114|      }
   115|    },
   116|    {
   117|      "job_id": "scene_03_nvidia_prompt_refine",
   118|      "scene_id": "scene_03",
   119|      "asset_mode": "mechanism_animation",
   120|      "provider": "free_nvidia_llm",
   121|      "execution_mode": "prompt_to_motion_brief",
   122|      "cost": "free",
   123|      "requires_api_key": "NVIDIA_API_KEY",
   124|      "side_effect": "text_refinement_only",
   125|      "free_workflow": "Optionally ask a free build.nvidia.com LLM to compress the visual brief into local renderer instructions. This is text planning only and must not call paid video-credit providers.",
   126|      "input": {
   127|        "visual_direction": "3D car x-ray with price tag layers and risk meters",
   128|        "retention_goal": "Reveal the invisible system through moving layers, not narration alone.",
   129|        "motion_directive": "large kinetic caption words pop and slide with beat-synced emphasis"
   130|      },
   131|      "output_placeholder": "outputs/briefs/scene_03_motion_brief.json"
   132|    },
   133|    {
   134|      "job_id": "scene_04_local_motion",
   135|      "scene_id": "scene_04",
   136|      "asset_mode": "mechanism_animation",
   137|      "provider": "local_renderer",
   138|      "execution_mode": "mechanism_reveal_animation",
   139|      "cost": "free",
   140|      "requires_api_key": false,
   141|      "duration_seconds": 6,
   142|      "overlay_text": "Der teuerste Fehler ist nicht der Kaufpreis, sondern der Zeitpunkt.",
   143|      "motion_directive": "large kinetic caption words pop and slide with beat-synced emphasis",
   144|      "free_workflow": "Use local mechanism animation: vector-like shapes, x-ray/layer reveals, animated arrows, risk meters, price-tag layers, sequential labels, and beat-synced clicks.",
   145|      "source_prompt": "Vertical 9:16 mechanism-reveal animation. Build a clear visual explanation with moving layers, x-ray/röntgen reveal, arrows, meters, object cutaways, and sequential labels added by our renderer. Mechanism task: camera snaps from obvious cause to real cause; wrong layer fades out. Motion: snap zoom + wrong answer collapses. No static holds longer than one second; keep all readable text as renderer overlays.",
   146|      "negative_prompt": "no static diagram, no dense labels, no tiny text, no unreadable chart junk, no generic infographic",
   147|      "outputs": {
   148|        "clip_placeholder": "outputs/clips/scene_04.mp4",
   149|        "metadata_placeholder": "outputs/clips/scene_04.json"
   150|      }
   151|    },
   152|    {
   153|      "job_id": "scene_04_nvidia_prompt_refine",
   154|      "scene_id": "scene_04",
   155|      "asset_mode": "mechanism_animation",
   156|      "provider": "free_nvidia_llm",
   157|      "execution_mode": "prompt_to_motion_brief",
   158|      "cost": "free",
   159|      "requires_api_key": "NVIDIA_API_KEY",
   160|      "side_effect": "text_refinement_only",
   161|      "free_workflow": "Optionally ask a free build.nvidia.com LLM to compress the visual brief into local renderer instructions. This is text planning only and must not call paid video-credit providers.",
   162|      "input": {
   163|        "visual_direction": "camera snaps from obvious cause to real cause; wrong layer fades out.",
   164|        "retention_goal": "Reframe the obvious cause into the real cause.",
   165|        "motion_directive": "large kinetic caption words pop and slide with beat-synced emphasis"
   166|      },
   167|      "output_placeholder": "outputs/briefs/scene_04_motion_brief.json"
   168|    },
   169|    {
   170|      "job_id": "scene_05_local_motion",
   171|      "scene_id": "scene_05",
   172|      "asset_mode": "kinetic_text_overlay",
   173|      "provider": "local_ffmpeg",
   174|      "execution_mode": "kinetic_typography",
   175|      "cost": "free",
   176|      "requires_api_key": false,
   177|      "duration_seconds": 6,
   178|      "overlay_text": "Morgen zeige ich das nächste versteckte System, bevor es dich Geld",
   179|      "motion_directive": "large kinetic caption words pop and slide with beat-synced emphasis",
   180|      "free_workflow": "Use local kinetic typography: large words, scale pops, slide-ins, glow pulses, and FFmpeg/Pillow compositing.",
   181|      "source_prompt": "Kinetic text overlay scene. Use large caption words, fast emphasis, scale pops, and clean background motion. Overlay text: Morgen zeige ich das nächste versteckte System, bevor es dich Geld",
   182|      "negative_prompt": "no long paragraphs, no tiny footnotes, no static card",
   183|      "outputs": {
   184|        "clip_placeholder": "outputs/clips/scene_05.mp4",
   185|        "metadata_placeholder": "outputs/clips/scene_05.json"
   186|      }
   187|    }
   188|  ],
   189|  "assembly": {
   190|    "provider": "local_ffmpeg",
   191|    "cost": "free",
   192|    "steps": [
   193|      "render local scene clips",
   194|      "compose kinetic captions",
   195|      "add free/local SFX placeholders",
   196|      "assemble vertical MP4 with FFmpeg"
   197|    ]
   198|  }
   199|}
   200|


Explanation:
**Designing motion renderer tests**

I need to create tests first for the local_motion_renderer.py. It should render from free jobs to MP4 and include metadata. I’m considering using Pillow for animation, likely at full 1080p, but for tests, I want to keep it lighter with smaller width and lower fps. The behavior must filter local jobs, exclude NVIDIA, render individual scene clips, and compile them into MP4 with scene count. I also need to ensure that ffmpeg is installed and will test using TDD techniques.
**Implementing local motion renderer**

I’m working on the local_motion_renderer.py. I need to define parameters like _font, _wrap, and _gradient. I see that local production jobs have mixed jobs from local_renderer and local_ffmpeg. The function to build scene frames should return frame metadata, perhaps instead of images? I might generate frames as a PNG sequence temporarily and then encode them into MP4. There’s a duration mismatch issue to address since the expected duration is 34 seconds, while free jobs sum to 25 seconds. I'll need to inspect the data further.


{"total_count": 125, "matches": [{"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_asset_plan.py", "line": 45, "content": "        assert \"paid ai-video credits\" in prompt.lower()"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_asset_plan.py", "line": 46, "content": "        assert \"no readable text\" in scene[\"negative_prompt\"].lower()"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_asset_plan.py", "line": 47, "content": "        assert scene[\"duration_seconds\"] >= 3"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_asset_plan.py", "line": 48, "content": "        assert scene[\"duration_seconds\"] <= 6"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_asset_plan.py", "line": 49, "content": ""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_asset_plan.py", "line": 50, "content": ""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_review_package.py", "line": 28, "content": "    assert package[\"id\"] == \"ai-004\""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_review_package.py", "line": 29, "content": "    assert package[\"pillar\"] == \"ai_life_systems\""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_review_package.py", "line": 30, "content": "    assert package[\"duration_seconds\"] == 43"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_review_package.py", "line": 31, "content": "    assert package[\"quality_score\"] == 10"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_review_package.py", "line": 32, "content": "    assert package[\"visual_scene_count\"] == 5"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_testbatch_002.py", "line": 25, "content": ""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_testbatch_002.py", "line": 26, "content": "    assert min(scores) >= 8"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_testbatch_002.py", "line": 27, "content": "    assert sum(1 for idea in batch[\"ideas\"] if idea[\"duration_seconds\"] >= 60) == 3"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 12, "content": ""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 13, "content": "    assert draft[\"id\"] == idea[\"id\"]"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 14, "content": "    assert draft[\"duration_seconds\"] == idea[\"duration_seconds\"]"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 15, "content": "    assert draft[\"sections\"][0][\"label\"] == \"Hook\""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 16, "content": "    assert draft[\"sections\"][0][\"text\"] == idea[\"hook\"]"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 17, "content": "    assert sum(section[\"seconds\"] for section in draft[\"sections\"]) == idea[\"duration_seconds\"]"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 18, "content": "    assert draft[\"platform_note\"] == \"Growth Short\""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 19, "content": "    assert draft[\"hashtags\"]"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 22, "content": "def test_build_script_draft_marks_tiktok_rewards_candidate():"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 23, "content": "    batch = load_testbatch(Path(\"data/drafts/testbatch_001.json\"))"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 24, "content": "    idea = next(item for item in batch[\"ideas\"] if item[\"duration_seconds\"] >= 60)"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 25, "content": ""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_script_drafts.py", "line": 26, "content": "    draft = build_script_draft(idea)"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_voiceover_finalizer.py", "line": 13, "content": "    assert final[\"id\"] == \"ai-004\""}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_voiceover_finalizer.py", "line": 14, "content": "    assert final[\"voiceover\"].startswith(idea[\"hook\"])"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_voiceover_finalizer.py", "line": 15, "content": "    assert final[\"estimated_seconds\"] <= idea[\"duration_seconds\"] + 4"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_voiceover_finalizer.py", "line": 16, "content": "    assert final[\"estimated_seconds\"] >= idea[\"duration_seconds\"] - 12"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_voiceover_finalizer.py", "line": 17, "content": "    assert final[\"word_count\"] > 70"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_voiceover_finalizer.py", "line": 18, "content": "    assert final[\"caption_beats\"][0][\"start\"] == 0"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 15, "content": "    idea = {"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 16, "content": "        \"hook\": \"In this video I will talk about productivity tips.\","}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 17, "content": "        \"duration_seconds\": 75,"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 18, "content": "        \"visual_concept\": \"Static slideshow with abstract background.\","}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 19, "content": "        \"script_outline\": \"Some generic advice about habits and routines.\","}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 31, "content": "    idea = {"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 32, "content": "        \"hook\": \"You do not need more discipline. You need a smaller first step.\","}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 33, "content": "        \"duration_seconds\": 38,"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 34, "content": "        \"visual_concept\": \"Pattern interrupt: messy desk becomes one tiny next action with timer and captions.\","}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/tests/test_retention.py", "line": 35, "content": "        \"script_outline\": \"Challenge the discipline myth, show the two-minute starter, give one concrete example, close the loop.\","}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/autoshorts/review_package.py", "line": 37, "content": "    visual_types = sorted({item[\"type\"] for item in visual_items})"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/autoshorts/review_package.py", "line": 38, "content": "    return {"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/autoshorts/review_package.py", "line": 39, "content": "        \"duration_seconds\": round(manifest[\"video\"][\"duration_ms\"] / 1000),"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/autoshorts/review_package.py", "line": 40, "content": "        \"visual_scene_count\": len(visual_items),"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/autoshorts/review_package.py", "line": 41, "content": "        \"caption_beat_count\": len(caption_items),"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/autoshorts/review_package.py", "line": 56, "content": "        \"pillar\": idea[\"pillar\"],"}, {"path": "/home/agent/jarvis_runtime/AutoShortsBot/autoshorts/review_package.py", "line": 57, "content": "        \"platforms\": idea[\"platform_target\"],"}], "truncated": true}

[Hint: Results truncated. Use offset=50 to see more, or narrow with a more specific pattern or file_glob.]