social-media-shorts-automation

---
name: social-media-shorts-automation
description: Design, generate, review, publish, and optimize automated short-form social videos across YouTube Shorts, TikTok, Instagram Reels, and related channels.
version: 1.0.0
tags: [shorts, tiktok, youtube-shorts, reels, creator-monetization, content-automation, social-media]
---

# Social-Media Shorts Automation

Use this skill when building or operating systems that generate short-form social videos, especially faceless or semi-automated pipelines for YouTube Shorts, TikTok, Instagram Reels, Facebook Reels, Pinterest video pins, X video, or LinkedIn video.

## Core principle

Treat the system as a **Shorts Growth Engine** / **semi-automatic Retention Studio**, not merely a renderer. A working video generator can still fail if it produces generic, low-retention, AI-slop content. Optimize for audience identity, hook quality, retention mechanics, visual mechanism reveals, platform fit, and analytics feedback. For the user's strongest strategic direction, see `references/retention-studio-strategy.md`.

For this user's AutoShorts workflows, approval previews should be German for concept comprehension, while final social-media videos should remain English unless explicitly changed. Treat preview legibility as a quality gate: readable font sizes, pixel-width-aware line wrapping, no clipped words, and no overcrowded cards.

## Retention-Studio / Mechanism-Reveal Pipeline

For quality-first automated Shorts, especially Hidden Money & AI Systems concepts, prefer the pipeline documented in `references/retention-studio-pipeline-pattern.md`:

```text
strategy → scored ideas → mechanism explainer → shot plan → keyframe/I2V prompt plan → seeded clip jobs → clip review → deterministic final renderer
```

When implementing a new repo or core pipeline, first scaffold the approval-first typed artifact chain in `references/approval-first-retention-studio-core.md`:

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

Hard rule for this class: AI-video clips provide photorealistic texture and motion only. Captions, numbers, labels, arrows, UI text, price/risk/margin layers, and factual claims are renderer-owned. Final content should keep `static_card_limit = 0`, use visual changes every 1–2 seconds, and reject technically successful clips that do not serve the retention beat.

Before video generation, build an inspectable content-to-review spine: `ContentBrief → ScriptDraft → CandidateDraft → VideoCandidate → TestBatch → ReviewBatch → Telegram ReviewDelivery`. Keep it deterministic, side-effect free, and human-approval-first so hooks, retention beats, visual concepts, policy notes, and review copy can be optimized before spending GPU/API time. See `references/content-to-review-pipeline.md` for the implementation pattern and guardrails.

Hard rule for this class: AI-video clips provide photorealistic texture and motion only. Captions, numbers, labels, arrows, UI text, price/risk/margin layers, and factual claims are renderer-owned. Final content should keep `static_card_limit = 0`, use visual changes every 1–2 seconds, and reject technically successful clips that do not serve the retention beat.

## Recommended workflow

1. **Define channel position**
   - Niche(s): e.g. life optimization, AI life systems, Christian faith/FaithTok.
   - Target audience and emotional problem.
   - Language and tone.
   - Whether the brand is faceless, AI-persona-led, or creator-led.
   - No-go topics and theological/ethical boundaries if faith-related.

2. **Generate test ideas before code-heavy automation**
   - Create 20–30 candidate ideas across 2–4 content pillars.
   - For each: hook, script kernel, visual concept, caption, CTA, risk/quality score.
   - Select a small first batch (e.g. 3 per pillar) for real testing.
   - Before renderers or external AI APIs, add a deterministic content-development core: `ContentPillar → ContentBrief → ScriptDraft → CandidateDraft → VideoCandidate`. See `references/content-development-core.md`.

3. **Use hook-first, retention-led scripting**
   - First 1–2 seconds must contain a clear tension, identity statement, pain point, contrarian line, or immediate self-recognition.
   - Prefer one idea per video over generic “4 tips” unless the list format is the explicit series.
   - Avoid generic advice viewers have already heard unless framed with a new angle, concrete payoff, or strong story.
   - Reward hooks with direct address, specificity, open loops, tension, and a visible payoff; reject slow intros such as “In this video...”.
   - See `references/retention-led-shorts.md` for scoring signals, penalties, and voiceover finalization rules.

4. **Quality gate before rendering**
   Score drafts on:
   - hook strength
   - novelty
   - emotional pull
   - clarity
   - retention risk
   - comment/share potential
   - AI-slop risk
   - platform/policy risk
   Render only drafts above the chosen threshold, e.g. 8/10.

5. **Visual strategy**
   - Avoid obvious AI artifacts, fake UI text, illegible generated lettering, generic charts, and soulless stock visuals.
   - Prefer high-contrast kinetic captions, visible hook text in first seconds, relatable B-roll, screen recordings for AI/tutorial content, phone/screen close-ups, before/after transformations, cards, and timer visuals.
   - Use pattern interrupts every 2–3 seconds for retention.
   - Before rendering, generate a shot plan from the finalized voiceover: first-frame hook, pattern interrupt, core explanation, screen/card visual, CTA, caption beats, and asset type.
   - Before writing a renderer, convert shot plans into deterministic render manifests: machine-readable timeline items, asset placeholders, caption tracks, style constraints, and approval constraints. See `references/render-manifest-pipeline.md`.
   - Treat this as a deterministic intermediate artifact, not optional prose. See `references/retention-render-planning.md` for the five-scene shot-plan pattern and renderer-facing fields.
   - For the first video output, build a manifest-driven preview renderer before any AI-video or polished asset pipeline: first create a side-effect-free local preview plan, then write deterministic PNG scene cards, then build a side-effect-free FFmpeg concat plan, and only then call FFmpeg from an explicit renderer boundary. The boundary should report side effects such as `write_png`, `write_concat`, `call_ffmpeg`, and `write_mp4`; keep generated PNG/MP4 artifacts out of Git. See `references/manifest-preview-renderer.md`.
   - When the user rejects static/slideshow output, add an `asset_plan.v2` layer that maps each scene to motion-led local asset modes such as `cinematic_ai_video`, `screen_recording_simulation`, `mechanism_animation`, or `kinetic_text_overlay` with no baked text. If the user requires a free workflow, do not depend on paid video-credit tools; use local procedural motion and optional free NVIDIA LLM text refinement. See `references/ai-video-asset-planning.md`.
   - For hybrid workflows, add an imported-clip assembly layer: provider handoff prompts create short manual AI-video clips, humans drop them into deterministic `data/imported_ai_clips/<slug>/scene_XX.mp4` paths, and the final renderer combines imported clips with local mechanism animations/captions. See `references/imported-ai-clip-assembly.md`.
   - For local Wan2.2 quality workflows, prefer keyframe/image-to-video over raw text-to-video: generate/select a clean keyframe, run native Wan2.2 I2V variants, review clips bluntly for subject clarity/motion/artifacts, then add all labels/numbers/captions via deterministic renderer. See `references/wan-i2v-retention-studio.md`.
   - When using NVIDIA Build/API Catalog for keyframes, prefer hosted `ai.api.nvidia.com` calls before considering self-hosted NIM deployment; implement a configurable model fallback chain, store the key as a runtime secret, and inspect/clean logos, emblems, license plates, and fake text before I2V. See `references/nvidia-keyframe-generation.md`.

6. **Social-media draft and content-preview approval packages before TTS/rendering polish**
   - Before TTS, platform APIs, or polished video generation, build a typed `SocialMediaDraft` from the approved candidate plus render manifest/preview.
   - Include platform targets, title variants, caption, hashtags, CTA, hook, duration, quality/risk summary, and preview reference.
   - Render this into a Telegram-safe approval message and wrap it in a side-effect-free delivery plan with `requires_human_approval=True` and `side_effects=()`.
   - After a local MP4 timing preview exists, combine the MP4 path plus social draft review into a side-effect-free content preview approval package. This package should carry `approval_language=de`, `final_video_language=en`, explicit approval commands, no external calls, and no side effects.
   - Add local demo CLIs that print metadata first, then review text; draft-only CLIs must not write files, call renderers, call Telegram, or access credentials. MP4 preview CLIs may write local artifacts and call FFmpeg only at explicit renderer boundaries that report side effects.
   - See `references/social-draft-approval-package.md` and `references/content-automation-patterns.md` for concrete artifact sequence, user-specific language rules, and external workflow patterns.

7. **Human approval loop before posting**
   - Send preview to the user through the requested channel, usually Telegram.
   - Include: video or preview reference, title, caption, hashtags, platform recommendation, duration, quality/risk score.
   - When sending multiple preview videos, verify and label distinct artifacts before delivery: candidate ID/title, absolute path, duration, and preferably checksum/hash. If several previews are visually similar timing cards, do not spam them as separate unlabeled media; send a concise indexed list first or bundle with clear labels so the user can tell them apart.
   - Wait for explicit approval such as `Freigabe alle`, `Freigabe YouTube`, `Freigabe TikTok`, `Ablehnen`, or `Ändern: ...`.
   - Do not publish automatically unless the user has explicitly authorized that workflow.

