[M G] co/Civitai/Qwen-Image-2512-GGUF "Civitai/Qwen-Image-2512-GGUF · Hugging Face"
Sollen wir qwen image auch selber über comfyui laufen lassen? Oder wäre das parallel mit dem videomodell nicht möglich?
 für GGUF + ComfyUI ist aktuell Qwen-Image-2512-GGUF wahrscheinlich die beste praktische Wahl – nicht das ältere Qwen-Image, sondern die neuere **Qwen-Image-2512**-Variante.

Meine Rangliste für deinen Usecase:

| Zweck                                                                     | Empfehlung                    | Warum                                                                                |
| ------------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Beste kostenlose GGUF-Bildgenerierung in ComfyUI                      | Qwen-Image-2512-GGUF      | Sehr stark bei Realismus, Text im Bild, Prompt-Folgen; Apache-2.0; direkt als GGUF verfügbar. Unsloth beschreibt es als GGUF-Quantisierung mit ComfyUI-GGUF-Support. ([Hugging Face][1]) |
| Bildbearbeitung / Referenzbilder / Text in bestehenden Bildern ändern | Qwen-Image-Edit-2511-GGUF | Separates Edit-Modell, GGUF, nutzt ComfyUI-GGUF. ([Hugging Face][2])                                                                                |
| Schnell / weniger VRAM / experimentell                                | Z-Image Turbo GGUF        | Läuft mit ComfyUI-GGUF und ist deutlich leichter/schneller, aber nicht die beste Gesamtqualität. ([Hugging Face][3])                                                                     |
| Alternative zu Qwen, stark aber schwerer                              | FLUX.2-dev-GGUF           | Gute Qualität, aber grösser, Lizenz weniger frei/praktisch als Apache-2.0; Q4_K_M allein ca. 18.7 GB. ([Hugging Face][4])                                                                |
| Aktuell stärkstes Open-Weights-Modell allgemein                       | HiDream-O1-Image-Dev-2604 | Führt bei Artificial Analysis unter Open-Weights, aber der ComfyUI-Weg ist derzeit eher über Custom Node/FP8, nicht sauber als etablierter GGUF-Standard. ([artificialanalysis.ai][5])   |

Kurz gesagt:
Für deine lokale ComfyUI/Hermes-Pipeline würde ich Qwen-Image-2512-GGUF nehmen.

Die passende Auswahl nach VRAM:

|        VRAM | Modell/Quantisierung                                   |
| ----------: | ------------------------------------------------------ |
| 8–12 GB | Qwen-Image-2512 Q4, Low-VRAM, langsam aber möglich |
|   16 GB | Q5 oder Q6 testen                                      |
|   24 GB | Q6 oder Q8, beste Balance aus Qualität und Stabilität  |
|   <8 GB | eher Z-Image Turbo GGUF statt Qwen                 |

Wichtig: Qwen-Image-2512 ist ein 20B Image-Diffusion-Modell, kein klassisches LLM. Es nutzt aber grosse Text-/Vision-Komponenten. Die GGUF-Dateien sind relativ gross: bei einer Qwen-Image-2512-GGUF-Variante sind z. B. Q4 ca. 11.5 GB, Q6 ca. 16.8 GB, Q8 ca. 21.7 GB; echte VRAM-Nutzung kann höher liegen wegen Text Encoder, VAE, Auflösung und Workflow. ([Hugging Face][6])

Meine konkrete Empfehlung:
Installiere in ComfyUI ComfyUI-GGUF, lade Qwen-Image-2512-GGUF und starte mit Q4 oder Q5. Wenn es stabil läuft und du genug VRAM hast, auf Q6/Q8 wechseln. Für Edit/Compositing zusätzlich Qwen-Image-Edit-2511-GGUF.

[1]: https://huggingface.co/unsloth/Qwen-Image-2512-GGUF "unsloth/Qwen-Image-2512-GGUF · Hugging Face"
[2]: https://huggingface.co/unsloth/Qwen-Image-Edit-2511-GGUF "unsloth/Qwen-Image-Edit-2511-GGUF · Hugging Face"
[3]: https://huggingface.co/jayn7/Z-Image-Turbo-GGUF "jayn7/Z-Image-Turbo-GGUF · Hugging Face"
[4]: https://huggingface.co/orabazes/FLUX.2-dev-GGUF "orabazes/FLUX.2-dev-GGUF · Hugging Face"
[5]: https://artificialanalysis.ai/image/leaderboard/text-to-image "Text to Image Leaderboard - Top AI Image Models"
[6]: https://huggingface.