8. **TTS / voice layer after draft approval**
   - Treat TTS as a later layer, not the starting point for a first post draft.
   - Decide voice, final voiceover script, words-per-second, scene timing, caption sync, audio format, provider, and separate voice approval only after the social draft package is reviewable.
   - This avoids spending TTS/rendering effort on unapproved hooks, captions, or platform positioning.
   - For final-near previews, keep one copy source of truth: final voiceover → script draft/retention beats → shot plan → top card text → scene captions → subtitle track → TTS audio. Do not mix old approval-card text with new spoken/subtitle text.
   - If the user says the result is too abstract/abgehoben, rewrite around a concrete beginner-friendly everyday failure example before polishing visuals. Prefer “one weird email arrives” / missing field / unclear request / wrong confident action over jargon such as `exception path`, `input boundary`, or `output standard`. See `references/beginner-friendly-audio-caption-alignment.md`.

9. **Publish and measure**
   Track at minimum:
   - platform, video id/url, niche, format, hook, duration
   - views at 2h/24h/7d
   - likes, comments, shares, saves, follows
   - retention metrics if available
   Use results to generate the next batch.

## Platform duration/monetization notes

See `references/preview-media-delivery-guardrails.md` for avoiding duplicate-looking/unlabeled preview-video spam in Telegram review workflows.

See `references/audio-caption-preview-pipeline.md` for the approval-first EdgeTTS + ASS subtitles + FFmpeg review-MP4 pipeline, including actual-audio-duration probing and the explicit `preserve_video` vs `trim_to_audio` duration-mode guardrail.

See `references/final-motion-hybrid-rendering.md` for the final-near hybrid renderer pattern: ChatGPT Image/Codex or other AI tools provide text-free background/keyframe texture while deterministic renderer overlays own text, arrows, labels, captions, and final assembly.

See `references/monetization-and-approval-notes.md` for current working notes captured from official YouTube/TikTok docs and the recommended approval/posting setup. See `references/retention-led-shorts.md` for retention scoring signals, no-faith/adjacent-pillar handling, and voiceover finalization rules. See `references/render-manifest-pipeline.md` for the script → shot plan → render manifest → renderer sequence that prevents generic AI slideshow output. See `references/manifest-preview-renderer.md` for the first deterministic Pillow/FFmpeg preview-renderer pattern. See `references/local-preview-frame-writer.md` for the staged `LocalPreviewPlan → PNG scene cards → FFmpeg concat plan` boundary before MP4 rendering. See `references/social-draft-approval-package.md` for the typed first-post approval package to build before TTS or platform APIs. See `references/content-automation-patterns.md` for public workflow patterns, queue/status/cost tracking, versioned approval, synthetic-content disclosure, originality gates, and the user's German-preview/English-final rule. See `references/ai-video-asset-planning.md` for motion-led final-render asset planning with Kling/Hailuo/Runway-style clips and screen simulations. See `references/imported-ai-clip-assembly.md` for hybrid assembly where manually generated provider clips are imported into a deterministic local renderer. See `references/nvidia-keyframe-generation.md` for NVIDIA-hosted keyframe generation, model fallback, secret handling, and pre-I2V logo/plate cleanup. Re-check official docs before making business-critical decisions because platform programs change often.

## Project/repo hygiene for automated video bots

- Keep source code in Git; keep generated videos, assets, `.env`, OAuth tokens, API keys, and downloaded secrets out of Git.
- Use `.gitignore` aggressively for outputs, media caches, local credentials, and temp files.
- Store platform credentials as runtime secrets; never print raw tokens.
- Build the repo as modular components: idea generation, scripting, rendering, review, publishing, analytics.
- For approval-first Retention Studio implementations, use a typed side-effect-free core before real I/O: `ContentBrief`, `ScriptDraft`, `VideoCandidate`, review batches, Telegram delivery plans, then shot plans. See `references/approval-first-retention-studio-core.md`.

## Sensitive-content positioning

When AI/productivity automation is the primary brand, be cautious about mixing in religious/FaithTok, health, finance, or other high-trust themes. If the user flags the combination as sensitive, pause that pillar rather than forcing it into the same testbatch. Replace it with a safer adjacent pillar (for example `attention_design`) and document the paused pillar in config so it can be revisited only with explicit approval and a separate brand strategy.

## Pitfalls

- Do not send several generated preview videos as unlabeled media attachments just because they are distinct files. Telegram/mobile previews can make timing-card MP4s look identical. Verify uniqueness (path + title + hash/duration) and provide labels or an indexed summary; if the user complains about duplicate-looking media, stop resending, verify artifacts, and continue the pipeline.
- Do not let final-near preview layers drift apart: if the top field, voiceover, and subtitles do not say the same thing, rebuild the render plan from one final script source before sending another clip.
- Do not make broad-audience AI-system shorts sound like architecture lectures. For early-AI users, convert abstract workflow concepts into concrete everyday failures first, then reveal the principle.
- Do not over-optimize renderer quality before testing whether the content format earns retention.
- Do not let German approval previews become unreadable technical cards: use pixel-width-aware wrapping, readable fonts, limited line counts, and visual checks for clipped/overcrowded text.
- Do not confuse approval language with final audience language. For this user, approval previews are German by default; final social videos remain English unless explicitly changed.
- Do not copy public automation workflows wholesale: keep their queue/status/cost tracking and template structure, but remove blind auto-publishing until a human approval/version lock exists.
- Do not mass-generate repetitive shorts without originality/inauthentic-content gates; this can harm monetization/account health and create AI slop.
- Do not jump directly from a script idea to ComfyUI/Wan/Qwen/Image/video generation. First build and inspect the content-to-review spine so weak hooks, generic scripts, policy issues, and static visual concepts are killed before render time.
- Do not create a parallel candidate path for content experiments; convert content artifacts into the existing candidate factory input so IDs, hashtags, duration classes, quality gates, and pillar/platform validation stay centralized.
- Do not use `x or default` for optional content/script overrides in factories; explicit blank strings and zero durations must pass through to validation so tests can catch them.
- Do not assume TikTok monetization rules match YouTube: TikTok Creator Rewards emphasizes 1min+ eligible videos, while shorter videos may still be better for growth.
- Do not publish religious or health-adjacent content with overconfident claims; preserve humility, nuance, and platform safety.
- Do not rely on platform payout as the only monetization path; plan indirect monetization such as newsletter, templates, affiliates, digital products, or services.


     1|"""Deterministic renderer-owned subtitle cue planning."""
     2|
     3|from __future__ import annotations
     4|
     5|import re
     6|from dataclasses import dataclass, replace
     7|
     8|from autoshorts.ideas.candidate import ValidationResult
     9|
    10|
    11|_WORD_RE = re.compile(r"[A-Za-z0-9]+(?:['-][A-Za-z0-9]+)?")
    12|
    13|
    14|@dataclass(frozen=True)
    15|class SubtitleStyle:
    16|    name: str
    17|    primary_color: str
    18|    accent_color: str
    19|    stroke_color: str
    20|    shadow_color: str
    21|    placement: str
    22|    max_lines: int
    23|    min_words_per_cue: int
    24|    max_words_per_cue: int
    25|    safe_zone_bottom_percent: int
    26|    font_weight: str
    27|
    28|    @classmethod
    29|    def hormozi_inspired(cls) -> "SubtitleStyle":
    30|        """Return the TrueTrace kinetic-caption baseline: inspired mechanics, original brand skin."""
    31|
    32|        return cls(
    33|            name="truetrace_hormozi_inspired",
    34|            primary_color="#FFFFFF",
    35|            accent_color="#22D3EE",
    36|            stroke_color="#050816",
    37|            shadow_color="#000000",
    38|            placement="lower_middle_above_ui_safe_zone",
    39|            max_lines=2,
    40|            min_words_per_cue=2,
    41|            max_words_per_cue=5,
    42|            safe_zone_bottom_percent=14,
    43|            font_weight="800",
    44|        )
    45|
    46|
    47|@dataclass(frozen=True)
    48|class SubtitleCue:
    49|    text: str
    50|    start_ms: int
    51|    end_ms: int
    52|    active_word: str
    53|    renderer_owned: bool = True
    54|    max_lines: int = 2
    55|    emphasis_color: str = "#22D3EE"
    56|
    57|
    58|@dataclass(frozen=True)
    59|class SubtitleTrack:
    60|    cues: tuple[SubtitleCue, ...]
    61|    style: SubtitleStyle
    62|    renderer_owned: bool = True
    63|    side_effects: tuple[str, ...] = ()
    64|    external_calls: tuple[str, ...] = ()
    65|
    66|    def with_cues(self, cues: tuple[SubtitleCue, ...]) -> "SubtitleTrack":
    67|        return replace(self, cues=cues)
    68|
    69|
    70|def _words(text: str) -> list[str]:
    71|    return _WORD_RE.findall(text)
    72|
    73|
    74|def _chunk_words(words: list[str], *, min_words: int, max_words: int) -> list[list[str]]:
    75|    chunks: list[list[str]] = []
    76|    index = 0
    77|    while index < len(words):
    78|        remaining = len(words) - index
    79|        take = min(max_words, remaining)
    80|        if 0 < remaining < min_words and chunks:
    81|            chunks[-1].extend(words[index:])
    82|            break
    83|        if remaining == min_words + 1:
    84|            take = min_words
    85|        chunks.append(words[index : index + take])
    86|        index += take
    87|    return chunks
    88|
    89|
    90|def _active_word(chunk: list[str]) -> str:
    91|    # Prefer the longest meaningful word so the highlighted token carries semantic weight.
    92|    return max((word.upper() for word in chunk), key=lambda word: (len(word), word))
    93|
    94|
    95|def build_hormozi_subtitle_track(text: str, *, total_duration_seconds: int) -> SubtitleTrack:
    96|    """Create continuous 2..5-word renderer-owned subtitle cues for final burn-in."""
    97|
    98|    if not text.strip():
    99|        raise ValueError("subtitle text is required")
   100|    if total_duration_seconds <= 0:
   101|        raise ValueError("total_duration_seconds must be positive")
   102|
   103|    style = SubtitleStyle.hormozi_inspired()
   104|    words = _words(text)
   105|    if len(words) < style.min_words_per_cue:
   106|        raise ValueError("subtitle text must contain at least two words")
   107|
   108|    chunks = _chunk_words(words, min_words=style.min_words_per_cue, max_words=style.max_words_per_cue)
   109|    total_ms = total_duration_seconds * 1000
   110|    cues: list[SubtitleCue] = []
   111|    for index, chunk in enumerate(chunks):
   112|        start_ms = round(index * total_ms / len(chunks))
   113|        end_ms = round((index + 1) * total_ms / len(chunks))
   114|        cues.append(
   115|            SubtitleCue(
   116|                text=" ".join(word.upper() for word in chunk),
   117|                start_ms=start_ms,
   118|                end_ms=end_ms,
   119|                active_word=_active_word(chunk),
   120|                renderer_owned=True,
   121|                max_lines=style.max_lines,
   122|                emphasis_color=style.accent_color,
   123|            )
   124|        )
   125|
   126|    track = SubtitleTrack(cues=tuple(cues), style=style)
   127|    validation = validate_subtitle_track(track)
   128|    if not validation.is_valid:
   129|        raise ValueError("; ".join(validation.errors))
   130|    return track
   131|
   132|
   133|def validate_subtitle_track(track: SubtitleTrack) -> ValidationResult:
   134|    errors: list[str] = []
   135|    if not track.renderer_owned:
   136|        errors.append("subtitle track must be renderer-owned")
   137|    if track.side_effects:
   138|        errors.append("subtitle track must not have side effects")
   139|    if track.external_calls:
   140|        errors.append("subtitle track must not have external calls")
   141|    if not track.cues:
   142|        errors.append("at least one subtitle cue is required")
   143|
   144|    previous_end: int | None = None
   145|    for index, cue in enumerate(track.cues, start=1):
   146|        cue_words = cue.text.split()
   147|        if not cue.renderer_owned:
   148|            errors.append(f"subtitle cue {index} must be renderer-owned")
   149|        if not (track.style.min_words_per_cue <= len(cue_words) <= track.style.max_words_per_cue):
   150|            errors.append(f"subtitle cue {index} must contain 2..5 words")
   151|        if cue.end_ms <= cue.start_ms:
   152|            errors.append(f"subtitle cue {index} end_ms must be greater than start_ms")
   153|        if previous_end is None:
   154|            if cue.start_ms != 0:
   155|                errors.append("first subtitle cue must start at 0ms")
   156|        elif cue.start_ms != previous_end:
   157|            errors.append(f"subtitle cue {index} must start when the previous cue ends")
   158|        if not cue.active_word.strip():
   159|            errors.append(f"subtitle cue {index} active_word is required")
   160|        elif cue.active_word not in {word.upper() for word in cue_words}:
   161|            errors.append(f"subtitle cue {index} active_word must appear in cue text")
   162|        if cue.max_lines > track.style.max_lines:
   163|            errors.append(f"subtitle cue {index} exceeds style max lines")
   164|        previous_end = cue.end_ms
   165|
   166|    return ValidationResult(is_valid=not errors, errors=tuple(errors))
   167|