[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. 342 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.

## Neuer Baustein: Review Batch Renderer

Ich habe jetzt den direkt Telegram-tauglichen Batch-Renderer eingebaut:

```text
autoshorts/review/render.py
tests/test_review_render.py
```

## Was neu ist

### `render_review_batch_summary()`

Diese Funktion rendert einen kompletten `ReviewBatch` als einen Telegram-sicheren Textblock.

Er enthält:

- Batch-ID
- Summary Counts
  - `Ready`
  - `Blocked`
  - `Invalid`
- vollständige Review Packages für ready Candidates
- kompakte Fehlerlisten für blocked Candidates
- kompakte Fehlerlisten für invalid Candidates

Beispielstruktur:

```text
## Review Batch: batch-001
Ready: 1
Blocked: 1
Invalid: 1

## Ready for review
...

## Blocked
- blocked — Blocked title
  Reasons: quality gate blocked candidate

## Invalid
- invalid — Invalid title
  Reasons: hook is required
```

## Wichtiger Guardrail

Der Renderer vermeidet bewusst Sprache wie:

```text
publish
post
```

Warum? Weil Review und Publishing getrennte Phasen bleiben sollen.

Also kein versehentlicher semantischer Stolperdraht à la “approved-looking text = upload”. Das wäre genau die Sorte Automations-Dummheit, für die später Meetings erfunden werden.

## TDD eingehalten

Ablauf:

1. Tests für Batch Renderer geschrieben.
2. RED verifiziert:

```text
ModuleNotFoundError: No module named 'autoshorts.review.render'
```

3. Minimal implementiert.
4. Tests grün.
5. README aktualisiert.
6. Commit erstellt.

## Tests

Aktueller Stand:

```text
41 passed in 0.08s
```

## Neuer Commit

Branch:

```text
feat/core-approval-quality-gates
```

Commit:

```text
b7b5270 feat: add review batch renderer
```

Aktueller Verlauf:

```text
b7b5270 feat: add review batch renderer
2155728 feat: add review workflow buckets
deea24f feat: add candidate factory
ae6a4f6 feat: add content pillar registry
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit
```

## Pipeline jetzt

Wir haben jetzt die erste durchgängige interne Review-Strecke:

```text
Content Pillar
→ CandidateDraft
→ Candidate Factory
→ VideoCandidate
→ TestBatch
→ Review Workflow
   → ready_for_review
   → blocked
   → invalid
→ Review Batch Renderer
→ Telegram-safe Review Text
→ Approval Parser
```

## Nächster sinnvoller Schritt

Als Nächstes würde ich bauen:

```text
autoshorts/cli/demo_review_batch.py
```

oder besser als testbarer Core:

```text
autoshorts/demo/sample_batch.py
```

Ziel:

- sample AI-Life-Systems Batch erzeugen
- 1 ready Candidate
- 1 blocked Candidate
- 1 invalid Candidate
- ReviewBatch bauen
- Telegram-Text rendern

Dann können wir mit einem einzigen Command einen echten Review-Output lokal erzeugen und später direkt an Telegram hängen.


[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, mach weiter"

Konkreter noch unerfüllter nächster Schritt aus dem Verlauf: `autoshorts/content/demo_pipeline.py` und `tests/test_content_demo_pipeline.py` bauen für den deterministischen End-to-End-Flow:

```text
ContentBrief
→ ScriptDraft
→ VideoCandidate
→ TestBatch
→ ReviewBatch
→ Telegram Delivery Preview
```

z. B. als API:

```python
build_ai_life_systems_demo_delivery()
```

Hinweis aus letztem Assistentenstatus: „Need README patch.“ — Es wurde offenbar gerade mit diesem nächsten Schritt begonnen bzw. sollte README danach aktualisiert werden.

## Goal
Der Nutzer will ein kontrolliertes AutoShorts-System aufbauen, das Content-Ideen strukturiert erarbeitet, daraus reviewbare Video-Kandidaten erzeugt, Quality Gates/Human Approval erzwingt und später Telegram-Review sowie Video-Erstellung/Optimierung unterstützt — zunächst ohne echte externe Side Effects, ohne echtes Telegram-Senden und ohne unkontrolliertes Publishing.

Gesamtpipeline-Ziel:

```text
Content-Pillar
→ ContentBrief
→ ScriptDraft
→ CandidateDraft
→ VideoCandidate
→ TestBatch
→ ReviewBatch
→ Telegram Delivery Preview
→ später: ShotPlan / RenderBrief / Video-Erstellung testen und optimieren
```

## Constraints & Preferences
- Sprache/Arbeitsstil: Deutsch, direkte knappe Statusupdates, gelegentlich humorvoll/sarkastisch.
- Vorgehen: strikt TDD-orientiert:
  - zuerst RED-Test,
  - dann Implementierung,
  - dann GREEN,
  - dann Full Suite,
  - dann README,
  - dann Commit.
- Keine echten Side Effects in frühen Stufen:
  - keine Telegram API Calls,
  - kein echtes Senden,
  - kein Publishing,
  - keine externen Video-/AI-APIs.
- Human Approval bleibt mandatory.
- Safety/Guardrails:
  - Review/Delivery darf keine `publish`-/`post`-Sprache enthalten.
  - Telegram-Nachrichtenlimit: max. `4096` Zeichen.
  - Content-Validierung gegen generische AI-Slop-Phrasen.
  - No-Go/Policy Notes werden durch die Pipeline weitergereicht.
- Determinismus bevorzugt: Demo-Batches und Demo-Pipelines sollen reproduzierbar sein.
- Ziel ist erst robuste Content-Erarbeitung/Review-Struktur, dann Video-Erstellung, damit GPU-Zeit nicht an schlechte Prompts verschwendet wird.
- Niemals API Keys, Tokens, Passwörter, Secrets, Credentials oder Connection Strings in Zusammenfassungen übernehmen; falls vorhanden: `[REDACTED]`.

## Completed Actions
1. CREATE `autoshorts/demo/__init__.py` — Demo-Package für Sample Review Batch angelegt [tool: editor/terminal].
2. CREATE `autoshorts/demo/sample_batch.py` — deterministischen Demo-Batch implementiert mit `build_sample_ai_life_systems_batch()` und `render_sample_review_batch()` [tool: editor/terminal].
3. CREATE `tests/test_sample_batch.py` — Tests für Sample Review Demo angelegt; RED war `ModuleNotFoundError: No module named 'autoshorts.demo'`, GREEN danach `2 passed` [tool: pytest].
4. TEST Full Suite nach Sample Demo — Ergebnis: `43 passed in 0.09s` [tool: terminal].
5. UPDATE `README.md` — Dokumentation ergänzt für `autoshorts.demo.sample_batch`, Sample Review Demo und Testabdeckung [tool: editor].
6. COMMIT Sample Demo — Commit `30159ff feat: add sample review batch demo` auf Branch `feat/core-approval-quality-gates` [tool: git].
7. CREATE `autoshorts/cli/__init__.py` — CLI-Package angelegt [tool: editor/terminal].
8. CREATE `autoshorts/cli/demo_review_batch.py` — CLI Entry Point für `python -m autoshorts.cli.demo_review_batch` implementiert; druckt Sample Review Batch auf stdout [tool: editor/terminal].
9. CREATE `tests/test_demo_review_batch_cli.py` — Test für Review-Batch-CLI angelegt; RED war `ModuleNotFoundError: No module named 'autoshorts.cli'`, GREEN danach `1 passed` [tool: pytest].
10. TEST Full Suite nach Review-Batch-CLI — Ergebnis: `44 passed in 0.08s` [tool: terminal].
11. VERIFY CLI manuell — `python -m autoshorts.cli.demo_review_batch`; zusätzlich Output nach `/tmp/autoshorts-demo-review.txt`, Datei hatte `39` Zeilen; erster Output:
    ```text
    ## Review Batch: sample-ai-life-systems
    Ready: 1
    Blocked: 1
    Invalid: 1
    ```
    [tool: terminal].
12. UPDATE `README.md` — CLI-Ausführung und `autoshorts.cli.demo_review_batch` dokumentiert [tool: editor].
13. COMMIT Review-Batch-CLI — Commit `dc1c59c feat: add demo review batch cli` [tool: git].
14. CREATE `autoshorts/telegram/__init__.py` — Telegram-Package angelegt [tool: editor/terminal].
15. CREATE `autoshorts/telegram/review_sender.py` — side-effect-free Telegram Delivery Preparation implementiert mit `prepare_review_delivery(review_text: str, *, topic: str = "review") -> ReviewDelivery` [tool: editor/terminal].
16. CREATE `tests/test_telegram_review_sender.py` — Tests für Telegram Delivery Preparation angelegt; RED war `ModuleNotFoundError: No module named 'autoshorts.telegram'`, GREEN danach `4 passed` [tool: pytest].
17. IMPLEMENT Guardrails in `autoshorts/telegram/review_sender.py` — validiert nicht-leeren Text, max. `4096` Zeichen, keine `publish`-/`post`-Sprache, `requires_human_approval=True`, `side_effects=()` [tool: editor].
18. TEST Full Suite nach Telegram Delivery Preparation — Ergebnis: `48 passed in 0.09s` [tool: terminal].
19. VERIFY Telegram Delivery manuell — Sample Review Batch durch `prepare_review_delivery()` geschickt; Output:
    ```text
    telegram_review
    review
    True
    ()
    1000
    ```
    [tool: terminal].
20. UPDATE `README.md` — `autoshorts.telegram.review_sender`, Delivery Preparation, Telegram-Limit, keine Side Effects, Human Approval dokumentiert [tool: editor].
21. COMMIT Telegram Delivery Preparation — Commit `54863dd feat: add telegram review delivery preparation` [tool: git].
22. CREATE `autoshorts/cli/demo_telegram_delivery.py` — CLI für Delivery Preview implementiert; ausführbar via `python -m autoshorts.cli.demo_telegram_delivery`; Pipeline: Sample Review Batch → `prepare_review_delivery()` → Metadata + Review Text auf stdout [tool: editor/terminal].
23. CREATE `tests/test_demo_telegram_delivery_cli.py` — Test für Demo Telegram Delivery CLI angelegt; RED war `ModuleNotFoundError: No module named 'autoshorts.cli.demo_telegram_delivery'`, GREEN danach `1 passed` [tool: pytest].
24. TEST Full Suite nach Demo Telegram Delivery CLI — Ergebnis: `49 passed in 0.08s` [tool: terminal].
25. VERIFY Demo Telegram Delivery CLI manuell — Command:
    ```bash
    python -m autoshorts.cli.demo_telegram_delivery >/tmp/autoshorts-demo-delivery.txt
    ```
    Ergebnis:
    ```text
    45 /tmp/autoshorts-demo-delivery.txt
    Delivery type: telegram_review
    Topic: review
    Requires human approval: yes
    Side effects: none
    Text length: 1000
    ```
    [tool: terminal].
26. UPDATE `README.md` — `autoshorts.cli.demo_telegram_delivery`, Preview der vorbereiteten Telegram Delivery und „kein echtes Senden“ dokumentiert [tool: editor].
27. COMMIT Demo Telegram Delivery CLI — Commit `53b45ca feat: add demo telegram delivery cli` [tool: git].
28. CREATE `autoshorts/content/__init__.py` — Content-Package angelegt [tool: editor/terminal].
29. CREATE `autoshorts/content/brief.py` — `ContentBrief`-Core implementiert inkl. `create_content_brief()` und `validate_content_brief()` [tool: editor/terminal].
30. CREATE `tests/test_content_brief.py` — Tests für ContentBrief angelegt; RED war `ModuleNotFoundError: No module named 'autoshorts.content'`, GREEN danach `4 passed` [tool: pytest].
31. IMPLEMENT `create_content_brief()` — erzeugt strategischen Brief mit `brief_id`, Pillar, Topic, Audience Problem, Payoff, Positioning, Tone, Visual Rules, No-Go Topics, Zielplattformen und Ziel-Dauer [tool: editor].
32. VERIFY ContentBrief Beispiel — Input:
    ```python
    create_content_brief(
        pillar_id="ai_life_systems",
        topic="AI weekly review routine",
        audience_problem="I use AI tools but still lose control of priorities.",
        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
    )
    ```
    erzeugte verifizierte `brief_id`:
    ```text
    ai_life_systems-ai-weekly-review-routine
    ```
    [tool: terminal/pytest].
33. IMPLEMENT `validate_content_brief()` Guardrails — rejects fehlendes Topic, fehlendes Audience Problem, fehlenden Payoff, unsupported Platform, Duration außerhalb `10..180` [tool: editor].
34. CREATE `autoshorts/content/script_draft.py` — `ScriptDraft`-Core implementiert inkl. `create_script_draft()` und `validate_script_draft()` [tool: editor/terminal].
35. CREATE `tests/test_script_draft.py` — Tests für ScriptDraft angelegt; RED war `ModuleNotFoundError: No module named 'autoshorts.content.script_draft'`, GREEN danach `5 passed` [tool: pytest].
36. IMPLEMENT `create_script_draft(brief)` — erzeugt Title, Hook, Script Outline, 3–5 Retention Beats, Visual Concept, Caption, CTA, Estimated Duration [tool: editor].
37. VERIFY ScriptDraft Beispiel — Hook:
    ```text
    If AI runs your week, you have already lost control.
    ```
    Retention Beats: `4`; Validation: `True` [tool: terminal/pytest].
38. IMPLEMENT `validate_script_draft()` Guardrails — rejects fehlenden Hook, fehlendes Script Outline, fehlendes Visual Concept, fehlende Caption/CTA, weniger als 3 oder mehr als 5 Retention Beats, Duration außerhalb `10..180`, AI-Slop-Phrasen:
    ```text
    in this video
    unlock your full potential
    game changer
    revolutionize your life
    boost productivity
    ```
    [tool: editor].
39. TEST Full Suite nach ContentBrief + ScriptDraft — Ergebnis: `58 passed in 0.10s` [tool: terminal].
40. UPDATE `README.md` — `autoshorts.content.brief`, `autoshorts.content.script_draft`, Content Development Flow und Validation Guardrails dokumentiert [tool: editor].
41. COMMIT ContentBrief + ScriptDraft — Commit `9c125f9 feat: add content brief and script draft models` [tool: git].
42. CREATE `autoshorts/content/to_candidate.py` — Bridge von `ContentBrief + ScriptDraft` zu `CandidateDraft`/`VideoCandidate` implementiert [tool: editor/terminal].
43. CREATE `tests/test_content_to_candidate.py` — Tests für Content-to-Candidate-Bridge angelegt; RED war `ModuleNotFoundError: No module named 'autoshorts.content.to_candidate'`, GREEN danach `4 passed` [tool: pytest].
44. MODIFY `autoshorts/content/script_draft.py` — leere Overrides wie `hook=""` werden nun wirklich als leer validiert statt durch Default-Hook ersetzt [tool: editor].
45. IMPLEMENT `convert_script_draft_to_candidate_draft(brief, script)` — übernimmt Pillar, Title, Platforms, Duration, Hook, Script Outline, Visual Concept, Caption, CTA, Policy Notes aus Brief, Hypothesis und konservativen Initial Quality Score [tool: editor].
46. IMPLEMENT Initial Quality Score für CandidateDraft — Werte:
    ```text
    hook_strength: 8
    novelty: 8
    clarity: 8
    retention: 8
    ai_slop_risk: 3
    policy_risk: low
    ```
    [tool: editor].
47. IMPLEMENT `convert_script_draft_to_candidate(brief, script)` — nutzt bestehende Candidate Factory `create_candidate()` und erzeugt echten `VideoCandidate` mit stabiler Candidate ID, Default Hashtags, Duration Class, Pillar/Platform Validation und Candidate Validation [tool: editor].
48. VERIFY End-to-End-Pfad manuell — Pfad:
    ```text
    ContentBrief
    → ScriptDraft
    → VideoCandidate
    → build_candidate_review_item()
    ```
    Output:
    ```text
    ai_life_systems-ai-weekly-review-routine-35s
    ('#AI', '#LifeSystems', '#Productivity')
    8.0
    ready_for_review
    ```
    [tool: terminal].
49. IMPLEMENT Bridge Guardrails — rejects invalid ScriptDrafts, z. B. leerer Hook; rejects mismatched Brief/Script Paare; nutzt Candidate Factory/Pillar Validation für weitere Fehler [tool: editor].
50. ADD Policy Notes in Bridge — enthalten u. a.:
    ```text
    Source brief: ai_life_systems-ai-weekly-review-routine
    Human approval remains explicit
    No-go inherited: fake earnings
    ```
    [tool: editor].
51. TEST Full Suite nach Content-to-Candidate-Bridge — Ergebnis: `62 passed in 0.09s` [tool: terminal].
52. UPDATE `README.md` — `autoshorts.content.to_candidate`, ContentBrief + ScriptDraft → CandidateDraft und CandidateDraft → VideoCandidate via bestehender Factory dokumentiert [tool: editor].
53. COMMIT Content-to-Candidate-Bridge — Commit `72dc887 feat: convert content drafts to video candidates` [tool: git].

## Active State
- Arbeitsverzeichnis: AutoShorts-Projekt; genauer Pfad nicht im kompaktierten Verlauf enthalten.
- Branch:
  ```text
  feat/core-approval-quality-gates
  ```
- Letzter bekannter Commit:
  ```text
  72dc887 feat: convert content drafts to video candidates
  ```
- Bekannter Commit-Verlauf auf Branch:
  ```text
  72dc887 feat: convert content drafts to video candidates
  9c125f9 feat: add content brief and script draft models
  53b45ca feat: add demo telegram delivery cli
  54863dd feat: add telegram review delivery preparation
  dc1c59c feat: add demo review batch cli
  30159ff feat: add sample review batch demo
  b7b5270 feat: add review batch renderer
  2155728 feat: add review workflow buckets
  deea24f feat: add candidate factory
  ae6a4f6 feat: add content pillar registry
  ```
- Letzter bekannter Full-Suite-Status:
  ```text
  62 passed in 0.09s
  ```
- Keine laufenden Server oder Prozesse bekannt.
- Keine echten externen Integrationen aktiv.
- Keine Telegram API Calls.
- Keine Video-/AI-API Calls.
- Der letzte Assistenten-Zwischenstatus nach User „Ok, mach weiter“ war:
  ```text
  Need README patch.
  ```
  Das deutet darauf hin, dass der nächste Demo-Pipeline-Schritt begonnen werden sollte oder bereits Code teilweise erstellt wurde, README aber noch fehlt. Aktuellen Dateistatus per `git status` prüfen.

## In Progress
- Aufbau eines deterministischen Demo-End-to-End-Flows:
  ```text
  ContentBrief
  → ScriptDraft
  → VideoCandidate
  → TestBatch
  → ReviewBatch
  → Telegram Delivery Preview
  ```
- Geplante Dateien:
  ```text
  autoshorts/content/demo_pipeline.py
  tests/test_content_demo_pipeline.py
  ```
- Geplante API:
  ```python
  build_ai_life_systems_demo_delivery()
  ```
- README-Patch steht wahrscheinlich noch aus oder wurde gerade als nötig erkannt.

## Blocked
Keine bekannten Blocker.

Bekannte frühere RED-Fehler wurden alle behoben:
```text
ModuleNotFoundError: No module named 'autoshorts.demo'
ModuleNotFoundError: No module named 'autoshorts.cli'
ModuleNotFoundError: No module named 'autoshorts.telegram'
ModuleNotFoundError: No module named 'autoshorts.cli.demo_telegram_delivery'
ModuleNotFoundError: No module named 'autoshorts.content'
ModuleNotFoundError: No module named 'autoshorts.content.script_draft'
ModuleNotFoundError: No module named 'autoshorts.content.to_candidate'
```

## Key Decisions
- Review/Telegram-Delivery bleibt zunächst vollständig side-effect-free, damit keine versehentliche Veröffentlichung oder Telegram-Auslieferung passiert.
- Telegram Delivery Preparation erzeugt nur ein Delivery-Objekt mit Metadaten, `requires_human_approval=True` und `side_effects=()`.
- Telegram-Limit von `4096` Zeichen wird bereits im Core validiert.
- „publish“/„post“-Sprache ist in Review Deliveries verboten, um konzeptionell klar zwischen Review und Veröffentlichung zu trennen.
- Content-Erarbeitung wurde bewusst vor Video-Erstellung eingeführt:
  - Erst Brief und Script strukturiert validieren.
  - Dann Candidate erzeugen.
  - Dann Review.
  - Erst später ShotPlan/RenderBrief/Video-Generation.
- ContentBrief und ScriptDraft sind deterministisch modelliert, um Tests und Optimierung reproduzierbar zu machen.
- AI-Slop-Phrasen werden hart rejectet, um generische Shorts-Sprache früh zu vermeiden.
- `ScriptDraft` darf leere Overrides nicht stillschweigend durch Defaults reparieren; leere Eingaben müssen validierbar fehlschlagen.
- Candidate Bridge nutzt bestehende Candidate Factory, damit ID, Hashtags, Duration Class und Validierungen konsistent bleiben.
- Initialer Quality Score für aus Content erzeugte Kandidaten ist konservativ, aber reviewfähig:
  ```text
  hook_strength: 8
  novelty: 8
  clarity: 8
  retention: 8
  ai_slop_risk: 3
  policy_risk: low
  ```

## Resolved Questions
- Nutzer wollte nach Telegram Delivery Preview mit Content-Erarbeitung/Videos testen und optimieren beginnen.
  - Antwort/Umsetzung: Es wurde zuerst der kontrollierte Core für ContentBrief und ScriptDraft gebaut, noch ohne Video-Erzeugung oder externe APIs.
- Nutzer wollte „mach weiter“ nach ContentBrief/ScriptDraft.
  - Antwort/Umsetzung: Es wurde die Bridge `ContentBrief + ScriptDraft → CandidateDraft → VideoCandidate` gebaut.
- Nutzer wollte weiter nach der Bridge.
  - Geplanter nächster Schritt: Demo-Pipeline von Content-Erarbeitung bis Telegram Delivery Preview als ein deterministischer Pipeline-Call.

## Pending User Asks
- Noch nicht erfüllt: User sagte zuletzt „Ok, mach weiter“.
- Daraus folgt noch ausstehend:
  - `autoshorts/content/demo_pipeline.py` erstellen.
  - `tests/test_content_demo_pipeline.py` erstellen.
  - API wie `build_ai_life_systems_demo_delivery()` implementieren.
  - Pipeline testen:
    ```text
    ContentBrief
    → ScriptDraft
    → VideoCandidate
    → TestBatch
    → ReviewBatch
    → Telegram Delivery Preview
    ```
  - README aktualisieren.
  - Full Suite laufen lassen.
  - Commit erstellen.

## Relevant Files
- `autoshorts/demo/__init__.py` — Demo-Package.
- `autoshorts/demo/sample_batch.py` — deterministischer Sample Review Batch mit `build_sample_ai_life_systems_batch()` und `render_sample_review_batch()`.
- `tests/test_sample_batch.py` — Tests für Sample Review Demo.
- `autoshorts/cli/__init__.py` — CLI-Package.
- `autoshorts/cli/demo_review_batch.py` — CLI für Sample Review Batch.
- `tests/test_demo_review_batch_cli.py` — Tests für Review Batch CLI.
- `autoshorts/telegram/__init__.py` — Telegram-Package.
- `autoshorts/telegram/review_sender.py` — `ReviewDelivery` und `prepare_review_delivery()`.
- `tests/test_telegram_review_sender.py` — Tests für Telegram Delivery Preparation.
- `autoshorts/cli/demo_telegram_delivery.py` — CLI für Telegram Delivery Preview.
- `tests/test_demo_telegram_delivery_cli.py` — Tests für Telegram Delivery CLI.
- `autoshorts/content/__init__.py` — Content-Package.
- `autoshorts/content/brief.py` — `ContentBrief`, `create_content_brief()`, `validate_content_brief()`.
- `tests/test_content_brief.py` — Tests für ContentBrief.
- `autoshorts/content/script_draft.py` — `ScriptDraft`, `create_script_draft()`, `validate_script_draft()`, inkl. Fix für leere Overrides.
- `tests/test_script_draft.py` — Tests für ScriptDraft.
- `autoshorts/content/to_candidate.py` — `convert_script_draft_to_candidate_draft()` und `convert_script_draft_to_candidate()`.
- `tests/test_content_to_candidate.py` — Tests für Content-to-Candidate-Bridge.
- `README.md` — mehrfach aktualisiert für Demo, CLI, Telegram Delivery, Content Flow und Candidate Bridge.
- Geplant/noch zu prüfen:
  - `autoshorts/content/demo_pipeline.py`
  - `tests/test_content_demo_pipeline.py`

## Remaining Work
- Aktuellen Git-Status prüfen, da letzter Zwischenstatus „Need README patch.“ war.
- Falls Demo-Pipeline-Dateien noch nicht existieren:
  1. RED-Test für `autoshorts.content.demo_pipeline` schreiben.
  2. `build_ai_life_systems_demo_delivery()` implementieren.
  3. Pipeline sollte deterministisch:
     ```text
     create_content_brief()
     → create_script_draft()
     → convert_script_draft_to_candidate()
     → TestBatch
     → build_review_batch()
     → render_review_batch_summary()
     → prepare_review_delivery()
     ```
     ausführen.
  4. Tests für:
     - Delivery type `telegram_review`,
     - Topic `review`,
     - `requires_human_approval is True`,
     - `side_effects == ()`,
     - Review-Text enthält Batch-Header und Ready Count,
     - Candidate aus Content ist enthalten.
  5. README dokumentieren.
  6. Full Suite laufen lassen.
  7. Commit erstellen.
- Danach nächster größerer Block:
  ```text
  ScriptDraft
  → ShotPlan
  → RenderBrief
  → Visual/Video generation test
  ```
  um „Videos erstellen, testen und optimieren“ strukturiert anzugehen.

## Critical Context
- Sample Review Batch erzeugt exakt:
  ```text
  sample-ready   → ready for review
  sample-blocked → valid, aber Quality Gate blockiert
  sample-invalid → invalid, weil Hook fehlt
  ```
- Sample Review Output beginnt:
  ```text
  ## Review Batch: sample-ai-life-systems
  Ready: 1
  Blocked: 1
  Invalid: 1
  ```
- Demo Telegram Delivery CLI Output beginnt:
  ```text
  Delivery type: telegram_review
  Topic: review
  Requires human approval: yes
  Side effects: none
  Text length: 1000

  ## Review Batch: sample-ai-life-systems
  Ready: 1
  ```
- `prepare_review_delivery()` erzeugt:
  ```python
  ReviewDelivery(
      text=...,
      topic="review",
      delivery_type="telegram_review",
      requires_human_approval=True,
      side_effects=(),
  )
  ```
- Telegram Delivery Guardrails:
  - Text nicht leer.
  - Max. `4096` Zeichen.
  - Keine `publish`-/`post`-Sprache.
  - Keine Side Effects.
  - Human Approval mandatory.
- Beispiel-ContentBrief:
  ```python
  create_content_brief(
      pillar_id="ai_life_systems",
      topic="AI weekly review routine",
      audience_problem="I use AI tools but still lose control of priorities.",
      payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
  )
  ```
  erzeugt:
  ```text
  ai_life_systems-ai-weekly-review-routine
  ```
- Beispiel-ScriptDraft-Hook:
  ```text
  If AI runs your week, you have already lost control.
  ```
- Beispiel-ScriptDraft:
  - Retention Beats: `4`
  - Validation: `True`
- AI-Slop-Blacklist:
  ```text
  in this video
  unlock your full potential
  game changer
  revolutionize your life
  boost productivity
  ```
- Content-to-Candidate manuell verifizierter Output:
  ```text
  ai_life_systems-ai-weekly-review-routine-35s
  ('#AI', '#LifeSystems', '#Productivity')
  8.0
  ready_for_review
  ```
- Policy Notes aus Bridge enthalten:
  ```text
  Source brief: ai_life_systems-ai-weekly-review-routine
  Human approval remains explicit
  No-go inherited: fake earnings
  ```
- Letzter sicherer Teststand:
  ```text
  62 passed in 0.09s
  ```
- Keine Secrets/Credentials im Summary erhalten; falls im entfernten Verlauf vorhanden, wurden sie nicht übernommen und wären als `[REDACTED]` zu behandeln.

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


Erledigt, Sir.

## Neu eingebaut: Content Demo Review Pipeline

Neue Dateien:

```text
autoshorts/content/demo_pipeline.py
tests/test_content_demo_pipeline.py
```

## Neuer End-to-End-Demo-Call

```python
build_ai_life_systems_demo_delivery()
```

Der läuft deterministisch durch:

```text
ContentBrief
→ ScriptDraft
→ VideoCandidate
→ TestBatch
→ ReviewBatch
→ Telegram ReviewDelivery
```

Und gibt ein inspectable Result zurück:

```python
ContentDemoPipelineResult(
    brief=...,
    script=...,
    candidate=...,
    testbatch=...,
    review_batch=...,
    delivery=...,
)
```

Also nicht nur finalen Text, sondern jeden Zwischenzustand. Sehr nützlich zum Debuggen und Optimieren, ohne in Rendering- oder Telegram-Nebenwirkungen zu rutschen.

## Verifizierter Output

Manuell geprüft:

```text
ai_life_systems-ai-weekly-review-routine
ai_life_systems-ai-weekly-review-routine-35s
1 0 0
telegram_review review () 1294
```

Bedeutung:

- Brief ID: `ai_life_systems-ai-weekly-review-routine`
- Candidate ID: `ai_life_systems-ai-weekly-review-routine-35s`
- Review Buckets:
  - Ready: `1`
  - Blocked: `0`
  - Invalid: `0`
- Delivery:
  - Type: `telegram_review`
  - Topic: `review`
  - Side effects: `()`
  - Text length: `1294`

## Safety

Weiterhin:

- kein echtes Telegram-Senden
- kein Publishing
- keine externen API Calls
- keine generierten Medien
- keine Side Effects
- Human approval bleibt Pflicht

Eine Pipeline, die tatsächlich nichts Gefährliches tut. Man möchte fast misstrauisch werden.

## TDD eingehalten

RED:

```text
ModuleNotFoundError: No module named 'autoshorts.content.demo_pipeline'
```

GREEN:

```text
2 passed
```

Full suite:

```text
64 passed in 0.10s
```

## README aktualisiert

Dokumentiert:

- `autoshorts.content.demo_pipeline`
- End-to-End-Flow
- alle Zwischenartefakte
- keine send/publish side effects

## Commit

Branch:

```text
feat/core-approval-quality-gates
```

Neuer Commit:

```text
6e6569a feat: add content demo review pipeline
```

Aktueller Verlauf:

```text
6e6569a feat: add content demo review pipeline
72dc887 feat: convert content drafts to video candidates
9c125f9 feat: add content brief and script draft models
53b45ca feat: add demo telegram delivery cli
54863dd feat: add telegram review delivery preparation
dc1c59c feat: add demo review batch cli
30159ff feat: add sample review batch demo
b7b5270 feat: add review batch renderer
2155728 feat: add review workflow buckets
deea24f feat: add candidate factory
ae6a4f6 feat: add content pillar registry
```

## Nächster sinnvoller Schritt

Jetzt können wir mit dem eigentlichen Video-Erstellungs-Vorlauf beginnen:

```text
ScriptDraft
→ ShotPlan
→ RenderBrief
```

Ich würde als nächstes bauen:

```text
autoshorts/rendering/shot_plan.py
tests/test_shot_plan.py
```

Ziel:

- aus `ScriptDraft` einen 4–5 Scene ShotPlan erzeugen
- pro Scene:
  - Start/End time
  - Voiceover beat
  - Visual action
  - Motion type
  - Caption beat
  - Asset mode, z. B.:
    - `mechanism_animation`
    - `screen_recording_simulation`
    - `cinematic_ai_video`
    - `kinetic_text_overlay`
- harte Guardrails:
  - keine static slideshow
  - keine baked-in fake UI text
  - captions/labels renderer-owned
  - Szenenlängen plausibel
  - erster Frame hat Hook/Pattern Interrupt

Das ist der notwendige Zwischenschritt, bevor wir Qwen-Image/Wan/ComfyUI sinnvoll anwerfen. Sonst generieren wir hübschen Nebel mit Stromrechnung.


[M G] ok, mach das.


Erledigt, Sir.

## Neu eingebaut: Retention Shot Planning

Neue Dateien:

```text
autoshorts/rendering/__init__.py
autoshorts/rendering/shot_plan.py
tests/test_shot_plan.py
```

## Neue API

```python
create_shot_plan(script: ScriptDraft) -> ShotPlan
```

Erzeugt aus einem validierten `ScriptDraft` einen deterministischen 5-Scene ShotPlan:

```text
first_frame_hook
→ pattern_interrupt
→ core_explanation
→ screen_or_card_visual
→ cta
```

## ShotPlan-Struktur

```python
ShotPlan(
    script_id=...,
    title=...,
    estimated_duration_seconds=...,
    scenes=(...)
)
```

Jede Szene enthält:

```python
ShotScene(
    purpose=...,
    start_seconds=...,
    end_seconds=...,
    voiceover_beat=...,
    visual_action=...,
    asset_mode=...,
    caption_beat=...,
    retention_goal=...,
    labels_renderer_owned=True,
)
```

## Asset Modes

Aktueller Default-Plan:

```text
first_frame_hook        → kinetic_text_overlay
pattern_interrupt       → mechanism_animation
core_explanation        → screen_recording_simulation
screen_or_card_visual   → mechanism_animation
cta                     → kinetic_text_overlay
```

Damit vermeiden wir bewusst:

```text
static slideshow
```

Weil “AI slideshow mit Musik” nicht Content ist, sondern ein sehr langsamer Hilferuf.

## Guardrails

`validate_shot_plan()` rejected:

- `static_slideshow`
- baked-in fake UI text
- generated/unreadable UI text
- Labels/Captions, die nicht renderer-owned sind
- schlechte Scene Timings
- fehlende Pflichtfelder
- Plan ohne `first_frame_hook` als erste Szene
- ungültige Duration

## Verifizierter Output

Manuell geprüft:

```text
ai_life_systems-ai-weekly-review-routine
5
['first_frame_hook', 'pattern_interrupt', 'core_explanation', 'screen_or_card_visual', 'cta']
['kinetic_text_overlay', 'mechanism_animation', 'screen_recording_simulation', 'mechanism_animation', 'kinetic_text_overlay']
True
```

## TDD eingehalten

RED:

```text
ModuleNotFoundError: No module named 'autoshorts.rendering'
```

GREEN:

```text
5 passed
```

Full suite:

```text
69 passed in 0.10s
```

## README aktualisiert

Dokumentiert:

- `autoshorts.rendering.shot_plan`
- 5-Scene Retention Flow
- Scene-Felder
- Guardrails gegen Slideshow/Fake-UI/Baked Text

## Commit

Branch:

```text
feat/core-approval-quality-gates
```

Neuer Commit:

```text
d118a76 feat: add retention shot planning
```

Aktueller Verlauf:

```text
d118a76 feat: add retention shot planning
6e6569a feat: add content demo review pipeline
72dc887 feat: convert content drafts to video candidates
9c125f9 feat: add content brief and script draft models
53b45ca feat: add demo telegram delivery cli
54863dd feat: add telegram review delivery preparation
dc1c59c feat: add demo review batch cli
30159ff feat: add sample review batch demo
b7b5270 feat: add review batch renderer
2155728 feat: add review workflow buckets
deea24f feat: add candidate factory
ae6a4f6 feat: add content pillar registry
```

## Nächster sinnvoller Schritt

Jetzt kommt der nächste wichtige Zwischenlayer:

```text
ShotPlan
→ RenderManifest
```

Ich würde bauen:

```text
autoshorts/rendering/manifest.py
tests/test_render_manifest.py
```

Ziel:

- ShotPlan in maschinenlesbaren RenderManifest übersetzen
- pro Timeline Item:
  - start/end
  - asset mode
  - motion instruction
  - caption layer
  - renderer-owned label layer
  - placeholder asset path
- globale Render Settings:
  - vertical `1080x1920`
  - fps `30`
  - no generated text inside assets
  - captions renderer-owned
  - static_card_limit = `0`

Danach können wir erst lokal einen Preview-Renderer bauen — und danach Qwen-Image/Wan/ComfyUI gezielt anbinden, statt blind GPU-Roulette zu spielen.


Explanation:
**Considering coding actions**

I need to take action now! I want to load the relevant skill, which seems to be related to software development—maybe something about test-driven development (TDD). The user is asking me to continue coding, so I need to inspect what’s at hand carefully. It’s important to make sure I’m proceeding correctly to meet their needs. Let’s keep this efficient and effective!


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")
```

### CLI Entry Points

For new `python -m package.cli.command` style entry points, keep the TDD loop boring and side-effect free:

1. Write a test that imports `main` from the missing CLI module and calls `main([])`.
2. Capture stdout/stderr with the project test framework (`capsys` in pytest) or inject a `stdout` file-like object if supported.
3. RED should usually be an import failure for the missing CLI module/package, not a vague end-to-end shell failure.
4. Implement `main(argv: Sequence[str] | None = None, *, stdout: TextIO | None = None) -> int` so tests can call it without spawning a subprocess.
5. Add the `if __name__ == "__main__": raise SystemExit(main())` guard only after the callable path is green.
6. Verify both the unit test and the real module invocation, e.g. `python -m package.cli.command`, before committing.

This catches command wiring while avoiding accidental network calls, publishing actions, or other theatrical nonsense masquerading as a CLI test.

### 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.

## Demo / Fixture Features

When adding a deterministic demo fixture for later integration wiring, still use RED-GREEN-REFACTOR. Write the wished-for import/API in the test first, verify the missing module/API failure, then implement the smallest fixture that proves the real workflow path. Assert stable IDs/order, bucket counts/classifications, and safety invariants such as review text not implying publication. See `references/deterministic-demo-fixtures.md` for the concise pattern.

## Side-Effect-Free Integration Boundaries

When building toward an external integration (Telegram, email, publishing APIs, render farms, payment flows, etc.), add a tested preparation boundary before any real I/O:

1. Write the wished-for pure function in the test first, e.g. `prepare_review_delivery(text) -> DeliveryPlan`.
2. RED should be a missing module/API or missing validation failure, not a real network/send failure.
3. The GREEN implementation should return a typed/dataclass plan object with explicit metadata: destination/topic, delivery type, human-approval requirement, and `side_effects=()`.
4. Validate safety constraints at the boundary: non-empty content, platform size limits, no forbidden action language, no implicit approval, no credentials or generated media unless explicitly expected.
5. Add a demo CLI only after the pure boundary is green. The CLI should print metadata first, then payload, and still perform no external I/O.
6. Verify both the unit tests and a real `python -m package.cli.demo_*` invocation before committing.

This pattern lets later sending/rendering/publishing adapters consume a boring, audited object instead of smuggling side effects into formatting code. Boring is how we avoid building a content catapult with a moustache.

## 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.
- **Domain classes named `Test*` trigger pytest collection warnings** — if a production dataclass/model must be named like `TestBatch`, set `__test__ = False` on the class or choose a non-`Test` prefix. Keep the fix in production code when the name is part of the domain language, and verify the warning is gone before committing.
- **Defaulting with `x or default` hides invalid explicit inputs** — when a factory/test helper accepts optional overrides, use `x if x is not None else default` so tests can pass intentionally blank strings (`""`) or zero values and watch validation reject them. Otherwise invalid-input tests may silently receive defaults and give false confidence.

## Final Rule

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

No exceptions without the user's explicit permission.

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


Explanation:
**Searching for repository**

I need to locate a repository. The current working directory (pwd) is hermes-agent, but the project could be located elsewhere. I think it’s a good idea to start searching for it now to avoid wasting time. It might take a bit of digging, but I’m determined to find it. Let's explore different locations or search patterns to see if I can track it down!

terminal

git status --short && pwd && git branch --show-current


Output: fatal: Kein Git-Repository (oder irgendeines der Elternverzeichnisse): .git


Explanation:
**Searching for files**

I need to find something specific. Should I be using the search_files function? It seems like that would be the most straightforward way to get what I need. I’m thinking about how to clarify what I'm looking for so I can use it effectively. It’s a bit overwhelming, but I'll figure it out! I just need to make sure I'm being precise in my search query to get the best results.


Explanation:
**Considering file search options**

I’m wondering if I should check the repository in the current working directory, but that doesn’t seem right. Should I search the README instead? Perhaps using the terminal's find command could help, but it says not to use that. I might need to use search_files with a pattern like *AutoShorts*. It’s a bit confusing, but I think I’ll settle on refining my search strategy for the files I need!


Explanation:
**Reflecting on tasks**

I need to focus on my work, particularly on Test-Driven Development (TDD). I should take the time to inspect the relevant files to ensure everything is aligned with my goals. This step is crucial to maintain quality and effectiveness in the project. I’m curious to see if there might be any improvements I can make. It feels good to have a clear direction, and I’m ready to dive in!

git status --short && git branch --show-current


Output: feat/core-approval-quality-gates


     1|"""Retention-led shot planning before any video rendering."""
     2|
     3|from __future__ import annotations
     4|
     5|from dataclasses import dataclass
     6|from typing import Literal
     7|
     8|from autoshorts.content.script_draft import ScriptDraft, validate_script_draft
     9|from autoshorts.ideas.candidate import ValidationResult
    10|
    11|ShotPurpose = Literal[
    12|    "first_frame_hook",
    13|    "pattern_interrupt",
    14|    "core_explanation",
    15|    "screen_or_card_visual",
    16|    "cta",
    17|]
    18|AssetMode = Literal[
    19|    "kinetic_text_overlay",
    20|    "mechanism_animation",
    21|    "screen_recording_simulation",
    22|    "cinematic_ai_video",
    23|    "static_slideshow",
    24|]
    25|
    26|_ALLOWED_ASSET_MODES = {
    27|    "kinetic_text_overlay",
    28|    "mechanism_animation",
    29|    "screen_recording_simulation",
    30|    "cinematic_ai_video",
    31|}
    32|_BAKED_TEXT_MARKERS = ("baked-in", "fake ui text", "generated text", "unreadable ui")
    33|
    34|
    35|@dataclass(frozen=True)
    36|class ShotScene:
    37|    purpose: ShotPurpose | str
    38|    start_seconds: int
    39|    end_seconds: int
    40|    voiceover_beat: str
    41|    visual_action: str
    42|    asset_mode: AssetMode | str
    43|    caption_beat: str
    44|    retention_goal: str
    45|    labels_renderer_owned: bool = True
    46|
    47|
    48|@dataclass(frozen=True)
    49|class ShotPlan:
    50|    script_id: str
    51|    title: str
    52|    estimated_duration_seconds: int
    53|    scenes: tuple[ShotScene, ...]
    54|
    55|
    56|def _is_blank(value: str) -> bool:
    57|    return not (value or "").strip()
    58|
    59|
    60|def _scene_windows(duration_seconds: int) -> tuple[tuple[int, int], ...]:
    61|    first_end = min(3, duration_seconds)
    62|    second_end = min(8, duration_seconds)
    63|    third_end = min(17, duration_seconds)
    64|    fourth_end = min(max(duration_seconds - 5, third_end + 1), duration_seconds)
    65|    return (
    66|        (0, first_end),
    67|        (first_end, second_end),
    68|        (second_end, third_end),
    69|        (third_end, fourth_end),
    70|        (fourth_end, duration_seconds),
    71|    )
    72|
    73|
    74|def create_shot_plan(script: ScriptDraft) -> ShotPlan:
    75|    """Create a deterministic five-scene retention shot plan from a script draft."""
    76|
    77|    validation = validate_script_draft(script)
    78|    if not validation.is_valid:
    79|        raise ValueError("; ".join(validation.errors))
    80|
    81|    windows = _scene_windows(script.estimated_duration_seconds)
    82|    beats = script.retention_beats
    83|    scenes = (
    84|        ShotScene(
    85|            purpose="first_frame_hook",
    86|            start_seconds=windows[0][0],
    87|            end_seconds=windows[0][1],
    88|            voiceover_beat=script.hook,
    89|            visual_action="Immediate motion punch-in on clean kinetic hook text; no generated UI lettering.",
    90|            asset_mode="kinetic_text_overlay",
    91|            caption_beat=script.hook,
    92|            retention_goal="Stop the scroll with a first-frame hook and pattern interrupt.",
    93|        ),
    94|        ShotScene(
    95|            purpose="pattern_interrupt",
    96|            start_seconds=windows[1][0],
    97|            end_seconds=windows[1][1],
    98|            voiceover_beat=beats[0],
    99|            visual_action="Rapid desk-to-phone change showing messy priorities becoming a controlled workflow.",
   100|            asset_mode="mechanism_animation",
   101|            caption_beat="AI suggests. Human approves.",
   102|            retention_goal="Create visible change before the viewer can classify it as a slideshow.",
   103|        ),
   104|        ShotScene(
   105|            purpose="core_explanation",
   106|            start_seconds=windows[2][0],
   107|            end_seconds=windows[2][1],
   108|            voiceover_beat=beats[1] if len(beats) > 1 else script.script_outline,
   109|            visual_action="Simulated screen flow with renderer-owned boxes, cursor motion, and approval checkpoint.",
   110|            asset_mode="screen_recording_simulation",
   111|            caption_beat="The rule: AI drafts, you decide.",
   112|            retention_goal="Make the mechanism concrete without trusting generated text inside assets.",
   113|        ),
   114|        ShotScene(
   115|            purpose="screen_or_card_visual",
   116|            start_seconds=windows[3][0],
   117|            end_seconds=windows[3][1],
   118|            voiceover_beat=beats[2] if len(beats) > 2 else script.script_outline,
   119|            visual_action="Animated checklist/card sequence showing the weekly review routine step by step.",
   120|            asset_mode="mechanism_animation",
   121|            caption_beat="10-minute review. One human checkpoint.",
   122|            retention_goal="Deliver the payoff visually before the CTA.",
   123|        ),
   124|        ShotScene(
   125|            purpose="cta",
   126|            start_seconds=windows[4][0],
   127|            end_seconds=windows[4][1],
   128|            voiceover_beat=script.cta,
   129|            visual_action="Clean kinetic CTA overlay with renderer-owned caption and final checklist card motion.",
   130|            asset_mode="kinetic_text_overlay",
   131|            caption_beat=script.cta,
   132|            retention_goal="Give a concrete comment action tied to the promised checklist.",
   133|        ),
   134|    )
   135|    return ShotPlan(
   136|        script_id=script.brief_id,
   137|        title=script.title,
   138|        estimated_duration_seconds=script.estimated_duration_seconds,
   139|        scenes=scenes,
   140|    )
   141|
   142|
   143|def validate_shot_plan(plan: ShotPlan) -> ValidationResult:
   144|    errors: list[str] = []
   145|    if _is_blank(plan.script_id):
   146|        errors.append("script_id is required")
   147|    if _is_blank(plan.title):
   148|        errors.append("title is required")
   149|    if not 10 <= plan.estimated_duration_seconds <= 180:
   150|        errors.append("duration_seconds must be between 10 and 180")
   151|    if not plan.scenes:
   152|        errors.append("at least one scene is required")
   153|    elif plan.scenes[0].purpose != "first_frame_hook":
   154|        errors.append("first scene must be first_frame_hook")
   155|
   156|    previous_end: int | None = None
   157|    for index, scene in enumerate(plan.scenes, start=1):
   158|        if scene.end_seconds <= scene.start_seconds:
   159|            errors.append(f"scene {index} end_seconds must be greater than start_seconds")
   160|        if previous_end is not None and scene.start_seconds < previous_end:
   161|            errors.append(f"scene {index} start_seconds overlaps previous scene")
   162|        previous_end = scene.end_seconds
   163|
   164|        for field_name, value in {
   165|            "purpose": scene.purpose,
   166|            "voiceover_beat": scene.voiceover_beat,
   167|            "visual_action": scene.visual_action,
   168|            "asset_mode": scene.asset_mode,
   169|            "caption_beat": scene.caption_beat,
   170|            "retention_goal": scene.retention_goal,
   171|        }.items():
   172|            if _is_blank(value):
   173|                errors.append(f"scene {index} {field_name} is required")
   174|
   175|        if scene.asset_mode == "static_slideshow":
   176|            errors.append("static slideshow asset mode is not allowed")
   177|        elif scene.asset_mode not in _ALLOWED_ASSET_MODES:
   178|            errors.append(f"unsupported asset mode: {scene.asset_mode}")
   179|        if not scene.labels_renderer_owned:
   180|            errors.append("labels and captions must be renderer-owned")
   181|        visual = scene.visual_action.casefold()
   182|        if any(marker in visual for marker in _BAKED_TEXT_MARKERS):
   183|            errors.append("baked-in fake UI text is not allowed")
   184|
   185|    return ValidationResult(is_valid=not errors, errors=tuple(errors))
   186|


Explanation:
**Considering implementation details**

I need to implement the manifest. It’s crucial to inspect the test patterns for TestBatch and ReviewBatch to make sure everything is working correctly. I should check the README as well; it often contains valuable information that can guide me in this process. There’s a lot to juggle here, but I’m focusing on making sure I cover all the necessary parts effectively. Let's get started!


     1|import pytest
     2|
     3|from autoshorts.content.brief import create_content_brief
     4|from autoshorts.content.script_draft import create_script_draft
     5|from autoshorts.rendering.shot_plan import (
     6|    ShotPlan,
     7|    ShotScene,
     8|    create_shot_plan,
     9|    validate_shot_plan,
    10|)
    11|
    12|
    13|def _script():
    14|    brief = create_content_brief(
    15|        pillar_id="ai_life_systems",
    16|        topic="AI weekly review routine",
    17|        audience_problem="I use AI tools but still lose control of priorities.",
    18|        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
    19|    )
    20|    return create_script_draft(brief)
    21|
    22|
    23|def test_create_shot_plan_builds_five_retention_scenes_from_script_draft():
    24|    script = _script()
    25|
    26|    plan = create_shot_plan(script)
    27|
    28|    assert isinstance(plan, ShotPlan)
    29|    assert plan.script_id == "ai_life_systems-ai-weekly-review-routine"
    30|    assert plan.title == "AI weekly review routine"
    31|    assert plan.estimated_duration_seconds == 35
    32|    assert [scene.purpose for scene in plan.scenes] == [
    33|        "first_frame_hook",
    34|        "pattern_interrupt",
    35|        "core_explanation",
    36|        "screen_or_card_visual",
    37|        "cta",
    38|    ]
    39|    assert [scene.asset_mode for scene in plan.scenes] == [
    40|        "kinetic_text_overlay",
    41|        "mechanism_animation",
    42|        "screen_recording_simulation",
    43|        "mechanism_animation",
    44|        "kinetic_text_overlay",
    45|    ]
    46|    assert plan.scenes[0].start_seconds == 0
    47|    assert plan.scenes[-1].end_seconds == 35
    48|    assert plan.scenes[0].caption_beat == script.hook
    49|    assert "pattern interrupt" in plan.scenes[0].retention_goal.casefold()
    50|    assert all(scene.labels_renderer_owned for scene in plan.scenes)
    51|    assert all("fake UI text" not in scene.visual_action for scene in plan.scenes)
    52|
    53|
    54|def test_validate_shot_plan_accepts_default_plan():
    55|    result = validate_shot_plan(create_shot_plan(_script()))
    56|
    57|    assert result.is_valid is True
    58|    assert result.errors == ()
    59|
    60|
    61|def test_validate_shot_plan_rejects_static_slideshow_and_baked_text():
    62|    scene = ShotScene(
    63|        purpose="first_frame_hook",
    64|        start_seconds=0,
    65|        end_seconds=3,
    66|        voiceover_beat="Hook",
    67|        visual_action="Static slide with baked-in fake UI text",
    68|        asset_mode="static_slideshow",
    69|        caption_beat="Hook",
    70|        retention_goal="Stop scroll",
    71|        labels_renderer_owned=False,
    72|    )
    73|    plan = ShotPlan(
    74|        script_id="script",
    75|        title="Title",
    76|        estimated_duration_seconds=3,
    77|        scenes=(scene,),
    78|    )
    79|
    80|    result = validate_shot_plan(plan)
    81|
    82|    assert result.is_valid is False
    83|    assert "static slideshow asset mode is not allowed" in result.errors
    84|    assert "labels and captions must be renderer-owned" in result.errors
    85|    assert "baked-in fake UI text is not allowed" in result.errors
    86|
    87|
    88|def test_validate_shot_plan_rejects_bad_timing_and_missing_first_frame_hook():
    89|    scene = ShotScene(
    90|        purpose="pattern_interrupt",
    91|        start_seconds=2,
    92|        end_seconds=1,
    93|        voiceover_beat="Beat",
    94|        visual_action="Quick motion cut",
    95|        asset_mode="mechanism_animation",
    96|        caption_beat="Beat",
    97|        retention_goal="Interrupt",
    98|    )
    99|    plan = ShotPlan(
   100|        script_id="script",
   101|        title="Title",
   102|        estimated_duration_seconds=35,
   103|        scenes=(scene,),
   104|    )
   105|
   106|    result = validate_shot_plan(plan)
   107|
   108|    assert result.is_valid is False
   109|    assert "first scene must be first_frame_hook" in result.errors
   110|    assert "scene 1 end_seconds must be greater than start_seconds" in result.errors
   111|
   112|
   113|def test_create_shot_plan_rejects_invalid_script_draft():
   114|    script = create_script_draft(_script_to_brief(), hook="")
   115|
   116|    with pytest.raises(ValueError, match="hook is required"):
   117|        create_shot_plan(script)
   118|
   119|
   120|def _script_to_brief():
   121|    return create_content_brief(
   122|        pillar_id="ai_life_systems",
   123|        topic="AI weekly review routine",
   124|        audience_problem="I use AI tools but still lose control of priorities.",
   125|        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
   126|    )
   127|


     1|# AutoShortsBot
     2|
     3|AutoShortsBot is a semi-automatic short-form content engine for high-quality YouTube Shorts, TikTok, and Instagram Reels.
     4|
     5|The project is deliberately approval-first: it can draft, score, render previews, and prepare publishing metadata, but it must not publish anything without explicit human approval.
     6|
     7|## Current scope
     8|
     9|Implemented foundation:
    10|
    11|- `autoshorts.cli.demo_review_batch` — local CLI entry point that prints the deterministic review demo.
    12|- `autoshorts.cli.demo_telegram_delivery` — local CLI entry point that prints side-effect-free Telegram delivery metadata plus review text.
    13|- `autoshorts.config.pillars` — content pillar registry, default AI/life-systems positioning, and pillar fit validation.
    14|- `autoshorts.content.brief` — strategy brief model for retention-led content development before scripting.
    15|- `autoshorts.content.demo_pipeline` — deterministic content-to-Telegram-review demo pipeline.
    16|- `autoshorts.content.script_draft` — structured script drafts with retention beats, visual concept, caption, CTA, and AI-slop validation.
    17|- `autoshorts.content.to_candidate` — converts content briefs and script drafts into candidate drafts and validated video candidates.
    18|- `autoshorts.demo.sample_batch` — deterministic AI-life-systems review demo with ready, blocked, and invalid candidates.
    19|- `autoshorts.ideas.candidate` — structured video candidate model and validation.
    20|- `autoshorts.ideas.factory` — draft-to-candidate factory with stable ids, default hashtags, and duration classes.
    21|- `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
    22|- `autoshorts.review.approvals` — deterministic Telegram approval command parser.
    23|- `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.
    24|- `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
    25|- `autoshorts.review.render` — renders complete review batches into Telegram-safe text.
    26|- `autoshorts.review.workflow` — splits candidates into ready, blocked, and invalid review buckets.
    27|- `autoshorts.telegram.review_sender` — validates Telegram review text and prepares side-effect-free delivery plans.
    28|- `autoshorts.scripting.quality` — conservative pre-render quality gate.
    29|- Tests for content briefs, content demo pipeline, content-to-candidate conversion, content pillars, candidate factory, idea candidates, script drafts, shot plans, testbatches, sample review CLI, sample Telegram delivery CLI, sample review demos, review workflow, review-batch rendering, approval parsing, review formatting, Telegram delivery preparation, and script quality gating.
    30|
    31|## Content development
    32|
    33|`autoshorts.content.brief.create_content_brief()` turns a pillar, topic, audience problem, and payoff into a deterministic `ContentBrief`. The brief carries positioning, tone, visual rules, no-go topics, target platforms, and target duration before any script or renderer exists.
    34|
    35|`validate_content_brief()` rejects missing strategy fields, unsupported platforms, and durations outside `10..180` seconds.
    36|
    37|`autoshorts.content.script_draft.create_script_draft()` turns a `ContentBrief` into a structured `ScriptDraft` with:
    38|
    39|- exact hook;
    40|- script outline;
    41|- 3–5 retention beats;
    42|- visual concept;
    43|- caption and CTA;
    44|- estimated duration.
    45|
    46|`validate_script_draft()` rejects missing core fields, invalid beat counts, invalid durations, and generic AI-slop phrases such as `in this video` or `unlock your full potential`.
    47|
    48|`autoshorts.content.to_candidate.convert_script_draft_to_candidate_draft()` turns a validated `ContentBrief` + `ScriptDraft` pair into the existing `CandidateDraft` type. It preserves platform strategy, hook, script outline, visual concept, caption, CTA, no-go policy notes, and an initial conservative quality score.
    49|
    50|`convert_script_draft_to_candidate()` then uses the existing candidate factory to create a validated `VideoCandidate`, including stable candidate id generation, default hashtags, duration classification, and pillar/platform validation.
    51|
    52|`autoshorts.content.demo_pipeline.build_ai_life_systems_demo_delivery()` runs the deterministic demo path end-to-end:
    53|
    54|```text
    55|ContentBrief
    56|→ ScriptDraft
    57|→ VideoCandidate
    58|→ TestBatch
    59|→ ReviewBatch
    60|→ Telegram ReviewDelivery
    61|```
    62|
    63|The result object exposes every intermediate artifact for inspection and still has no send/publish side effects.
    64|
    65|## Render planning
    66|
    67|`autoshorts.rendering.shot_plan.create_shot_plan()` turns a validated `ScriptDraft` into a deterministic five-scene retention plan:
    68|
    69|```text
    70|first_frame_hook
    71|→ pattern_interrupt
    72|→ core_explanation
    73|→ screen_or_card_visual
    74|→ cta
    75|```
    76|
    77|Each `ShotScene` carries timing, voiceover beat, visual action, asset mode, caption beat, retention goal, and whether labels/captions remain renderer-owned.
    78|
    79|`validate_shot_plan()` rejects static slideshow asset modes, baked-in fake UI text, non-renderer-owned labels/captions, bad timing, missing fields, and plans whose first scene is not the first-frame hook.
    80|
    81|## Human approval commands
    82|
    83|Supported review responses:
    84|
    85|- `Freigabe alle` — approve for YouTube, TikTok, and Instagram.
    86|- `Freigabe YouTube`
    87|- `Freigabe TikTok`
    88|- `Freigabe Instagram`
    89|- `Ablehnen`
    90|- `Ändern: <requested change>`
    91|
    92|Ambiguous messages such as `passt schon` intentionally return `needs_clarification` and must not trigger posting.
    93|
    94|## Quality gate
    95|
    96|A script candidate only proceeds to rendering when:
    97|
    98|- average of hook strength, novelty, clarity, and retention is at least `8.0`;
    99|- AI-slop risk is not above `5`;
   100|- policy risk is not `high`.
   101|
   102|This keeps rendering capacity for candidates that might actually deserve to exist. A rare mercy.
   103|
   104|## Review package
   105|
   106|`autoshorts.review.package.render_telegram_review_package()` produces the human approval message for one candidate. It includes:
   107|
   108|- candidate id, title, pillar, target platforms, and duration;
   109|- hook, script summary, visual concept, caption, and hashtags;
   110|- average quality score, AI-slop risk, policy notes, and render-gate status;
   111|- optional preview path;
   112|- only the relevant platform-specific approval commands.
   113|
   114|A blocked quality gate is rendered as `Render gate: BLOCKED — nicht automatisch rendern`.
   115|
   116|## Review workflow
   117|
   118|`build_candidate_review_item()` classifies one candidate as:
   119|
   120|- `ready_for_review` — valid and renderable; includes a review package;
   121|- `blocked` — valid but blocked by quality gate; still includes a review package with blocked gate status;
   122|- `invalid` — fails candidate validation; does not create a review package.
   123|
   124|`build_review_batch()` processes a `TestBatch` into `ready_for_review`, `blocked`, and `invalid` buckets while preserving input order inside each bucket. Optional preview paths are attached by candidate id.
   125|
   126|`render_review_batch_summary()` turns the complete `ReviewBatch` into one Telegram-safe text block with summary counts, full ready-for-review packages, and compact blocked/invalid reason lists. It deliberately avoids publish/post language; approval and publishing remain separate later stages.
   127|
   128|## Sample review demo
   129|
   130|`autoshorts.demo.sample_batch.build_sample_ai_life_systems_batch()` creates a deterministic AI-life-systems sample with:
   131|
   132|- one ready candidate;
   133|- one blocked candidate that fails the quality gate;
   134|- one invalid candidate missing its hook.
   135|
   136|`render_sample_review_batch()` builds that sample, runs it through the review workflow, and returns the Telegram-safe batch summary. This gives the later Telegram sender one boring, repeatable fixture to wire against.
   137|
   138|Run the same fixture locally with:
   139|
   140|```bash
   141|python -m autoshorts.cli.demo_review_batch
   142|```
   143|
   144|## Telegram review delivery preparation
   145|
   146|`autoshorts.telegram.review_sender.prepare_review_delivery()` wraps rendered review text in a side-effect-free `ReviewDelivery` object. It does not call Telegram. It validates that:
   147|
   148|- review text is not blank;
   149|- text fits Telegram's 4096-character message limit;
   150|- review deliveries do not contain publish/post language;
   151|- the delivery still requires human approval.
   152|
   153|Actual Telegram I/O remains a later explicit boundary.
   154|
   155|Preview the prepared delivery locally with:
   156|
   157|```bash
   158|python -m autoshorts.cli.demo_telegram_delivery
   159|```
   160|
   161|It prints delivery metadata first, then the review text. It still sends nothing.
   162|
   163|## Idea candidates and testbatches
   164|
   165|`VideoCandidate` is the structured unit that moves through the early pipeline. It carries:
   166|
   167|- candidate id, pillar, title, target platforms, duration, and hypothesis;
   168|- hook, script outline, visual concept, caption, CTA, and hashtags;
   169|- quality score and policy notes.
   170|
   171|`validate_candidate()` rejects missing strategic fields, unsupported platforms, and durations outside `10..180` seconds.
   172|
   173|`TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.
   174|
   175|## Candidate factory
   176|
   177|`CandidateDraft` represents a raw idea before it becomes a validated `VideoCandidate`.
   178|
   179|`create_candidate()` turns a draft into a candidate by:
   180|
   181|- generating a stable id from pillar, title, and duration;
   182|- adding default pillar hashtags when no hashtags are supplied;
   183|- classifying duration as `growth_short`, `monetization_candidate`, or `experiment`;
   184|- appending the duration class to policy notes;
   185|- enforcing candidate validation and pillar/platform fit validation.
   186|
   187|Duration classes:
   188|
   189|- `growth_short`: 20–45 seconds;
   190|- `monetization_candidate`: 60–75 seconds;
   191|- `experiment`: all other valid durations.
   192|
   193|Missing hypothesis, unknown pillar, or unfit platform raises `ValueError`.
   194|
   195|## Content pillars
   196|
   197|`ContentPillar` keeps strategy separate from rendering. The default registry currently contains:
   198|
   199|- `ai_life_systems` — AI as a servant for ordinary life systems, never as values or authority.
   200|
   201|A pillar defines audience, positioning, tone, hook patterns, visual rules, no-go topics, platform fit, and monetization angle.
   202|
   203|`validate_pillar_registry()` rejects duplicate pillar ids and incomplete pillar definitions. `validate_candidate_against_pillars()` rejects candidates with unknown pillars or platforms that are not configured for that pillar.
   204|
   205|## Development
   206|
   207|Run tests:
   208|
   209|```bash
   210|python -m pytest tests -q
   211|```
   212|
   213|## Planned architecture
   214|
   215|```text
   216|autoshorts/
   217|  config/       # niches, platforms, workflow settings
   218|  ideas/        # idea generation and scoring
   219|  scripting/    # script drafts, quality gates, retention checks
   220|  rendering/    # local/AI-assisted preview rendering
   221|  review/       # Telegram review package and approval parser
   222|  publishing/   # platform-specific publishing metadata and later APIs
   223|  analytics/    # performance imports and feedback loop
   224|```
   225|
   226|## Non-negotiables
   227|
   228|- No credentials in git.
   229|- No generated media in git unless explicitly intended.
   230|- No automatic publishing without explicit approval.
   231|- Quality over speed; avoid generic AI-slop.
   232|


     1|"""Deterministic content-to-review demo pipeline."""
     2|
     3|from __future__ import annotations
     4|
     5|from dataclasses import dataclass
     6|
     7|from autoshorts.content.brief import ContentBrief, create_content_brief
     8|from autoshorts.content.script_draft import ScriptDraft, create_script_draft
     9|from autoshorts.content.to_candidate import convert_script_draft_to_candidate
    10|from autoshorts.ideas.candidate import VideoCandidate
    11|from autoshorts.ideas.testbatch import TestBatch
    12|from autoshorts.review.render import render_review_batch_summary
    13|from autoshorts.review.workflow import ReviewBatch, build_review_batch
    14|from autoshorts.telegram.review_sender import ReviewDelivery, prepare_review_delivery
    15|
    16|
    17|@dataclass(frozen=True)
    18|class ContentDemoPipelineResult:
    19|    brief: ContentBrief
    20|    script: ScriptDraft
    21|    candidate: VideoCandidate
    22|    testbatch: TestBatch
    23|    review_batch: ReviewBatch
    24|    delivery: ReviewDelivery
    25|
    26|
    27|def build_ai_life_systems_demo_delivery() -> ContentDemoPipelineResult:
    28|    """Run the deterministic AI-life-systems content demo through review prep."""
    29|
    30|    brief = create_content_brief(
    31|        pillar_id="ai_life_systems",
    32|        topic="AI weekly review routine",
    33|        audience_problem="I use AI tools but still lose control of priorities.",
    34|        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
    35|    )
    36|    script = create_script_draft(brief)
    37|    candidate = convert_script_draft_to_candidate(brief, script)
    38|    testbatch = TestBatch(
    39|        batch_id="content-demo-ai-life-systems",
    40|        candidates=(candidate,),
    41|    )
    42|    review_batch = build_review_batch(testbatch)
    43|    review_text = render_review_batch_summary(review_batch)
    44|    delivery = prepare_review_delivery(review_text, topic="review")
    45|
    46|    return ContentDemoPipelineResult(
    47|        brief=brief,
    48|        script=script,
    49|        candidate=candidate,
    50|        testbatch=testbatch,
    51|        review_batch=review_batch,
    52|        delivery=delivery,
    53|    )
    54|


Explanation:
**Planning test implementation**

I need to start by writing tests and figuring out the validation style. The next step is to implement RenderManifest and design the necessary data classes. For the tests, I’ll ensure that creating a shot plan leads to the correct render manifest by checking settings like width, height, fps, and item count. I also need to validate that it rejects any invalid shot plans. Once done, I’ll use write_file and prepare to run the initial tests.

import pytest

from autoshorts.content.brief import create_content_brief
from autoshorts.content.script_draft import create_script_draft
from autoshorts.rendering.manifest import (
    RenderManifest,
    RenderSettings,
    build_render_manifest,
    validate_render_manifest,
)
from autoshorts.rendering.shot_plan import ShotPlan, ShotScene, create_shot_plan

def _shot_plan():
    brief = create_content_brief(
        pillar_id="ai_life_systems",
        topic="AI weekly review routine",
        audience_problem="I use AI tools but still lose control of priorities.",
        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
    )
    script = create_script_draft(brief)
    return create_shot_plan(script)

def test_build_render_manifest_turns_shot_plan_into_vertical_timeline():
    plan = _shot_plan()

    manifest = build_render_manifest(plan)

    assert isinstance(manifest, RenderManifest)
    assert manifest.script_id == "ai_life_systems-ai-weekly-review-routine"
    assert manifest.title == "AI weekly review routine"
    assert manifest.settings == RenderSettings(
        width=1080,
        height=1920,
        fps=30,
        no_generated_text_inside_assets=True,
        captions_renderer_owned=True,
        static_card_limit=0,
    )
    assert len(manifest.timeline) == len(plan.scenes)
    assert [item.scene_purpose for item in manifest.timeline] == [scene.purpose for scene in plan.scenes]
    assert manifest.timeline[0].start_seconds == 0
    assert manifest.timeline[-1].end_seconds == 35
    assert manifest.timeline[0].placeholder_asset_path == (
        "placeholders/ai_life_systems-ai-weekly-review-routine/01-first_frame_hook.png"
    )
    assert all(item.caption_layer.renderer_owned for item in manifest.timeline)
    assert all(item.label_layer.renderer_owned for item in manifest.timeline)
    assert all("static" not in item.motion_instruction.casefold() for item in manifest.timeline)

def test_validate_render_manifest_accepts_default_manifest():
    result = validate_render_manifest(build_render_manifest(_shot_plan()))

    assert result.is_valid is True
    assert result.errors == ()

def test_build_render_manifest_rejects_invalid_shot_plan():
    invalid_plan = ShotPlan(
        script_id="script",
        title="Title",
        estimated_duration_seconds=35,
        scenes=(
            ShotScene(
                purpose="first_frame_hook",
                start_seconds=0,
                end_seconds=3,
                voiceover_beat="Hook",
                visual_action="Static card",
                asset_mode="static_slideshow",
                caption_beat="Hook",
                retention_goal="Stop scroll",
            ),
        ),
    )

    with pytest.raises(ValueError, match="static slideshow asset mode is not allowed"):
        build_render_manifest(invalid_plan)

def test_validate_render_manifest_rejects_unsafe_render_settings():
    manifest = build_render_manifest(_shot_plan())
    unsafe = RenderManifest(
        script_id=manifest.script_id,
        title=manifest.title,
        settings=RenderSettings(
            width=1920,
            height=1080,
            fps=24,
            no_generated_text_inside_assets=False,
            captions_renderer_owned=False,
            static_card_limit=1,
        ),
        timeline=manifest.timeline,
    )

    result = validate_render_manifest(unsafe)

    assert result.is_valid is False
    assert "render size must be vertical 1080x1920" in result.errors
    assert "fps must be 30" in result.errors
    assert "generated text inside assets is not allowed" in result.errors
    assert "captions must be renderer-owned" in result.errors
    assert "static_card_limit must be 0" in result.errors

def test_validate_render_manifest_rejects_timeline_items_without_renderer_owned_layers():
    manifest = build_render_manifest(_shot_plan())
    first = manifest.timeline[0]
    bad_item = type(first)(
        scene_purpose=first.scene_purpose,
        start_seconds=first.start_seconds,
        end_seconds=first.end_seconds,
        asset_mode=first.asset_mode,
        motion_instruction=first.motion_instruction,
        placeholder_asset_path=first.placeholder_asset_path,
        caption_layer=type(first.caption_layer)(text=first.caption_layer.text, renderer_owned=False),
        label_layer=type(first.label_layer)(text=first.label_layer.text, renderer_owned=False),
    )
    unsafe = RenderManifest(
        script_id=manifest.script_id,
        title=manifest.title,
        settings=manifest.settings,
        timeline=(bad_item,) + manifest.timeline[1:],
    )

    result = validate_render_manifest(unsafe)

    assert result.is_valid is False
    assert "timeline item 1 caption layer must be renderer-owned" in result.errors
    assert "timeline item 1 label layer must be renderer-owned" in result.errors


LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_render_manifest.py">
ERROR [5:6] Import "autoshorts.rendering.manifest" could not be resolved [reportMissingImports] (Pyright)
</diagnostics>


python -m pytest tests/test_render_manifest.py -q


Output:
==================================== ERRORS ====================================
________________ ERROR collecting tests/test_render_manifest.py ________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_render_manifest.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_render_manifest.py:5: in <module>
    from autoshorts.rendering.manifest import (
E   ModuleNotFoundError: No module named 'autoshorts.rendering.manifest'
=========================== short test summary info ============================
ERROR tests/test_render_manifest.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.14s


Explanation:
**Implementing validation logic**

Alright, I'm diving into this implementation. I’ll need to import ValidationResult and consider using dataclasses. For the motion instruction, I should probably avoid static from visual_action. However, the test expects no static involved, which is tricky with the "static card." I’ll make it so that if visual_action has Static, it defaults to no. It’s all about using functions efficiently! Let’s keep that in mind while I work through the code.

"""Side-effect-free render manifest generation from shot plans."""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.ideas.candidate import ValidationResult
from autoshorts.rendering.shot_plan import ShotPlan, validate_shot_plan

@dataclass(frozen=True)
class RenderSettings:
    width: int = 1080
    height: int = 1920
    fps: int = 30
    no_generated_text_inside_assets: bool = True
    captions_renderer_owned: bool = True
    static_card_limit: int = 0

@dataclass(frozen=True)
class RenderLayer:
    text: str
    renderer_owned: bool = True

@dataclass(frozen=True)
class RenderTimelineItem:
    scene_purpose: str
    start_seconds: int
    end_seconds: int
    asset_mode: str
    motion_instruction: str
    placeholder_asset_path: str
    caption_layer: RenderLayer
    label_layer: RenderLayer

@dataclass(frozen=True)
class RenderManifest:
    script_id: str
    title: str
    settings: RenderSettings
    timeline: tuple[RenderTimelineItem, ...]

def _is_blank(value: str) -> bool:
    return not (value or "").strip()

def _placeholder_path(script_id: str, index: int, purpose: str) -> str:
    return f"placeholders/{script_id}/{index:02d}-{purpose}.png"

def build_render_manifest(plan: ShotPlan) -> RenderManifest:
    """Turn a validated shot plan into a machine-readable render manifest."""

    validation = validate_shot_plan(plan)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))

    timeline = tuple(
        RenderTimelineItem(
            scene_purpose=scene.purpose,
            start_seconds=scene.start_seconds,
            end_seconds=scene.end_seconds,
            asset_mode=scene.asset_mode,
            motion_instruction=scene.visual_action,
            placeholder_asset_path=_placeholder_path(plan.script_id, index, scene.purpose),
            caption_layer=RenderLayer(text=scene.caption_beat, renderer_owned=True),
            label_layer=RenderLayer(text=scene.purpose.replace("_", " "), renderer_owned=True),
        )
        for index, scene in enumerate(plan.scenes, start=1)
    )
    return RenderManifest(
        script_id=plan.script_id,
        title=plan.title,
        settings=RenderSettings(),
        timeline=timeline,
    )

def validate_render_manifest(manifest: RenderManifest) -> ValidationResult:
    errors: list[str] = []
    if _is_blank(manifest.script_id):
        errors.append("script_id is required")
    if _is_blank(manifest.title):
        errors.append("title is required")

    settings = manifest.settings
    if (settings.width, settings.height) != (1080, 1920):
        errors.append("render size must be vertical 1080x1920")
    if settings.fps != 30:
        errors.append("fps must be 30")
    if not settings.no_generated_text_inside_assets:
        errors.append("generated text inside assets is not allowed")
    if not settings.captions_renderer_owned:
        errors.append("captions must be renderer-owned")
    if settings.static_card_limit != 0:
        errors.append("static_card_limit must be 0")

    if not manifest.timeline:
        errors.append("at least one timeline item is required")

    previous_end: int | None = None
    for index, item in enumerate(manifest.timeline, start=1):
        if _is_blank(item.scene_purpose):
            errors.append(f"timeline item {index} scene_purpose is required")
        if item.end_seconds <= item.start_seconds:
            errors.append(f"timeline item {index} end_seconds must be greater than start_seconds")
        if previous_end is not None and item.start_seconds < previous_end:
            errors.append(f"timeline item {index} start_seconds overlaps previous item")
        previous_end = item.end_seconds

        for field_name, value in {
            "asset_mode": item.asset_mode,
            "motion_instruction": item.motion_instruction,
            "placeholder_asset_path": item.placeholder_asset_path,
            "caption_layer text": item.caption_layer.text,
            "label_layer text": item.label_layer.text,
        }.items():
            if _is_blank(value):
                errors.append(f"timeline item {index} {field_name} is required")

        if item.asset_mode == "static_slideshow":
            errors.append("static slideshow asset mode is not allowed")
        if "baked-in" in item.motion_instruction.casefold():
            errors.append("baked-in text in render assets is not allowed")
        if not item.caption_layer.renderer_owned:
            errors.append(f"timeline item {index} caption layer must be renderer-owned")
        if not item.label_layer.renderer_owned:
            errors.append(f"timeline item {index} label layer must be renderer-owned")

    return ValidationResult(is_valid=not errors, errors=tuple(errors))


Output:
.....                                                                    [100%]
5 passed in 0.06s


Output:
........................................................................ [ 97%]
..                                                                       [100%]
74 passed in 0.11s


patch

- `autoshorts.review.approvals` — deterministic Telegram approval command parser.
- `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.
- `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.

- `autoshorts.review.approvals` — deterministic Telegram approval command parser.
- `autoshorts.rendering.manifest` — side-effect-free vertical render manifest generation from shot plans.
- `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.
- `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -20,6 +20,7 @@
 - `autoshorts.ideas.factory` — draft-to-candidate factory with stable ids, default hashtags, and duration classes.
 - `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
 - `autoshorts.review.approvals` — deterministic Telegram approval command parser.
+- `autoshorts.rendering.manifest` — side-effect-free vertical render manifest generation from shot plans.
 - `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.
 - `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
 - `autoshorts.review.render` — renders complete review batches into Telegram-safe text.

No linter for .md files


`validate_shot_plan()` rejects static slideshow asset modes, baked-in fake UI text, non-renderer-owned labels/captions, bad timing, missing fields, and plans whose first scene is not the first-frame hook.

## Human approval commands

`validate_shot_plan()` rejects static slideshow asset modes, baked-in fake UI text, non-renderer-owned labels/captions, bad timing, missing fields, and plans whose first scene is not the first-frame hook.

`autoshorts.rendering.manifest.build_render_manifest()` turns a validated `ShotPlan` into a machine-readable `RenderManifest` with:

- vertical `1080x1920` render settings;
- `30` fps;
- one timeline item per shot scene;
- motion instructions inherited from the shot plan;
- deterministic placeholder asset paths;
- renderer-owned caption and label layers;
- `no_generated_text_inside_assets=True`;
- `static_card_limit=0`.

`validate_render_manifest()` rejects unsafe render settings, generated text inside assets, non-renderer-owned captions/labels, static slideshow timeline items, missing timeline fields, and bad timeline timing. The manifest is still a preparation artifact only; it renders nothing and calls no external API.

## Human approval commands


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -79,6 +79,19 @@

 `validate_shot_plan()` rejects static slideshow asset modes, baked-in fake UI text, non-renderer-owned labels/captions, bad timing, missing fields, and plans whose first scene is not the first-frame hook.

+`autoshorts.rendering.manifest.build_render_manifest()` turns a validated `ShotPlan` into a machine-readable `RenderManifest` with:
+
+- vertical `1080x1920` render settings;
+- `30` fps;
+- one timeline item per shot scene;
+- motion instructions inherited from the shot plan;
+- deterministic placeholder asset paths;
+- renderer-owned caption and label layers;
+- `no_generated_text_inside_assets=True`;
+- `static_card_limit=0`.
+
+`validate_render_manifest()` rejects unsafe render settings, generated text inside assets, non-renderer-owned captions/labels, static slideshow timeline items, missing timeline fields, and bad timeline timing. The manifest is still a preparation artifact only; it renders nothing and calls no external API.
+
 ## Human approval commands

 Supported review responses:

No linter for .md files


terminal

python -m pytest tests -q && python - <<'PY'
from autoshorts.content.brief import create_content_brief
from autoshorts.content.script_draft import create_script_draft
from autoshorts.rendering.shot_plan import create_shot_plan
from autoshorts.rendering.manifest import build_render_manifest, validate_render_manifest
brief = create_content_brief(
    pillar_id='ai_life_systems',
    topic='AI weekly review routine',
    audience_problem='I use AI tools but still lose control of priorities.',
    payoff='a 10-minute Sunday review that keeps AI as assistant, not boss',
)
script = create_script_draft(brief)
manifest = build_render_manifest(create_shot_plan(script))
print(manifest.script_id)
print((manifest.settings.width, manifest.settings.height, manifest.settings.fps))
print(len(manifest.timeline))
print(manifest.timeline[0].placeholder_asset_path)
print(validate_render_manifest(manifest).is_valid)
PY


Output:
........................................................................ [ 97%]
..                                                                       [100%]
74 passed in 0.11s
ai_life_systems-ai-weekly-review-routine
(1080, 1920, 30)
5
placeholders/ai_life_systems-ai-weekly-review-routine/01-first_frame_hook.png
True