Explanation:
**Planning scene backgrounds**

I need to generate scene-specific backgrounds, maybe four images, and update the renderer to support those backgrounds for each scene. The subtitle safe zone might need adjusting upward too. So, let's create an asset plan based on that. The user wants me to continue, so I’ll focus next on scene-specific image backgrounds and render them in version 5. I think we won't need TDD for generated images, but will need tests for accepting scene background paths.


     1|"""Audio + subtitle final-preview planning and FFmpeg rendering boundary."""
     2|
     3|from __future__ import annotations
     4|
     5|from collections.abc import Callable
     6|from dataclasses import dataclass
     7|from pathlib import Path
     8|import subprocess
     9|
    10|from autoshorts.audio.subtitles import SubtitleTrack, validate_subtitle_track
    11|from autoshorts.ideas.candidate import ValidationResult
    12|
    13|Runner = Callable[..., subprocess.CompletedProcess[str]]
    14|
    15|
    16|@dataclass(frozen=True)
    17|class AudioCaptionPreviewPlan:
    18|    video_path: str
    19|    audio_path: str
    20|    output_path: str
    21|    subtitle_file_path: str
    22|    subtitle_file_content: str
    23|    subtitle_track: SubtitleTrack
    24|    duration_mode: str = "preserve_video"
    25|    side_effects: tuple[str, ...] = ()
    26|    external_calls: tuple[str, ...] = ()
    27|
    28|
    29|@dataclass(frozen=True)
    30|class AudioCaptionPreviewResult:
    31|    output_path: str
    32|    subtitle_file_path: str
    33|    returncode: int
    34|    stdout: str
    35|    stderr: str
    36|    duration_mode: str = "preserve_video"
    37|    side_effects: tuple[str, ...] = ("write_subtitles", "call_ffmpeg", "write_mp4")
    38|    external_calls: tuple[str, ...] = ("ffmpeg",)
    39|
    40|
    41|def _is_blank(value: str) -> bool:
    42|    return not (value or "").strip()
    43|
    44|
    45|def _ass_timestamp(ms: int) -> str:
    46|    centiseconds = round(ms / 10)
    47|    hours, remainder = divmod(centiseconds, 360000)
    48|    minutes, remainder = divmod(remainder, 6000)
    49|    seconds, centis = divmod(remainder, 100)
    50|    return f"{hours}:{minutes:02d}:{seconds:02d}.{centis:02d}"
    51|
    52|
    53|def _ass_color(hex_color: str) -> str:
    54|    # ASS uses &HAABBGGRR. We keep alpha at 00 and convert #RRGGBB to BGR.
    55|    value = hex_color.removeprefix("#")
    56|    if len(value) != 6:
    57|        return "&H00FFFFFF"
    58|    red, green, blue = value[0:2], value[2:4], value[4:6]
    59|    return f"&H00{blue}{green}{red}"
    60|
    61|
    62|def _escape_ass_text(text: str) -> str:
    63|    return text.replace("{", "").replace("}", "").replace("\n", " ")
    64|
    65|
    66|def render_ass_subtitle_file(track: SubtitleTrack) -> str:
    67|    validation = validate_subtitle_track(track)
    68|    if not validation.is_valid:
    69|        raise ValueError("; ".join(validation.errors))
    70|
    71|    style = track.style
    72|    primary = _ass_color(style.primary_color)
    73|    accent = _ass_color(style.accent_color)
    74|    outline = _ass_color(style.stroke_color)
    75|    lines = [
    76|        "[Script Info]",
    77|        "ScriptType: v4.00+",
    78|        "PlayResX: 1080",
    79|        "PlayResY: 1920",
    80|        "ScaledBorderAndShadow: yes",
    81|        "",
    82|        "[V4+ Styles]",
    83|        "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
    84|        f"Style: TrueTraceCaption,Arial,86,{primary},{accent},{outline},&H80000000,-1,0,0,0,100,100,0,0,1,5,2,2,80,80,260,1",
    85|        "",
    86|        "[Events]",
    87|        "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
    88|    ]
    89|    for cue in track.cues:
    90|        cue_text = _escape_ass_text(cue.text)
    91|        lines.append(
    92|            "Dialogue: 0,"
    93|            f"{_ass_timestamp(cue.start_ms)},{_ass_timestamp(cue.end_ms)},"
    94|            f"TrueTraceCaption,,0,0,0,,{cue_text}"
    95|        )
    96|    return "\n".join(lines) + "\n"
    97|
    98|
    99|def build_audio_caption_preview_plan(
   100|    *,
   101|    video_path: str | Path,
   102|    audio_path: str | Path,
   103|    subtitle_track: SubtitleTrack,
   104|    output_path: str | Path,
   105|    duration_mode: str = "preserve_video",
   106|) -> AudioCaptionPreviewPlan:
   107|    out = Path(output_path)
   108|    plan = AudioCaptionPreviewPlan(
   109|        video_path=str(video_path),
   110|        audio_path=str(audio_path),
   111|        output_path=str(out),
   112|        subtitle_file_path=str(out.with_suffix(".ass")),
   113|        subtitle_file_content=render_ass_subtitle_file(subtitle_track),
   114|        subtitle_track=subtitle_track,
   115|        duration_mode=duration_mode,
   116|        side_effects=(),
   117|        external_calls=(),
   118|    )
   119|    validation = validate_audio_caption_preview_plan(plan)
   120|    if not validation.is_valid:
   121|        raise ValueError("; ".join(validation.errors))
   122|    return plan
   123|
   124|
   125|def validate_audio_caption_preview_plan(plan: AudioCaptionPreviewPlan) -> ValidationResult:
   126|    errors: list[str] = []
   127|    for field_name, value in {
   128|        "video_path": plan.video_path,
   129|        "audio_path": plan.audio_path,
   130|        "output_path": plan.output_path,
   131|        "subtitle_file_path": plan.subtitle_file_path,
   132|        "subtitle_file_content": plan.subtitle_file_content,
   133|    }.items():
   134|        if _is_blank(value):
   135|            errors.append(f"{field_name} is required")
   136|    if plan.video_path and not plan.video_path.endswith(".mp4"):
   137|        errors.append("video_path must end with .mp4")
   138|    if plan.audio_path and not plan.audio_path.endswith(".mp3"):
   139|        errors.append("audio_path must end with .mp3")
   140|    if plan.output_path and not plan.output_path.endswith(".mp4"):
   141|        errors.append("output_path must end with .mp4")
   142|    if plan.subtitle_file_path and not plan.subtitle_file_path.endswith(".ass"):
   143|        errors.append("subtitle_file_path must end with .ass")
   144|    if plan.duration_mode not in {"preserve_video", "trim_to_audio"}:
   145|        errors.append("duration_mode must be preserve_video or trim_to_audio")
   146|    if plan.side_effects:
   147|        errors.append("audio caption preview plans must not have side effects")
   148|    if plan.external_calls:
   149|        errors.append("audio caption preview plans must not have external calls")
   150|    errors.extend(validate_subtitle_track(plan.subtitle_track).errors)
   151|    return ValidationResult(is_valid=not errors, errors=tuple(errors))
   152|
   153|
   154|def _ass_filter_path(path: str) -> str:
   155|    # Avoid shell quoting entirely by passing argv; escape colon/backslash for FFmpeg filter parser.
   156|    return path.replace("\\", "\\\\").replace(":", "\\:").replace("'", "\\'")
   157|
   158|
   159|def _command(plan: AudioCaptionPreviewPlan) -> list[str]:
   160|    command = [
   161|        "ffmpeg",
   162|        "-y",
   163|        "-i",
   164|        plan.video_path,
   165|        "-i",
   166|        plan.audio_path,
   167|        "-vf",
   168|        f"ass='{_ass_filter_path(plan.subtitle_file_path)}'",
   169|        "-map",
   170|        "0:v:0",
   171|        "-map",
   172|        "1:a:0",
   173|        "-c:v",
   174|        "libx264",
   175|        "-c:a",
   176|        "aac",
   177|    ]
   178|    if plan.duration_mode == "trim_to_audio":
   179|        command.append("-shortest")
   180|    command.extend([
   181|        "-movflags",
   182|        "+faststart",
   183|        plan.output_path,
   184|    ])
   185|    return command
   186|
   187|
   188|def render_audio_caption_preview(plan: AudioCaptionPreviewPlan, *, runner: Runner = subprocess.run) -> AudioCaptionPreviewResult:
   189|    """Write ASS subtitles and call FFmpeg to mux voiceover + burn captions into an MP4."""
   190|
   191|    validation = validate_audio_caption_preview_plan(plan)
   192|    if not validation.is_valid:
   193|        raise ValueError("; ".join(validation.errors))
   194|    if not Path(plan.video_path).exists():
   195|        raise FileNotFoundError(plan.video_path)
   196|    if not Path(plan.audio_path).exists():
   197|        raise FileNotFoundError(plan.audio_path)
   198|
   199|    subtitle_path = Path(plan.subtitle_file_path)
   200|    output_path = Path(plan.output_path)
   201|    subtitle_path.parent.mkdir(parents=True, exist_ok=True)
   202|    output_path.parent.mkdir(parents=True, exist_ok=True)
   203|    subtitle_path.write_text(plan.subtitle_file_content)
   204|
   205|    completed = runner(_command(plan), capture_output=True, text=True, check=False)
   206|    if completed.returncode != 0:
   207|        raise RuntimeError(completed.stderr or "ffmpeg audio/caption preview failed")
   208|    if not output_path.exists():
   209|        raise RuntimeError("ffmpeg completed but did not create output video")
   210|
   211|    return AudioCaptionPreviewResult(
   212|        output_path=str(output_path),
   213|        subtitle_file_path=str(subtitle_path),
   214|        returncode=completed.returncode,
   215|        stdout=completed.stdout,
   216|        stderr=completed.stderr,
   217|        duration_mode=plan.duration_mode,
   218|        side_effects=("write_subtitles", "call_ffmpeg", "write_mp4"),
   219|        external_calls=("ffmpeg",),
   220|    )
   221|


from pathlib import Path

from autoshorts.audio.voiceover import build_truetrace_final_voiceover
from autoshorts.rendering.final_video_plan import build_truetrace_weird_email_final_video_plan
from autoshorts.rendering.scene_backgrounds import (
    SceneBackgroundPlan,
    build_chatgpt_image_scene_background_plan,
    validate_scene_background_plan,
)

def test_build_chatgpt_image_scene_background_plan_is_text_free_and_scene_specific(tmp_path):
    voiceover = build_truetrace_final_voiceover("truetrace-ai-automation-quiet-rot-38s")
    final_plan = build_truetrace_weird_email_final_video_plan(voiceover)

    plan = build_chatgpt_image_scene_background_plan(final_plan, output_dir=tmp_path)

    assert isinstance(plan, SceneBackgroundPlan)
    assert plan.candidate_id == final_plan.candidate_id
    assert [asset.scene_id for asset in plan.assets] == [scene.scene_id for scene in final_plan.scenes]
    assert all(asset.provider == "chatgpt_image_codex" for asset in plan.assets)
    assert all(asset.output_path.endswith(".png") for asset in plan.assets)
    assert all("no readable text" in asset.prompt.casefold() for asset in plan.assets)
    assert all("no logos" in asset.prompt.casefold() for asset in plan.assets)
    assert all(asset.renderer_owned_overlay_required is True for asset in plan.assets)
    assert plan.side_effects == ()
    assert plan.external_calls == ()

def test_validate_scene_background_plan_rejects_text_or_side_effects(tmp_path):
    voiceover = build_truetrace_final_voiceover("truetrace-ai-automation-quiet-rot-38s")
    final_plan = build_truetrace_weird_email_final_video_plan(voiceover)
    valid = build_chatgpt_image_scene_background_plan(final_plan, output_dir=tmp_path)
    broken_asset = valid.assets[0].__class__(
        scene_id="clean-demo",
        provider="chatgpt_image_codex",
        prompt="Generate a dashboard with readable text and logos",
        output_path=str(tmp_path / "bad.jpg"),
        renderer_owned_overlay_required=False,
    )
    broken = valid.__class__(
        candidate_id=valid.candidate_id,
        assets=(broken_asset,),
        side_effects=("call_image_api",),
        external_calls=("openai",),
    )

    validation = validate_scene_background_plan(broken)

    assert validation.is_valid is False
    assert "scene background plan must not have side effects" in validation.errors
    assert "scene background plan must not have external calls" in validation.errors
    assert "asset 1 output_path must end with .png" in validation.errors
    assert "asset 1 prompt must explicitly forbid readable text" in validation.errors
    assert "asset 1 prompt must explicitly forbid logos" in validation.errors
    assert "asset 1 requires renderer-owned overlays" in validation.errors

Explanation:
**Considering style adjustments**

I’m thinking about whether to add a style for the safe zone that affects the margin for subtitles. The existing safe zone has a margin of 260, which I could increase or adjust. Maybe I should consider a function to compact things better for a safer style? I wonder if the user mentioned to continue after the final motion. I’ll also check on rendering backgrounds for each scene and see if I can generate images concurrently. First, though, I need to code the tests.


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


patch

    background_paths = {scene.scene_id: tmp_path / f"{scene.scene_id}.png" for scene in final_plan.scenes}
    for path in background_paths.values():
        path.write_bytes(b"fake image")
    render_plan = build_motion_render_plan(
        final_plan,
        output_dir=tmp_path,
        preview_scale=0.5,
        background_paths=background_paths,
    )

    assert render_plan.background_paths == {scene_id: str(path) for scene_id, path in background_paths.items()}
    assert render_plan.width == 540

    background = tmp_path / "chatgpt-image-background.png"
    background.write_bytes(b"fake image")
    render_plan = build_motion_render_plan(final_plan, output_dir=tmp_path, preview_scale=0.5, background_path=background)

    assert render_plan.background_path == str(background)
    assert render_plan.width == 540


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_motion_renderer.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_motion_renderer.py
@@ -15,11 +15,17 @@
     voiceover = build_truetrace_final_voiceover("truetrace-ai-automation-quiet-rot-38s")
     final_plan = build_truetrace_weird_email_final_video_plan(voiceover)

-    background = tmp_path / "chatgpt-image-background.png"
-    background.write_bytes(b"fake image")
-    render_plan = build_motion_render_plan(final_plan, output_dir=tmp_path, preview_scale=0.5, background_path=background)
+    background_paths = {scene.scene_id: tmp_path / f"{scene.scene_id}.png" for scene in final_plan.scenes}
+    for path in background_paths.values():
+        path.write_bytes(b"fake image")
+    render_plan = build_motion_render_plan(
+        final_plan,
+        output_dir=tmp_path,
+        preview_scale=0.5,
+        background_paths=background_paths,
+    )

-    assert render_plan.background_path == str(background)
+    assert render_plan.background_paths == {scene_id: str(path) for scene_id, path in background_paths.items()}
     assert render_plan.width == 540
     assert render_plan.height == 960
     assert render_plan.fps == 24

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_motion_renderer.py">
ERROR [25:9] No parameter named "background_paths" [reportCallIssue] (Pyright)
</diagnostics>


Output:
==================================== ERRORS ====================================
_______________ ERROR collecting tests/test_scene_backgrounds.py _______________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_scene_backgrounds.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_scene_backgrounds.py:5: in <module>
    from autoshorts.rendering.scene_backgrounds import (
E   ModuleNotFoundError: No module named 'autoshorts.rendering.scene_backgrounds'
=========================== short test summary info ============================
ERROR tests/test_scene_backgrounds.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.16s


"""Scene-specific text-free background planning for final-near motion renders."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

from autoshorts.ideas.candidate import ValidationResult
from autoshorts.rendering.final_video_plan import FinalVideoPlan, validate_final_video_plan

@dataclass(frozen=True)
class SceneBackgroundAsset:
    scene_id: str
    provider: str
    prompt: str
    output_path: str
    renderer_owned_overlay_required: bool = True

@dataclass(frozen=True)
class SceneBackgroundPlan:
    candidate_id: str
    assets: tuple[SceneBackgroundAsset, ...]
    side_effects: tuple[str, ...] = ()
    external_calls: tuple[str, ...] = ()

_SCENE_PROMPT_NOTES = {
    "clean-demo": "clean modern laptop workspace, calm inbox dashboard glow, orderly automation demo mood",
    "weird-email": "slightly messy email-workflow atmosphere, amber warning mood, desk close-up, shallow depth of field",
    "wrong-routing": "abstract AI routing node ambience, glowing path energy, subtle red warning tension",
    "three-way-split": "minimal dark technical background with empty space for a three-branch diagram overlay",
    "final-rule": "premium minimal blue-green abstract technology background, calm final-rule energy",
}

def build_chatgpt_image_scene_background_plan(final_plan: FinalVideoPlan, *, output_dir: str | Path) -> SceneBackgroundPlan:
    validation = validate_final_video_plan(final_plan)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))
    root = Path(output_dir)
    assets = tuple(
        SceneBackgroundAsset(
            scene_id=scene.scene_id,
            provider="chatgpt_image_codex",
            prompt=(
                "Vertical 9:16 cinematic background for a TrueTrace AI automation short. "
                f"Scene: {scene.scene_id}. {_SCENE_PROMPT_NOTES.get(scene.scene_id, scene.asset_prompt)}. "
                "No readable text, no logos, no watermarks, no UI labels, no fake letters. "
                "Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. "
                "Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered."
            ),
            output_path=str(root / f"{scene.scene_id}.png"),
            renderer_owned_overlay_required=True,
        )
        for scene in final_plan.scenes
    )
    plan = SceneBackgroundPlan(candidate_id=final_plan.candidate_id, assets=assets)
    validation = validate_scene_background_plan(plan)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))
    return plan

def validate_scene_background_plan(plan: SceneBackgroundPlan) -> ValidationResult:
    errors: list[str] = []
    if not plan.candidate_id.strip():
        errors.append("candidate_id is required")
    if not plan.assets:
        errors.append("at least one scene background asset is required")
    if plan.side_effects:
        errors.append("scene background plan must not have side effects")
    if plan.external_calls:
        errors.append("scene background plan must not have external calls")
    seen: set[str] = set()
    for index, asset in enumerate(plan.assets, start=1):
        if not asset.scene_id.strip():
            errors.append(f"asset {index} scene_id is required")
        if asset.scene_id in seen:
            errors.append(f"duplicate scene background asset: {asset.scene_id}")
        seen.add(asset.scene_id)
        if asset.provider != "chatgpt_image_codex":
            errors.append(f"asset {index} provider must be chatgpt_image_codex")
        if not asset.output_path.endswith(".png"):
            errors.append(f"asset {index} output_path must end with .png")
        prompt = asset.prompt.casefold()
        if "no readable text" not in prompt:
            errors.append(f"asset {index} prompt must explicitly forbid readable text")
        if "no logos" not in prompt:
            errors.append(f"asset {index} prompt must explicitly forbid logos")
        if not asset.renderer_owned_overlay_required:
            errors.append(f"asset {index} requires renderer-owned overlays")
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


    background_paths: dict[str, str] | None = None
    side_effects: tuple[str, ...] = ()

    background_path: str | None = None
    side_effects: tuple[str, ...] = ()


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
@@ -26,7 +26,7 @@
     width: int
     height: int
     fps: int
-    background_path: str | None = None
+    background_paths: dict[str, str] | None = None
     side_effects: tuple[str, ...] = ()
     external_calls: tuple[str, ...] = ()

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py">
ERROR [71:9] No parameter named "background_path" [reportCallIssue] (Pyright)
ERROR [248:13] Cannot access attribute "background_path" for class "MotionRenderPlan"
  Attribute "background_path" is unknown [reportAttributeAccessIssue] (Pyright)
ERROR [249:38] Cannot access attribute "background_path" for class "MotionRenderPlan"
  Attribute "background_path" is unknown [reportAttributeAccessIssue] (Pyright)
</diagnostics>


    preview_scale: float = 1.0,
    background_paths: dict[str, str | Path] | None = None,
) -> MotionRenderPlan:

    preview_scale: float = 1.0,
    background_path: str | Path | None = None,
) -> MotionRenderPlan:


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
@@ -48,7 +48,7 @@
     *,
     output_dir: str | Path,
     preview_scale: float = 1.0,
-    background_path: str | Path | None = None,
+    background_paths: dict[str, str | Path] | None = None,
 ) -> MotionRenderPlan:
     validation = validate_final_video_plan(final_plan)
     if not validation.is_valid:

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py">
ERROR [71:49] "background_path" is not defined [reportUndefinedVariable] (Pyright)
ERROR [71:29] "background_path" is not defined [reportUndefinedVariable] (Pyright)
</diagnostics>


        fps=fps,
        background_paths={scene_id: str(path) for scene_id, path in (background_paths or {}).items()} or None,
    )

        fps=fps,
        background_path=str(background_path) if background_path is not None else None,
    )


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
@@ -68,7 +68,7 @@
         width=width,
         height=height,
         fps=fps,
-        background_path=str(background_path) if background_path is not None else None,
+        background_paths={scene_id: str(path) for scene_id, path in (background_paths or {}).items()} or None,
     )
     validation = validate_motion_render_plan(plan)
     if not validation.is_valid:


    background_cache: dict[str, Image.Image] = {}
    for scene_id, path in (plan.background_paths or {}).items():
        background_cache[scene_id] = Image.open(path).convert("RGB").resize((plan.width, plan.height), Image.Resampling.LANCZOS)
    frame_index = 0

    background = None
    if plan.background_path:
        background = Image.open(plan.background_path).convert("RGB").resize((plan.width, plan.height), Image.Resampling.LANCZOS)
    frame_index = 0


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
@@ -244,9 +244,9 @@
     frame_dir.mkdir(parents=True, exist_ok=True)
     Path(plan.output_dir).mkdir(parents=True, exist_ok=True)
     frame_paths: list[str] = []
-    background = None
-    if plan.background_path:
-        background = Image.open(plan.background_path).convert("RGB").resize((plan.width, plan.height), Image.Resampling.LANCZOS)
+    background_cache: dict[str, Image.Image] = {}
+    for scene_id, path in (plan.background_paths or {}).items():
+        background_cache[scene_id] = Image.open(path).convert("RGB").resize((plan.width, plan.height), Image.Resampling.LANCZOS)
     frame_index = 0
     for scene in plan.final_plan.scenes:
         scene_duration = (scene.end_ms - scene.start_ms) / 1000

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py">
ERROR [258:69] "background" is not defined [reportUndefinedVariable] (Pyright)
</diagnostics>


image = _render_scene(plan, scene, progress, background=background_cache.get(scene.scene_id))

image = _render_scene(plan, scene, progress, background=background)


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/rendering/motion_renderer.py
@@ -255,7 +255,7 @@
             count = min(count, max_frames_per_scene)
         for local_index in range(count):
             progress = local_index / max(1, count - 1)
-            image = _render_scene(plan, scene, progress, background=background)
+            image = _render_scene(plan, scene, progress, background=background_cache.get(scene.scene_id))
             path = frame_dir / f"frame_{frame_index:05d}.png"
             image.save(path, format="PNG")
             frame_paths.append(str(path))


terminal

pytest tests/test_scene_backgrounds.py tests/test_motion_renderer.py -q


Output:
....                                                                     [100%]
4 passed in 0.26s


python - <<'PY'
from pathlib import Path
from autoshorts.audio.voiceover import build_truetrace_final_voiceover
from autoshorts.rendering.final_video_plan import build_truetrace_weird_email_final_video_plan
from autoshorts.rendering.scene_backgrounds import build_chatgpt_image_scene_background_plan
voiceover=build_truetrace_final_voiceover('truetrace-ai-automation-quiet-rot-38s')
plan=build_truetrace_weird_email_final_video_plan(voiceover)
bg=build_chatgpt_image_scene_background_plan(plan, output_dir='data/generated_backgrounds/truetrace-weird-email-v1')
for asset in bg.assets:
    print('---', asset.scene_id)
    print(asset.output_path)
    print(asset.prompt)
PY

Explanation:
**Generating scene images**

I'm thinking I need to generate 5 scene images. I could use the image generation tool, maybe in a sequential manner or through parallel execution. Although, I'm wondering if 5 is too many to handle at once. The parallel wrapper seems flexible enough to accommodate this, so I should explore how to best combine the image generation with the multi_tool capabilities. It feels like I have a few options here!


Output:
--- clean-demo
data/generated_backgrounds/truetrace-weird-email-v1/clean-demo.png
Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: clean-demo. clean modern laptop workspace, calm inbox dashboard glow, orderly automation demo mood. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.
--- weird-email
data/generated_backgrounds/truetrace-weird-email-v1/weird-email.png
Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: weird-email. slightly messy email-workflow atmosphere, amber warning mood, desk close-up, shallow depth of field. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.
--- wrong-routing
data/generated_backgrounds/truetrace-weird-email-v1/wrong-routing.png
Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: wrong-routing. abstract AI routing node ambience, glowing path energy, subtle red warning tension. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.
--- three-way-split
data/generated_backgrounds/truetrace-weird-email-v1/three-way-split.png
Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: three-way-split. minimal dark technical background with empty space for a three-branch diagram overlay. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.
--- final-rule
data/generated_backgrounds/truetrace-weird-email-v1/final-rule.png
Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: final-rule. premium minimal blue-green abstract technology background, calm final-rule energy. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.


Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: clean-demo. Clean modern laptop workspace, calm inbox dashboard glow, orderly automation demo mood. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.

Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: weird-email. Slightly messy email-workflow atmosphere, amber warning mood, desk close-up, shallow depth of field. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.

Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: wrong-routing. Abstract AI routing node ambience, glowing path energy, subtle red warning tension. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.


Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: clean-demo. Clean modern laptop workspace, calm inbox dashboard glow, orderly automation demo mood. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.


Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: weird-email. Slightly messy email-workflow atmosphere, amber warning mood, desk close-up, shallow depth of field. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.


Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: wrong-routing. Abstract AI routing node ambience, glowing path energy, subtle red warning tension. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.


Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: three-way-split. Minimal dark technical background with empty space for a three-branch diagram overlay. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.

Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: final-rule. Premium minimal blue-green abstract technology background, calm final-rule energy. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.


Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: three-way-split. Minimal dark technical background with empty space for a three-branch diagram overlay. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.


Prompt: Vertical 9:16 cinematic background for a TrueTrace AI automation short. Scene: final-rule. Premium minimal blue-green abstract technology background, calm final-rule energy. No readable text, no logos, no watermarks, no UI labels, no fake letters. Leave clean central negative space for renderer-owned cards, labels, arrows, and captions. Modern premium tech aesthetic, dark blue/green palette, realistic but not cluttered.


Need copy to data/generated... use terminal cp? Use terminal for cp ok.

mkdir -p data/generated_backgrounds/truetrace-weird-email-v1 && cp /home/agent/.hermes/cache/images/openai_codex_gpt-image-2-medium_20260525_002134_d3c821b5.png data/generated_backgrounds/truetrace-weird-email-v1/clean-demo.png && cp /home/agent/.hermes/cache/images/openai_codex_gpt-image-2-medium_20260525_002219_f22bec3f.png data/generated_backgrounds/truetrace-weird-email-v1/weird-email.png && cp /home/agent/.hermes/cache/images/openai_codex_gpt-image-2-medium_20260525_002251_1dfe8a3e.png data/generated_backgrounds/truetrace-weird-email-v1/wrong-routing.png && cp /home/agent/.hermes/cache/images/openai_codex_gpt-image-2-medium_20260525_002346_15f5da53.png data/generated_backgrounds/truetrace-weird-email-v1/three-way-split.png && cp /home/agent/.hermes/cache/images/openai_codex_gpt-image-2-medium_20260525_002425_90559d8e.png data/generated_backgrounds/truetrace-weird-email-v1/final-rule.png && sha256sum data/generated_backgrounds/truetrace-weird-email-v1/*.png


Output:
bf5cb0ea87177d298be147ce00c998ee4d3b04f1c102b1934b286cc2b14117e5  data/generated_backgrounds/truetrace-weird-email-v1/clean-demo.png
27c2135bfa512c06f57e9676b36915199ded08d112864912ee390a7a8f1a56c9  data/generated_backgrounds/truetrace-weird-email-v1/final-rule.png
8b0c27c2039e0d3fffca7c3bec015df3fae54f167c32933563ec039a6860e3aa  data/generated_backgrounds/truetrace-weird-email-v1/three-way-split.png
cd11aca663e55c6ee6e24aec3d60feb0cfc2973ed73f39eb7b3bbc66870b6fb5  data/generated_backgrounds/truetrace-weird-email-v1/weird-email.png
d6c8864c95295bd284a0b26ae7c32880b7ee689276c7588c56c05b8e288e462d  data/generated_backgrounds/truetrace-weird-email-v1/wrong-routing.png


python - <<'PY'
from pathlib import Path
from math import ceil
from autoshorts.audio.voiceover import build_truetrace_final_voiceover
from autoshorts.audio.tts_plan import build_default_truetrace_tts_plan
from autoshorts.audio.edge_tts_adapter import EdgeTTSRequest, synthesize_edge_tts
from autoshorts.audio.probe import probe_media_duration_seconds
from autoshorts.audio.subtitles import build_hormozi_subtitle_track
from autoshorts.audio.final_preview import build_audio_caption_preview_plan, render_audio_caption_preview
from autoshorts.rendering.final_video_plan import build_truetrace_weird_email_final_video_plan
from autoshorts.rendering.scene_backgrounds import build_chatgpt_image_scene_background_plan
from autoshorts.rendering.motion_renderer import build_motion_render_plan, render_motion_preview
candidate_id='truetrace-ai-automation-quiet-rot-38s'
output_dir=Path('data/previews/truetrace-final-motion-v5')/candidate_id
voiceover=build_truetrace_final_voiceover(candidate_id)
final_plan=build_truetrace_weird_email_final_video_plan(voiceover)
bg_plan=build_chatgpt_image_scene_background_plan(final_plan, output_dir='data/generated_backgrounds/truetrace-weird-email-v1')
background_paths={asset.scene_id: asset.output_path for asset in bg_plan.assets}
motion=render_motion_preview(build_motion_render_plan(final_plan, output_dir=output_dir, preview_scale=0.5, background_paths=background_paths))
if motion.returncode: raise SystemExit(motion.stderr)
voice=synthesize_edge_tts(EdgeTTSRequest(text=voiceover.text, tts_plan=build_default_truetrace_tts_plan(), output_path=str(output_dir/'voice.mp3')))
audio_duration=probe_media_duration_seconds(voice.output_path)
subtitles=build_hormozi_subtitle_track(voiceover.text, total_duration_seconds=ceil(audio_duration))
final=render_audio_caption_preview(build_audio_caption_preview_plan(video_path=motion.output_path,audio_path=voice.output_path,subtitle_track=subtitles,output_path=output_dir/'final-motion-audio-caption-preview.mp4',duration_mode='trim_to_audio'))
if final.returncode: raise SystemExit(final.stderr)
print('final', final.output_path)
print('frames', len(motion.frame_paths))
print('backgrounds', len(background_paths))
print('audio_duration', f'{audio_duration:.3f}')
PY
rm -rf /tmp/truetrace_review_v5 && mkdir -p /tmp/truetrace_review_v5 && ffmpeg -y -i data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4 -vf "select='eq(n,0)+eq(n,120)+eq(n,300)+eq(n,520)+eq(n,760)'" -vsync 0 /tmp/truetrace_review_v5/frame_%02d.png >/dev/null 2>&1
ffprobe -v error -show_entries stream=codec_type,codec_name,width,height,duration -of default=noprint_wrappers=1 data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4
sha256sum data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4


Output:
final data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4
frames 887
backgrounds 5
audio_duration 36.864
codec_name=h264
codec_type=video
width=540
height=960
duration=36.833333
codec_name=aac
codec_type=audio
duration=36.864000
ac8432ef5285abaa8bdb662610184cea491d3b1951dc9b2f2b6dd5a9f4ea9143  data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4


Question: Review this v5 three-way split frame after scene-specific ChatGPT Image background. Check mobile readability, text clipping, subtitle overlap, and whether the mechanism is clearer than a static card.


Image loaded into your context — you can see it natively now. Use your built-in vision to answer the user.

Question: Review this v5 three-way split frame after scene-specific ChatGPT Image background. Check mobile readability, text clipping, subtitle overlap, and whether the mechanism is clearer than a static card.
[screenshot]


Question: Review the weird-email scene frame for readability, no clipped renderer text, and whether AI background avoids readable fake text/logos.

Explanation:
**Evaluating image output**

I’m checking the vision output, and the actual image looks good. However, I’m noticing that the subtitle might be overlapping a bit at the bottom. Well, that’s not ideal, but it’s manageable. I think I need to evaluate the final frame more to see if there’s anything else I should adjust. Overall, it seems like I’m on the right track, but small details matter!


Image loaded into your context — you can see it natively now. Use your built-in vision to answer the user.

Question: Review the weird-email scene frame for readability, no clipped renderer text, and whether AI background avoids readable fake text/logos.
[screenshot]


git status --short && git check-ignore -v data/generated_backgrounds/truetrace-weird-email-v1/clean-demo.png data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4 || true


Output:
M autoshorts/rendering/motion_renderer.py
 M tests/test_motion_renderer.py
?? autoshorts/rendering/scene_backgrounds.py
?? tests/test_scene_backgrounds.py
.gitignore:27:*.png	data/generated_backgrounds/truetrace-weird-email-v1/clean-demo.png
.gitignore:20:data/previews/	data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4


patch

- `autoshorts.rendering.scene_backgrounds` — scene-specific ChatGPT Image/Codex background prompt plans that explicitly forbid readable text/logos and require renderer-owned overlays.
- `autoshorts.rendering.motion_renderer` — procedural motion preview renderer for inbox cards, warning routes, and three-path mechanism diagrams before heavier AI-video work.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -36,6 +36,7 @@
 - `autoshorts.strategy.score` — follower-conversion strategy scoring for hook, curiosity, identity, emotion, trust, follow reason, originality, and variety.
 - `autoshorts.strategy.series` — public-safe series registry and hard no-go validation before public content advances.
 - `autoshorts.rendering.final_video_plan` — final-near 1080x1920 production plans that keep AI visuals text-free and renderer-owned.
+- `autoshorts.rendering.scene_backgrounds` — scene-specific ChatGPT Image/Codex background prompt plans that explicitly forbid readable text/logos and require renderer-owned overlays.
 - `autoshorts.rendering.motion_renderer` — procedural motion preview renderer for inbox cards, warning routes, and three-path mechanism diagrams before heavier AI-video work.
 - `autoshorts.rendering.ffmpeg_plan` — side-effect-free FFmpeg concat planning for local preview frames.
 - `autoshorts.rendering.ffmpeg_renderer` — explicit FFmpeg boundary that writes concat files and MP4 timing previews.

No linter for .md files


- Next polish should improve subtitle phrasing/timing around the three-way split and optionally replace the single static background with per-scene generated backgrounds or short I2V texture clips.

## V5 update

`autoshorts.rendering.scene_backgrounds.build_chatgpt_image_scene_background_plan()` now creates one text-free ChatGPT Image/Codex prompt per production scene. The motion renderer accepts a `background_paths` mapping so each scene can use its own background while keeping all text, arrows, cards, labels, and captions deterministic.

Current v5 review artifact:

```text
data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4
```

V5 uses five generated backgrounds under:

```text
data/generated_backgrounds/truetrace-weird-email-v1/
```

These media files remain ignored by Git.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/docs/final_motion_prototype.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/docs/final_motion_prototype.md
@@ -65,3 +65,21 @@
 - The three-way split scene has been visually checked so arrows sit behind labels and subtitles do not critically overlap the mechanism.
 - The prototype is still a review prototype, not a final public export.
 - Next polish should improve subtitle phrasing/timing around the three-way split and optionally replace the single static background with per-scene generated backgrounds or short I2V texture clips.
+
+## V5 update
+
+`autoshorts.rendering.scene_backgrounds.build_chatgpt_image_scene_background_plan()` now creates one text-free ChatGPT Image/Codex prompt per production scene. The motion renderer accepts a `background_paths` mapping so each scene can use its own background while keeping all text, arrows, cards, labels, and captions deterministic.
+
+Current v5 review artifact:
+
+```text
+data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4
+```
+
+V5 uses five generated backgrounds under:
+
+```text
+data/generated_backgrounds/truetrace-weird-email-v1/
+```
+
+These media files remain ignored by Git.

No linter for .md files


Output:
........................................................................ [ 42%]
........................................................................ [ 85%]
........................                                                 [100%]
168 passed in 33.05s


terminal

git status --short && git add autoshorts/rendering/scene_backgrounds.py autoshorts/rendering/motion_renderer.py tests/test_scene_backgrounds.py tests/test_motion_renderer.py README.md docs/final_motion_prototype.md && git commit -m "feat: add scene-specific motion backgrounds" && git status --short && git log --oneline -5


Output:
M README.md
 M autoshorts/rendering/motion_renderer.py
 M docs/final_motion_prototype.md
 M tests/test_motion_renderer.py
?? autoshorts/rendering/scene_backgrounds.py
?? tests/test_scene_backgrounds.py
[feat/core-approval-quality-gates db200ae] feat: add scene-specific motion backgrounds
 6 files changed, 187 insertions(+), 12 deletions(-)
 create mode 100644 autoshorts/rendering/scene_backgrounds.py
 create mode 100644 tests/test_scene_backgrounds.py
db200ae feat: add scene-specific motion backgrounds
14310f9 feat: add TrueTrace final motion prototype
7375311 feat: make TrueTrace pilot beginner friendly
b9b6423 feat: align TrueTrace voiceover previews
3f68001 feat: add EdgeTTS audio caption preview


Erledigt, Sir.

## Weitergebaut: Scene-specific ChatGPT Image Backgrounds

Ich habe den Motion-Prototypen von **einem generischen Hintergrund** auf **5 szenenspezifische ChatGPT Image/Codex Backgrounds** erweitert.

Neuer Commit:

```text
db200ae feat: add scene-specific motion backgrounds
```

## Neu gebaut

### Neues Modul

```text
autoshorts/rendering/scene_backgrounds.py
```

### Neue Tests

```text
tests/test_scene_backgrounds.py
```

### Erweitert

```text
autoshorts/rendering/motion_renderer.py
```

Der Renderer kann jetzt pro Szene ein eigenes Hintergrundbild verwenden:

```python
background_paths = {
    "clean-demo": ".../clean-demo.png",
    "weird-email": ".../weird-email.png",
    "wrong-routing": ".../wrong-routing.png",
    "three-way-split": ".../three-way-split.png",
    "final-rule": ".../final-rule.png",
}
```

## Was generiert wurde

Ich habe über ChatGPT Image/Codex 5 textfreie Backgrounds erzeugt:

```text
data/generated_backgrounds/truetrace-weird-email-v1/clean-demo.png
data/generated_backgrounds/truetrace-weird-email-v1/weird-email.png
data/generated_backgrounds/truetrace-weird-email-v1/wrong-routing.png
data/generated_backgrounds/truetrace-weird-email-v1/three-way-split.png
data/generated_backgrounds/truetrace-weird-email-v1/final-rule.png
```

Alle Prompts erzwingen:

```text
no readable text
no logos
no watermarks
no UI labels
renderer-owned overlays
```

Also: AI macht Atmosphäre, Renderer macht Wahrheit. So wie es sein sollte.

## Neuer V5 Review Clip

MEDIA:/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/data/previews/truetrace-final-motion-v5/truetrace-ai-automation-quiet-rot-38s/final-motion-audio-caption-preview.mp4

## Technische Verifikation

```text
Video: h264
Size: 540x960
Duration: 36.833s
Audio: aac
Audio duration: 36.864s
Frames: 887
Backgrounds: 5
```

SHA256:

```text
ac8432ef5285abaa8bdb662610184cea491d3b1951dc9b2f2b6dd5a9f4ea9143
```

## Visuelle QA

Ich habe zwei kritische Szenen geprüft:

### Weird Email Scene

Status:

```text
Readable: yes
Renderer text clear: yes
No critical clipping: yes
AI background has no problematic readable fake text/logos
```

### Three-way Split Scene

Status:

```text
Normal → Go through
Unclear → Review
Weird/Risky → Stop
```

Ist lesbar, die Pfeile liegen hinter den Labels, und das wirkt jetzt deutlich mehr wie ein Mechanism Reveal statt nur Karten-Slideshow.

## Tests

Full Suite:

```text
168 passed in 33.05s
```

## Aktueller Verlauf

```text
db200ae feat: add scene-specific motion backgrounds
14310f9 feat: add TrueTrace final motion prototype
7375311 feat: make TrueTrace pilot beginner friendly
b9b6423 feat: align TrueTrace voiceover previews
3f68001 feat: add EdgeTTS audio caption preview
```

## Nächster sinnvoller Schritt

Jetzt würde ich als nächstes **Caption/Subtitle Timing und Layout polieren**, weil der Clip visuell langsam brauchbar wird.

Konkret:

```text
1. Subtitle chunks bei der 3-way-split Szene kürzer machen
2. Subtitles etwas höher/kompakter setzen
3. Key scene text nicht mit subtitles konkurrieren lassen
4. Danach 1080x1920 Export erzeugen
```

Dann hätten wir einen ernsthaften First-Public-Candidate statt nur einen technischen Prototypen mit Hut.


[M G] Ok, mach das.


Explanation:
**Inspecting tests for audio subtitles**

I need to check the audio subtitles and the final preview to ensure everything is functioning correctly. This might involve looking closely at how well the subtitles align with the audio and if there are any discrepancies. It's important to make sure that viewers can follow along without confusion. I'll go through each segment carefully to make sure everything matches up as intended. Let's ensure quality control here!


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.

### Media / Rendering Pipeline Boundaries

When building toward image/video generation or renderer integration, add deterministic planning artifacts before any GPU/API work:

1. Write tests for the wished-for pure API first, e.g. `create_shot_plan(script)`, `build_render_manifest(plan)`, `build_local_preview_plan(manifest)`, or `build_ffmpeg_concat_plan(plan, frames, output_path)`.
2. RED should be a missing module/API or missing validation behavior — never a failed ComfyUI/Wan/Qwen/API invocation.
3. Keep each stage inspectable and side-effect-free until the explicit adapter layer: `ScriptDraft -> ShotPlan -> RenderManifest -> PreviewPlan/PreviewArtifact -> renderer adapter`.
4. Assert stable IDs/order, scene counts, timing continuity, vertical settings (`1080x1920`, `30fps` when building Shorts/Reels/TikTok), reduced preview settings (`540x960`, `24fps` when building fast local timing previews), and placeholder/runtime paths before generating media.
5. Add guardrail tests that reject static slideshows, baked-in/generated text inside assets, non-renderer-owned captions/labels, unsafe render settings, missing fields, bad timings, non-PNG frame paths, non-MP4 output paths, and mismatched frame counts.
6. Split artifact-writing/rendering into explicit boundaries after the pure plans are green: frame writer returns `side_effects=("write_png",)`, concat planning remains side-effect-free, and the FFmpeg renderer returns `side_effects=("write_concat", "call_ffmpeg", "write_mp4")` plus `external_calls=("ffmpeg",)`.
7. Only after the manifest/preview boundary is green should a later adapter call local GPU tools or external services. Planning first; GPU roulette later.

This prevents wasting render time on unclear direction and keeps expensive media generation behind a small, audited boundary.

### Approval-First Media/Social Pipelines

When a feature crosses from content planning toward media/social delivery, prefer many small RED-GREEN-README-COMMIT slices instead of one large integration jump:

1. typed artifact/model (`SocialMediaDraft`, `RenderManifest`, etc.)
2. renderer/formatter for human review text
3. side-effect-free delivery/preparation boundary
4. demo CLI that prints metadata first, then payload

For each slice, write the missing import/API test first, verify the expected import/API failure, implement only that slice, run the focused test, run the full suite, update docs, and commit. Keep invariant tests explicit: `requires_human_approval=True`, `side_effects=()`, no external calls, no credential access, no file writes unless that adapter is the explicit feature, and no accidental action language such as `publish`/`post` in review/delivery text.

This staged approach keeps approval semantics auditable and prevents a later renderer, Telegram sender, or platform adapter from being smuggled into formatting code.

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


File not found: /home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_audio_subtitles.py


     1|from pathlib import Path
     2|import subprocess
     3|
     4|from autoshorts.audio.final_preview import (
     5|    AudioCaptionPreviewPlan,
     6|    build_audio_caption_preview_plan,
     7|    render_audio_caption_preview,
     8|    validate_audio_caption_preview_plan,
     9|)
    10|from autoshorts.audio.subtitles import build_hormozi_subtitle_track
    11|
    12|
    13|def test_build_audio_caption_preview_plan_is_side_effect_free_and_writes_ass_content(tmp_path):
    14|    video_path = tmp_path / "timing.mp4"
    15|    audio_path = tmp_path / "voice.mp3"
    16|    track = build_hormozi_subtitle_track("Most AI automations quietly rot when workflows lack boundaries", total_duration_seconds=5)
    17|
    18|    plan = build_audio_caption_preview_plan(
    19|        video_path=video_path,
    20|        audio_path=audio_path,
    21|        subtitle_track=track,
    22|        output_path=tmp_path / "final-preview.mp4",
    23|    )
    24|
    25|    assert plan.video_path == str(video_path)
    26|    assert plan.audio_path == str(audio_path)
    27|    assert plan.output_path.endswith("final-preview.mp4")
    28|    assert plan.subtitle_file_path.endswith("final-preview.ass")
    29|    assert plan.duration_mode == "preserve_video"
    30|    assert "[Script Info]" in plan.subtitle_file_content
    31|    assert "Dialogue:" in plan.subtitle_file_content
    32|    assert "MOST AI AUTOMATIONS" in plan.subtitle_file_content
    33|    assert plan.side_effects == ()
    34|    assert plan.external_calls == ()
    35|
    36|
    37|def test_validate_audio_caption_preview_plan_rejects_bad_paths_and_unsafe_side_effects(tmp_path):
    38|    track = build_hormozi_subtitle_track("valid subtitle text", total_duration_seconds=2)
    39|    plan = AudioCaptionPreviewPlan(
    40|        video_path="video.mov",
    41|        audio_path="voice.wav",
    42|        output_path="out.avi",
    43|        subtitle_file_path="out.srt",
    44|        subtitle_file_content="",
    45|        subtitle_track=track,
    46|        duration_mode="shortest",
    47|        side_effects=("write_file",),
    48|        external_calls=("ffmpeg",),
    49|    )
    50|
    51|    validation = validate_audio_caption_preview_plan(plan)
    52|
    53|    assert validation.is_valid is False
    54|    assert "video_path must end with .mp4" in validation.errors
    55|    assert "audio_path must end with .mp3" in validation.errors
    56|    assert "output_path must end with .mp4" in validation.errors
    57|    assert "subtitle_file_path must end with .ass" in validation.errors
    58|    assert "duration_mode must be preserve_video or trim_to_audio" in validation.errors
    59|    assert "audio caption preview plans must not have side effects" in validation.errors
    60|
    61|
    62|def test_build_audio_caption_preview_plan_can_explicitly_trim_to_audio(tmp_path):
    63|    track = build_hormozi_subtitle_track("Prompt is not system workflow boundaries matter", total_duration_seconds=4)
    64|
    65|    plan = build_audio_caption_preview_plan(
    66|        video_path=tmp_path / "timing.mp4",
    67|        audio_path=tmp_path / "voice.mp3",
    68|        subtitle_track=track,
    69|        output_path=tmp_path / "final.mp4",
    70|        duration_mode="trim_to_audio",
    71|    )
    72|
    73|    assert plan.duration_mode == "trim_to_audio"
    74|    assert plan.side_effects == ()
    75|    assert plan.external_calls == ()
    76|
    77|
    78|def test_render_audio_caption_preview_preserves_video_duration_by_default(tmp_path):
    79|    input_video = tmp_path / "timing.mp4"
    80|    audio = tmp_path / "voice.mp3"
    81|    output = tmp_path / "final.mp4"
    82|    input_video.write_bytes(b"fake video")
    83|    audio.write_bytes(b"fake audio")
    84|    track = build_hormozi_subtitle_track("Prompt is not system workflow boundaries matter", total_duration_seconds=4)
    85|    plan = build_audio_caption_preview_plan(
    86|        video_path=input_video,
    87|        audio_path=audio,
    88|        subtitle_track=track,
    89|        output_path=output,
    90|    )
    91|    calls = []
    92|
    93|    def fake_runner(command, *, capture_output, text, check):
    94|        calls.append(command)
    95|        output.write_bytes(b"fake final video")
    96|        return subprocess.CompletedProcess(command, 0, stdout="ok", stderr="")
    97|
    98|    result = render_audio_caption_preview(plan, runner=fake_runner)
    99|
   100|    assert Path(plan.subtitle_file_path).exists()
   101|    assert output.exists()
   102|    assert calls[0][:5] == ["ffmpeg", "-y", "-i", str(input_video), "-i"]
   103|    assert "ass=" in " ".join(calls[0])
   104|    assert "-shortest" not in calls[0]
   105|    assert result.output_path == str(output)
   106|    assert result.duration_mode == "preserve_video"
   107|    assert result.side_effects == ("write_subtitles", "call_ffmpeg", "write_mp4")
   108|    assert result.external_calls == ("ffmpeg",)
   109|
   110|
   111|def test_render_audio_caption_preview_trims_only_when_explicit(tmp_path):
   112|    input_video = tmp_path / "timing.mp4"
   113|    audio = tmp_path / "voice.mp3"
   114|    output = tmp_path / "final.mp4"
   115|    input_video.write_bytes(b"fake video")
   116|    audio.write_bytes(b"fake audio")
   117|    track = build_hormozi_subtitle_track("Prompt is not system workflow boundaries matter", total_duration_seconds=4)
   118|    plan = build_audio_caption_preview_plan(
   119|        video_path=input_video,
   120|        audio_path=audio,
   121|        subtitle_track=track,
   122|        output_path=output,
   123|        duration_mode="trim_to_audio",
   124|    )
   125|    calls = []
   126|
   127|    def fake_runner(command, *, capture_output, text, check):
   128|        calls.append(command)
   129|        output.write_bytes(b"fake final video")
   130|        return subprocess.CompletedProcess(command, 0, stdout="ok", stderr="")
   131|
   132|    result = render_audio_caption_preview(plan, runner=fake_runner)
   133|
   134|    assert "-shortest" in calls[0]
   135|    assert result.duration_mode == "trim_to_audio"
   136|


Explanation:
**Searching for subtitle tests**

I need to look for a subtitle tests file, possibly named "test_subtitle_tts." I wonder if that's the right filename. It sounds like a good place to start. I'll have to make sure I'm searching in the right locations to find this file quickly. If I can locate it, that should help me with whatever I'm working on. Let's see if I can track it down!


patch

from autoshorts.audio.subtitles import build_hormozi_subtitle_track

from autoshorts.audio.subtitles import build_final_motion_subtitle_track, build_hormozi_subtitle_track


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_audio_caption_preview.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_audio_caption_preview.py
@@ -7,7 +7,7 @@
     render_audio_caption_preview,
     validate_audio_caption_preview_plan,
 )
-from autoshorts.audio.subtitles import build_hormozi_subtitle_track
+from autoshorts.audio.subtitles import build_final_motion_subtitle_track, build_hormozi_subtitle_track

 def test_build_audio_caption_preview_plan_is_side_effect_free_and_writes_ass_content(tmp_path):

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_audio_caption_preview.py">
ERROR [10:40] "build_final_motion_subtitle_track" is unknown import symbol [reportAttributeAccessIssue] (Pyright)
</diagnostics>