from autoshorts.audio.tts_plan import (
    TTSProviderPlan,
    build_default_truetrace_tts_plan,
    build_tts_ab_test_plan,
    validate_tts_provider_plan,
)

def test_build_default_truetrace_tts_plan_uses_edge_tts_without_gpu_or_external_api_key():
    plan = build_default_truetrace_tts_plan()

    assert plan.provider == "edge_tts"
    assert plan.voice == "en-GB-RyanNeural"
    assert plan.language == "en"
    assert plan.requires_local_gpu is False
    assert plan.blocks_video_gpu is False
    assert plan.requires_api_key is False
    assert plan.role == "voice_generation"
    assert plan.side_effects == ()
    assert plan.external_calls == ()

def test_build_tts_ab_test_plan_keeps_magpie_hosted_and_whisper_as_alignment_only():
    plan = build_tts_ab_test_plan()

    assert [candidate.provider for candidate in plan.candidates] == [
        "edge_tts",
        "nvidia_magpie_tts_multilingual",
        "whisper_alignment",
    ]
    magpie = plan.candidates[1]
    whisper = plan.candidates[2]
    assert magpie.requires_local_gpu is False
    assert magpie.blocks_video_gpu is False
    assert magpie.requires_api_key is True
    assert magpie.role == "voice_generation"
    assert whisper.role == "alignment_qa"
    assert whisper.provider != "tts"
    assert whisper.blocks_video_gpu is False
    assert plan.recommended_default_provider == "edge_tts"

def test_validate_tts_provider_plan_rejects_whisper_as_voice_generation_and_gpu_blocking_default():
    bad_whisper = TTSProviderPlan(
        provider="whisper",
        voice="base",
        language="en",
        role="voice_generation",
        requires_local_gpu=False,
        blocks_video_gpu=False,
        requires_api_key=False,
    )
    bad_gpu_default = TTSProviderPlan(
        provider="local_gpu_tts",
        voice="some-voice",
        language="en",
        role="voice_generation",
        requires_local_gpu=True,
        blocks_video_gpu=True,
        requires_api_key=False,
        default_for_phase="phase_1",
    )

    whisper_validation = validate_tts_provider_plan(bad_whisper)
    gpu_validation = validate_tts_provider_plan(bad_gpu_default)

    assert whisper_validation.is_valid is False
    assert "Whisper is STT/alignment, not TTS" in whisper_validation.errors
    assert gpu_validation.is_valid is False
    assert "phase-1 TTS must not block the local video GPU" in gpu_validation.errors


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


terminal

pytest tests/test_subtitle_cues.py tests/test_tts_plan.py -q


Output:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_subtitle_cues.py _________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_subtitle_cues.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_subtitle_cues.py:1: in <module>
    from autoshorts.audio.subtitles import (
E   ModuleNotFoundError: No module named 'autoshorts.audio'
___________________ ERROR collecting tests/test_tts_plan.py ____________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_tts_plan.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_tts_plan.py:1: in <module>
    from autoshorts.audio.tts_plan import (
E   ModuleNotFoundError: No module named 'autoshorts.audio'
=========================== short test summary info ============================
ERROR tests/test_subtitle_cues.py
ERROR tests/test_tts_plan.py
!!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!!!
2 errors in 0.13s


"""Audio planning primitives for voiceover and subtitles."""


"""Deterministic renderer-owned subtitle cue planning."""

from __future__ import annotations

import re
from dataclasses import dataclass, replace

from autoshorts.ideas.candidate import ValidationResult

_WORD_RE = re.compile(r"[A-Za-z0-9]+(?:['-][A-Za-z0-9]+)?")

@dataclass(frozen=True)
class SubtitleStyle:
    name: str
    primary_color: str
    accent_color: str
    stroke_color: str
    shadow_color: str
    placement: str
    max_lines: int
    min_words_per_cue: int
    max_words_per_cue: int
    safe_zone_bottom_percent: int
    font_weight: str

    @classmethod
    def hormozi_inspired(cls) -> "SubtitleStyle":
        """Return the TrueTrace kinetic-caption baseline: inspired mechanics, original brand skin."""

        return cls(
            name="truetrace_hormozi_inspired",
            primary_color="#FFFFFF",
            accent_color="#22D3EE",
            stroke_color="#050816",
            shadow_color="#000000",
            placement="lower_middle_above_ui_safe_zone",
            max_lines=2,
            min_words_per_cue=2,
            max_words_per_cue=5,
            safe_zone_bottom_percent=14,
            font_weight="800",
        )

@dataclass(frozen=True)
class SubtitleCue:
    text: str
    start_ms: int
    end_ms: int
    active_word: str
    renderer_owned: bool = True
    max_lines: int = 2
    emphasis_color: str = "#22D3EE"

@dataclass(frozen=True)
class SubtitleTrack:
    cues: tuple[SubtitleCue, ...]
    style: SubtitleStyle
    renderer_owned: bool = True
    side_effects: tuple[str, ...] = ()
    external_calls: tuple[str, ...] = ()

    def with_cues(self, cues: tuple[SubtitleCue, ...]) -> "SubtitleTrack":
        return replace(self, cues=cues)

def _words(text: str) -> list[str]:
    return _WORD_RE.findall(text)

def _chunk_words(words: list[str], *, min_words: int, max_words: int) -> list[list[str]]:
    chunks: list[list[str]] = []
    index = 0
    while index < len(words):
        remaining = len(words) - index
        take = min(max_words, remaining)
        if 0 < remaining < min_words and chunks:
            chunks[-1].extend(words[index:])
            break
        if remaining == min_words + 1:
            take = min_words
        chunks.append(words[index : index + take])
        index += take
    return chunks

def _active_word(chunk: list[str]) -> str:
    # Prefer the longest meaningful word so the highlighted token carries semantic weight.
    return max((word.upper() for word in chunk), key=lambda word: (len(word), word))

def build_hormozi_subtitle_track(text: str, *, total_duration_seconds: int) -> SubtitleTrack:
    """Create continuous 2..5-word renderer-owned subtitle cues for final burn-in."""

    if not text.strip():
        raise ValueError("subtitle text is required")
    if total_duration_seconds <= 0:
        raise ValueError("total_duration_seconds must be positive")

    style = SubtitleStyle.hormozi_inspired()
    words = _words(text)
    if len(words) < style.min_words_per_cue:
        raise ValueError("subtitle text must contain at least two words")

    chunks = _chunk_words(words, min_words=style.min_words_per_cue, max_words=style.max_words_per_cue)
    total_ms = total_duration_seconds * 1000
    cues: list[SubtitleCue] = []
    for index, chunk in enumerate(chunks):
        start_ms = round(index * total_ms / len(chunks))
        end_ms = round((index + 1) * total_ms / len(chunks))
        cues.append(
            SubtitleCue(
                text=" ".join(word.upper() for word in chunk),
                start_ms=start_ms,
                end_ms=end_ms,
                active_word=_active_word(chunk),
                renderer_owned=True,
                max_lines=style.max_lines,
                emphasis_color=style.accent_color,
            )
        )

    track = SubtitleTrack(cues=tuple(cues), style=style)
    validation = validate_subtitle_track(track)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))
    return track

def validate_subtitle_track(track: SubtitleTrack) -> ValidationResult:
    errors: list[str] = []
    if not track.renderer_owned:
        errors.append("subtitle track must be renderer-owned")
    if track.side_effects:
        errors.append("subtitle track must not have side effects")
    if track.external_calls:
        errors.append("subtitle track must not have external calls")
    if not track.cues:
        errors.append("at least one subtitle cue is required")

    previous_end: int | None = None
    for index, cue in enumerate(track.cues, start=1):
        cue_words = cue.text.split()
        if not cue.renderer_owned:
            errors.append(f"subtitle cue {index} must be renderer-owned")
        if not (track.style.min_words_per_cue <= len(cue_words) <= track.style.max_words_per_cue):
            errors.append(f"subtitle cue {index} must contain 2..5 words")
        if cue.end_ms <= cue.start_ms:
            errors.append(f"subtitle cue {index} end_ms must be greater than start_ms")
        if previous_end is None:
            if cue.start_ms != 0:
                errors.append("first subtitle cue must start at 0ms")
        elif cue.start_ms != previous_end:
            errors.append(f"subtitle cue {index} must start when the previous cue ends")
        if not cue.active_word.strip():
            errors.append(f"subtitle cue {index} active_word is required")
        elif cue.active_word not in {word.upper() for word in cue_words}:
            errors.append(f"subtitle cue {index} active_word must appear in cue text")
        if cue.max_lines > track.style.max_lines:
            errors.append(f"subtitle cue {index} exceeds style max lines")
        previous_end = cue.end_ms

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


"""Side-effect-free TTS provider planning for TrueTrace videos."""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.ideas.candidate import ValidationResult

@dataclass(frozen=True)
class TTSProviderPlan:
    provider: str
    voice: str
    language: str
    role: str
    requires_local_gpu: bool
    blocks_video_gpu: bool
    requires_api_key: bool
    default_for_phase: str = ""
    notes: str = ""
    side_effects: tuple[str, ...] = ()
    external_calls: tuple[str, ...] = ()

@dataclass(frozen=True)
class TTSABTestPlan:
    candidates: tuple[TTSProviderPlan, ...]
    recommended_default_provider: str
    side_effects: tuple[str, ...] = ()
    external_calls: tuple[str, ...] = ()

def build_default_truetrace_tts_plan() -> TTSProviderPlan:
    """Return the phase-1 baseline voice plan: English EdgeTTS, no local GPU contention."""

    plan = TTSProviderPlan(
        provider="edge_tts",
        voice="en-GB-RyanNeural",
        language="en",
        role="voice_generation",
        requires_local_gpu=False,
        blocks_video_gpu=False,
        requires_api_key=False,
        default_for_phase="phase_1",
        notes="Baseline voice for first TrueTrace finals; keeps local GPU reserved for image/video.",
    )
    validation = validate_tts_provider_plan(plan)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))
    return plan

def build_tts_ab_test_plan() -> TTSABTestPlan:
    """Plan a later voice comparison without making API calls or consuming GPU."""

    candidates = (
        build_default_truetrace_tts_plan(),
        TTSProviderPlan(
            provider="nvidia_magpie_tts_multilingual",
            voice="default_multilingual",
            language="en",
            role="voice_generation",
            requires_local_gpu=False,
            blocks_video_gpu=False,
            requires_api_key=True,
            default_for_phase="ab_test",
            notes="Hosted NVIDIA Build/API Catalog candidate; test quality/latency/terms before defaulting.",
        ),
        TTSProviderPlan(
            provider="whisper_alignment",
            voice="n/a",
            language="en",
            role="alignment_qa",
            requires_local_gpu=False,
            blocks_video_gpu=False,
            requires_api_key=False,
            default_for_phase="qa_later",
            notes="Whisper is speech-to-text for subtitle alignment and QA, not voice generation.",
        ),
    )
    errors: list[str] = []
    for candidate in candidates:
        errors.extend(validate_tts_provider_plan(candidate).errors)
    if errors:
        raise ValueError("; ".join(errors))
    return TTSABTestPlan(candidates=candidates, recommended_default_provider="edge_tts")

def validate_tts_provider_plan(plan: TTSProviderPlan) -> ValidationResult:
    errors: list[str] = []
    if not plan.provider.strip():
        errors.append("provider is required")
    if not plan.voice.strip():
        errors.append("voice is required")
    if plan.language != "en":
        errors.append("TrueTrace final voice language must be en")
    if plan.role not in {"voice_generation", "alignment_qa"}:
        errors.append("role must be voice_generation or alignment_qa")
    if plan.provider in {"whisper", "whisper_alignment"} and plan.role == "voice_generation":
        errors.append("Whisper is STT/alignment, not TTS")
    if plan.default_for_phase == "phase_1" and plan.blocks_video_gpu:
        errors.append("phase-1 TTS must not block the local video GPU")
    if plan.requires_local_gpu and plan.blocks_video_gpu and plan.default_for_phase == "phase_1":
        errors.append("phase-1 local GPU TTS is not allowed while video generation needs the GPU")
    if plan.side_effects:
        errors.append("TTS plan must not have side effects")
    if plan.external_calls:
        errors.append("TTS plan must not have external calls")
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


Output:
.......                                                                  [100%]
7 passed in 0.05s


Explanation:
**Updating documentation strategy**

I’m thinking about updating the docs and README to include integration with the preview. The user mentioned to continue, which might be enough. I could create some documentation for that. Additionally, maybe I should add a demo summary at the top of the previews, which could include subtitles and TTS. For now, at least updating the docs related to subtitle TTS strategy and the README sounds like a solid plan. I’ll check the relevant parts.


     1|# Subtitle and TTS Strategy
     2|
     3|Date: 2026-05-24
     4|
     5|## Decision summary
     6|
     7|For phase 1, subtitles should be deterministic renderer-owned captions in a high-retention Hormozi-inspired style, not burned into AI-generated images or video clips.
     8|
     9|Recommended TTS path for now:
    10|
    11|```text
    12|EdgeTTS for first English final voice tests
    13|→ optional NVIDIA Magpie A/B test later
    14|→ Whisper only for alignment/transcription, not as TTS
    15|```
    16|
    17|Reasoning:
    18|
    19|- EdgeTTS is fast, free, no GPU, no API key, and leaves the GPU free for ComfyUI/Wan/Qwen video/image work.
    20|- NVIDIA Magpie TTS may be worth testing for voice quality, but it is an external API dependency and not yet proven better for our exact Shorts style.
    21|- Whisper is speech-to-text, not text-to-speech. It can align/generated-audio transcripts later, but it does not solve voice generation.
    22|- Local neural TTS on GPU is not attractive while the same GPU is needed for video generation.
    23|
    24|## Subtitle style
    25|
    26|Use a `Hormozi-inspired`, not `Hormozi-cloned`, subtitle system:
    27|
    28|- 2-5 words per subtitle chunk;
    29|- active word highlight;
    30|- strong contrast;
    31|- center-lower safe-zone placement;
    32|- occasional emphasis words in accent color;
    33|- hard line breaks controlled by renderer;
    34|- no baked text inside AI-generated images/clips;
    35|- no copyrighted creator-template dependency;
    36|- captions remain editable until final render.
    37|
    38|Default visual rules:
    39|
    40|```text
    41|Font: bold sans-serif
    42|Placement: lower-middle, above platform UI safe zone
    43|Primary color: white
    44|Stroke/shadow: black/dark outline
    45|Accent: TrueTrace green/blue
    46|Max lines: 2
    47|Max words per chunk: 5
    48|Chunk duration: roughly 0.7-1.6s
    49|```
    50|
    51|## Caption pipeline
    52|
    53|Preferred deterministic pipeline:
    54|
    55|```text
    56|Script voiceover text
    57|→ caption chunker
    58|→ timed subtitle cues
    59|→ renderer-owned caption layer
    60|→ FFmpeg burn-in for final MP4
    61|```
    62|
    63|Later, after real TTS is added:
    64|
    65|```text
    66|Final script
    67|→ TTS audio
    68|→ optional Whisper alignment/transcript QA
    69|→ corrected subtitle cues
    70|→ final renderer burn-in
    71|```
    72|
    73|Important: Whisper is useful for checking/aligning what the TTS actually said, but it should not own the script. The source of truth remains the approved script.
    74|
    75|## TTS options
    76|
    77|### EdgeTTS
    78|
    79|Use for phase 1 baseline.
    80|
    81|Pros:
    82|
    83|- no GPU;
    84|- free/no key;
    85|- quick iteration;
    86|- predictable enough;
    87|- available voices;
    88|- keeps video GPU free.
    89|
    90|Cons:
    91|
    92|- common synthetic sound;
    93|- less distinctive brand voice;
    94|- commercial/licensing terms should be reviewed before scaling monetized production.
    95|
    96|Suggested English voices to A/B later:
    97|
    98|- `en-US-GuyNeural` — clear male baseline;
    99|- `en-US-ChristopherNeural` — more mature male;
   100|- `en-US-AriaNeural` — polished female baseline;
   101|- `en-GB-RyanNeural` — British male, potentially fitting precise/dry tone.
   102|
   103|### NVIDIA Magpie multilingual TTS
   104|
   105|Use as an A/B candidate, not default yet.
   106|
   107|Pros:
   108|
   109|- likely higher modern neural quality than commodity voices;
   110|- multilingual support;
   111|- cloud-side, so it should not consume local video GPU.
   112|
   113|Cons:
   114|
   115|- external API dependency;
   116|- cost/rate limits/terms unknown until tested;
   117|- voice/style fit unknown;
   118|- not integrated into the current deterministic pipeline;
   119|- may create operational coupling to build.nvidia.com.
   120|
   121|Recommendation: run a short 15-20 second voice A/B sample against EdgeTTS before making it default.
   122|
   123|### Local TTS
   124|
   125|Not recommended as default for phase 1 if it needs GPU.
   126|
   127|Use only if:
   128|
   129|- it can run CPU-fast enough; or
   130|- it runs on a separate machine; or
   131|- video generation is idle; or
   132|- it gives a clearly superior branded voice.
   133|
   134|The local GPU should be reserved for ComfyUI/Wan/Qwen video/image jobs when those are active.
   135|
   136|### Whisper
   137|
   138|Whisper is not a TTS model.
   139|
   140|Use later for:
   141|
   142|- checking TTS output text;
   143|- subtitle timing alignment;
   144|- transcript QA;
   145|- detecting missing/garbled words.
   146|
   147|Avoid using Whisper as a required step for every preview until the audio pipeline exists.
   148|
   149|## Phase plan
   150|
   151|### Phase 1A: Current preview layer
   152|
   153|- German approval preview cards only.
   154|- No final TTS required.
   155|- Captions are card/caption beats for concept review.
   156|
   157|### Phase 1B: First final-video audio test
   158|
   159|- Use EdgeTTS English voice baseline.
   160|- Generate deterministic subtitle cues from the approved script.
   161|- Burn captions in renderer/FFmpeg.
   162|- Send preview with audio + captions for approval.
   163|
   164|### Phase 1C: Voice A/B
   165|
   166|Create the same 15-20 second script using:
   167|
   168|1. EdgeTTS baseline;
   169|2. NVIDIA Magpie if API access is configured;
   170|3. optional local CPU/offline TTS if available.
   171|
   172|Compare:
   173|
   174|- clarity;
   175|- retention energy;
   176|- pronunciation of AI/workflow terms;
   177|- audio artifacts;
   178|- latency/cost;
   179|- operational reliability.
   180|
   181|### Phase 2: Alignment QA
   182|
   183|- Add Whisper transcription/alignment after TTS.
   184|- Flag mismatches between approved script and spoken audio.
   185|- Adjust subtitle cues if the TTS timing differs.
   186|
   187|## Current recommendation
   188|
   189|Start with:
   190|
   191|```text
   192|Subtitles: deterministic Hormozi-inspired renderer-owned captions
   193|TTS: EdgeTTS English baseline
   194|Magpie: A/B test later
   195|Whisper: alignment/QA later, not TTS
   196|GPU: reserve for video/image generation
   197|```
   198|
   199|This is the lowest-friction path and avoids blocking the video pipeline on voice experiments. Voice quality matters, but first we need to prove that the concepts and visuals are worth voicing. A beautiful voice reading weak content is merely expensive politeness.
   200|


patch

- `autoshorts.demo.truetrace_preview_packages` — renders top-3 TrueTrace local MP4 timing previews plus approval/manual upload packages.
- `autoshorts.ideas.candidate` — structured video candidate model and validation.

- `autoshorts.demo.truetrace_preview_packages` — renders top-3 TrueTrace local MP4 timing previews plus approval/manual upload packages.
- `autoshorts.audio.subtitles` — deterministic Hormozi-inspired, renderer-owned subtitle cue planning with active-word metadata.
- `autoshorts.audio.tts_plan` — side-effect-free TTS provider planning: EdgeTTS phase-1 baseline, Magpie A/B candidate, Whisper alignment-only.
- `autoshorts.ideas.candidate` — structured video candidate model and validation.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -21,6 +21,8 @@
 - `autoshorts.demo.sample_batch` — deterministic AI-life-systems review demo with ready, blocked, and invalid candidates.
 - `autoshorts.demo.truetrace_concepts` — five concrete TrueTraceShorts concept candidates with metadata for strategy review.
 - `autoshorts.demo.truetrace_preview_packages` — renders top-3 TrueTrace local MP4 timing previews plus approval/manual upload packages.
+- `autoshorts.audio.subtitles` — deterministic Hormozi-inspired, renderer-owned subtitle cue planning with active-word metadata.
+- `autoshorts.audio.tts_plan` — side-effect-free TTS provider planning: EdgeTTS phase-1 baseline, Magpie A/B candidate, Whisper alignment-only.
 - `autoshorts.ideas.candidate` — structured video candidate model and validation.
 - `autoshorts.ideas.factory` — draft-to-candidate factory with stable ids, default hashtags, and duration classes.
 - `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.

No linter for .md files

/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md was last read with offset/limit pagination (partial view). Re-read the whole file before overwriting it.


## Implemented planning modules

Current code-level planning boundary:

- `autoshorts.audio.subtitles.build_hormozi_subtitle_track()` creates continuous 2-5-word subtitle cues with active-word metadata, lower-middle safe-zone style, no side effects, and no external calls.
- `autoshorts.audio.subtitles.validate_subtitle_track()` rejects non-renderer-owned cues, overcrowded captions, bad timing, and missing active words.
- `autoshorts.audio.tts_plan.build_default_truetrace_tts_plan()` selects `edge_tts` with `en-GB-RyanNeural` for phase 1.
- `autoshorts.audio.tts_plan.build_tts_ab_test_plan()` keeps NVIDIA Magpie as a hosted A/B candidate and Whisper as `alignment_qa`, not voice generation.

## Caption pipeline

Preferred deterministic pipeline:

```text
Script voiceover text
→ caption chunker
→ timed subtitle cues
→ renderer-owned caption layer
→ FFmpeg burn-in for final MP4
```


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/docs/subtitle_tts_strategy.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/docs/subtitle_tts_strategy.md
@@ -48,6 +48,15 @@
 Chunk duration: roughly 0.7-1.6s
 ```

+## Implemented planning modules
+
+Current code-level planning boundary:
+
+- `autoshorts.audio.subtitles.build_hormozi_subtitle_track()` creates continuous 2-5-word subtitle cues with active-word metadata, lower-middle safe-zone style, no side effects, and no external calls.
+- `autoshorts.audio.subtitles.validate_subtitle_track()` rejects non-renderer-owned cues, overcrowded captions, bad timing, and missing active words.
+- `autoshorts.audio.tts_plan.build_default_truetrace_tts_plan()` selects `edge_tts` with `en-GB-RyanNeural` for phase 1.
+- `autoshorts.audio.tts_plan.build_tts_ab_test_plan()` keeps NVIDIA Magpie as a hosted A/B candidate and Whisper as `alignment_qa`, not voice generation.
+
 ## Caption pipeline

 Preferred deterministic pipeline:

No linter for .md files


terminal

Output:
........................................................................ [ 48%]
........................................................................ [ 96%]
.....                                                                    [100%]
149 passed in 31.97s


git status --short && git diff -- autoshorts/audio/__init__.py autoshorts/audio/subtitles.py autoshorts/audio/tts_plan.py tests/test_subtitle_cues.py tests/test_tts_plan.py README.md docs/subtitle_tts_strategy.md | sed -n '1,260p'


Output:
M README.md
 M docs/subtitle_tts_strategy.md
?? autoshorts/audio/
?? tests/test_subtitle_cues.py
?? tests/test_tts_plan.py
diff --git a/README.md b/README.md
index 3c8ae71..e5cfe9e 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,8 @@ Implemented foundation:
 - `autoshorts.demo.sample_batch` — deterministic AI-life-systems review demo with ready, blocked, and invalid candidates.
 - `autoshorts.demo.truetrace_concepts` — five concrete TrueTraceShorts concept candidates with metadata for strategy review.
 - `autoshorts.demo.truetrace_preview_packages` — renders top-3 TrueTrace local MP4 timing previews plus approval/manual upload packages.
+- `autoshorts.audio.subtitles` — deterministic Hormozi-inspired, renderer-owned subtitle cue planning with active-word metadata.
+- `autoshorts.audio.tts_plan` — side-effect-free TTS provider planning: EdgeTTS phase-1 baseline, Magpie A/B candidate, Whisper alignment-only.
 - `autoshorts.ideas.candidate` — structured video candidate model and validation.
 - `autoshorts.ideas.factory` — draft-to-candidate factory with stable ids, default hashtags, and duration classes.
 - `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
diff --git a/docs/subtitle_tts_strategy.md b/docs/subtitle_tts_strategy.md
index fc6493e..ce0cb81 100644
--- a/docs/subtitle_tts_strategy.md
+++ b/docs/subtitle_tts_strategy.md
@@ -48,6 +48,15 @@ Max words per chunk: 5
 Chunk duration: roughly 0.7-1.6s
 ```

+## Implemented planning modules
+
+Current code-level planning boundary:
+
+- `autoshorts.audio.subtitles.build_hormozi_subtitle_track()` creates continuous 2-5-word subtitle cues with active-word metadata, lower-middle safe-zone style, no side effects, and no external calls.
+- `autoshorts.audio.subtitles.validate_subtitle_track()` rejects non-renderer-owned cues, overcrowded captions, bad timing, and missing active words.
+- `autoshorts.audio.tts_plan.build_default_truetrace_tts_plan()` selects `edge_tts` with `en-GB-RyanNeural` for phase 1.
+- `autoshorts.audio.tts_plan.build_tts_ab_test_plan()` keeps NVIDIA Magpie as a hosted A/B candidate and Whisper as `alignment_qa`, not voice generation.
+
 ## Caption pipeline

 Preferred deterministic pipeline:


Explanation:
**Considering Git Diff Behavior**

I’m realizing that the git diff command doesn’t show untracked files, which may be part of my confusion. It seems I might need to run a different command or adjust my approach. I think it's okay to commit what I've changed, but I want to ensure I'm not missing anything important that isn’t being tracked. It's essential to get this right before moving forward!

git add autoshorts/audio/__init__.py autoshorts/audio/subtitles.py autoshorts/audio/tts_plan.py tests/test_subtitle_cues.py tests/test_tts_plan.py README.md docs/subtitle_tts_strategy.md && git commit -m "feat: add subtitle and tts planning"


Output:
[feat/core-approval-quality-gates 30a8129] feat: add subtitle and tts planning
 7 files changed, 423 insertions(+)
 create mode 100644 autoshorts/audio/__init__.py
 create mode 100644 autoshorts/audio/subtitles.py
 create mode 100644 autoshorts/audio/tts_plan.py
 create mode 100644 tests/test_subtitle_cues.py
 create mode 100644 tests/test_tts_plan.py


Explanation:
**Verifying commit changes**

I’m seeing a commit indicating seven files changed, but I thought we already had seven changes? I need to check if the README was included in this count. It’s a little confusing, so I’ll want to verify the current status and take a look at the logs to clarify what’s happening. Ensuring everything is clean and accounted for is essential before moving forward.

git status --short && git log --oneline -3


Output:
30a8129 feat: add subtitle and tts planning
6efc4dc feat: add TrueTrace top preview packages
2d6af9f feat: add TrueTrace concept testbatch


Explanation:
**Integrating audio planning**

I'm considering whether to continue with another slice by attaching subtitle_track and tts_plan to TrueTracePreviewArtifact. However, this would modify existing tests, so I need to follow TDD. That approach seems valuable, especially since I've already committed. The user asked me to "mach weiter" after a complaint, so I should proceed with integrating audio planning into the preview packages, including both subtitle_track and tts_plan. It looks like I need to write tests and possibly modify the dataclass. Let's get this done!

patch

        assert artifact.package.requires_human_approval is True
        assert artifact.package.approval_language == "de"
        assert artifact.package.final_video_language == "en"
        assert artifact.subtitle_track.renderer_owned is True
        assert artifact.subtitle_track.side_effects == ()
        assert artifact.subtitle_track.external_calls == ()
        assert artifact.subtitle_track.cues[0].start_ms == 0
        assert artifact.subtitle_track.cues[-1].end_ms == artifact.candidate.duration_seconds * 1000
        assert artifact.tts_plan.provider == "edge_tts"
        assert artifact.tts_plan.voice == "en-GB-RyanNeural"
        assert artifact.tts_plan.blocks_video_gpu is False


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_truetrace_preview_packages.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_truetrace_preview_packages.py
@@ -26,6 +26,14 @@
         assert artifact.package.requires_human_approval is True
         assert artifact.package.approval_language == "de"
         assert artifact.package.final_video_language == "en"
+        assert artifact.subtitle_track.renderer_owned is True
+        assert artifact.subtitle_track.side_effects == ()
+        assert artifact.subtitle_track.external_calls == ()
+        assert artifact.subtitle_track.cues[0].start_ms == 0
+        assert artifact.subtitle_track.cues[-1].end_ms == artifact.candidate.duration_seconds * 1000
+        assert artifact.tts_plan.provider == "edge_tts"
+        assert artifact.tts_plan.voice == "en-GB-RyanNeural"
+        assert artifact.tts_plan.blocks_video_gpu is False
         assert artifact.package.side_effects == ()
         assert artifact.package.external_calls == ()
         assert Path(artifact.package.preview_video_path).exists()

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_truetrace_preview_packages.py">
ERROR [29:25] Cannot access attribute "subtitle_track" for class "TrueTracePreviewArtifact"
  Attribute "subtitle_track" is unknown [reportAttributeAccessIssue] (Pyright)
ERROR [30:25] Cannot access attribute "subtitle_track" for class "TrueTracePreviewArtifact"
  Attribute "subtitle_track" is unknown [reportAttributeAccessIssue] (Pyright)
ERROR [31:25] Cannot access attribute "subtitle_track" for class "TrueTracePreviewArtifact"
  Attribute "subtitle_track" is unknown [reportAttributeAccessIssue] (Pyright)
ERROR [32:25] Cannot access attribute "subtitle_track" for class "TrueTracePreviewArtifact"
  Attribute "subtitle_track" is unknown [reportAttributeAccessIssue] (Pyright)
ERROR [33:25] Cannot access attribute "subtitle_track" for class "TrueTracePreviewArtifact"
  Attribute "subtitle_track" is unknown [reportAttributeAccessIssue] (Pyright)
ERROR [34:25] Cannot access attribute "tts_plan" for class "TrueTracePreviewArtifact"
  Attribute "tts_plan" is unknown [reportAttributeAccessIssue] (Pyright)
ERROR [35:25] Cannot access attribute "tts_plan" for class "TrueTracePreviewArtifact"
  Attribute "tts_plan" is unknown [reportAttributeAccessIssue] (Pyright)
ERROR [36:25] Cannot access attribute "tts_plan" for class "TrueTracePreviewArtifact"
  Attribute "tts_plan" is unknown [reportAttributeAccessIssue] (Pyright)
</diagnostics>


    assert text.count("Preview video:") == 3
    assert "Subtitle cues:" in text
    assert "TTS baseline: edge_tts / en-GB-RyanNeural" in text
    assert "Freigabe alle" in text

    assert text.count("Preview video:") == 3
    assert "Freigabe alle" in text


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_truetrace_preview_packages.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_truetrace_preview_packages.py
@@ -65,6 +65,8 @@
     assert "Approval language: de" in text
     assert "Final video language: en" in text
     assert text.count("Preview video:") == 3
+    assert "Subtitle cues:" in text
+    assert "TTS baseline: edge_tts / en-GB-RyanNeural" in text
     assert "Freigabe alle" in text
     assert "Manual upload pack prepared: yes" in text
     assert "publish" not in text.casefold()


terminal

Output:
F.F                                                                      [100%]
=================================== FAILURES ===================================
_ test_build_top_truetrace_preview_packages_renders_three_mp4_approval_packages _

tmp_path = PosixPath('/tmp/pytest-of-agent/pytest-113/test_build_top_truetrace_previ0')

    def test_build_top_truetrace_preview_packages_renders_three_mp4_approval_packages(tmp_path):
        result = build_top_truetrace_preview_packages(output_dir=tmp_path, approval_language="de")

        assert result.batch_id == "truetrace-top3-preview-packages-001"
        assert [artifact.candidate.candidate_id for artifact in result.artifacts] == list(TRUE_TRACE_TOP3_CANDIDATE_IDS)
        assert len(result.artifacts) == 3
        assert result.side_effects == ("write_png", "write_concat", "call_ffmpeg", "write_mp4")
        assert result.external_calls == ("ffmpeg",)

        for artifact in result.artifacts:
            assert artifact.package.requires_human_approval is True
            assert artifact.package.approval_language == "de"
            assert artifact.package.final_video_language == "en"
>           assert artifact.subtitle_track.renderer_owned is True
                   ^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'TrueTracePreviewArtifact' object has no attribute 'subtitle_track'

tests/test_truetrace_preview_packages.py:29: AttributeError
_ test_render_top_truetrace_preview_summary_is_telegram_safe_and_references_three_videos _

tmp_path = PosixPath('/tmp/pytest-of-agent/pytest-113/test_render_top_truetrace_prev0')

    def test_render_top_truetrace_preview_summary_is_telegram_safe_and_references_three_videos(tmp_path):
        result = build_top_truetrace_preview_packages(output_dir=tmp_path, approval_language="de")

        text = render_top_truetrace_preview_summary(result)

        assert "## TrueTrace Top-3 Preview Packages" in text
        assert "Packages: 3" in text
        assert "Approval language: de" in text
        assert "Final video language: en" in text
        assert text.count("Preview video:") == 3
>       assert "Subtitle cues:" in text
E       AssertionError: assert 'Subtitle cues:' in '## TrueTrace Top-3 Preview Packages\nBatch: truetrace-top3-preview-packages-001\nPackages: 3\nApproval language: de\n...epared: yes\nApproval commands: Freigabe YouTube, Freigabe TikTok, Freigabe alle, Ablehnen, Ändern: <requested change>'

tests/test_truetrace_preview_packages.py:68: AssertionError
=========================== short test summary info ============================
FAILED tests/test_truetrace_preview_packages.py::test_build_top_truetrace_preview_packages_renders_three_mp4_approval_packages
FAILED tests/test_truetrace_preview_packages.py::test_render_top_truetrace_preview_summary_is_telegram_safe_and_references_three_videos
2 failed, 1 passed in 23.09s


patch

from pathlib import Path

from autoshorts.audio.subtitles import SubtitleTrack, build_hormozi_subtitle_track
from autoshorts.audio.tts_plan import TTSProviderPlan, build_default_truetrace_tts_plan
from autoshorts.content.script_draft import ScriptDraft

from pathlib import Path

from autoshorts.content.script_draft import ScriptDraft


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
@@ -5,6 +5,8 @@
 from dataclasses import dataclass
 from pathlib import Path

+from autoshorts.audio.subtitles import SubtitleTrack, build_hormozi_subtitle_track
+from autoshorts.audio.tts_plan import TTSProviderPlan, build_default_truetrace_tts_plan
 from autoshorts.content.script_draft import ScriptDraft
 from autoshorts.demo.truetrace_concepts import build_truetrace_concept_batch
 from autoshorts.ideas.candidate import VideoCandidate


class TrueTracePreviewArtifact:
    candidate: VideoCandidate
    script: ScriptDraft
    subtitle_track: SubtitleTrack
    tts_plan: TTSProviderPlan
    draft: SocialMediaDraft

class TrueTracePreviewArtifact:
    candidate: VideoCandidate
    script: ScriptDraft
    draft: SocialMediaDraft


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
@@ -33,6 +33,8 @@
 class TrueTracePreviewArtifact:
     candidate: VideoCandidate
     script: ScriptDraft
+    subtitle_track: SubtitleTrack
+    tts_plan: TTSProviderPlan
     draft: SocialMediaDraft
     package: ContentPreviewApprovalPackage
     manual_posting_pack: ManualPostingPack

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py">
ERROR [132:12] Arguments missing for parameters "subtitle_track", "tts_plan" [reportCallIssue] (Pyright)
</diagnostics>


def _script_from_candidate(candidate: VideoCandidate) -> ScriptDraft:
    return ScriptDraft(


def _top_candidates() -> tuple[VideoCandidate, ...]:
    by_id = {candidate.candidate_id: candidate for candidate in build_truetrace_concept_batch().candidates}
    return tuple(by_id[candidate_id] for candidate_id in TRUE_TRACE_TOP3_CANDIDATE_IDS)

def _voiceover_text(script: ScriptDraft) -> str:
    return " ".join((script.hook, *script.retention_beats, script.cta))

def _build_artifact(candidate: VideoCandidate, *, output_dir: Path, approval_language: str) -> TrueTracePreviewArtifact:

def _top_candidates() -> tuple[VideoCandidate, ...]:
    by_id = {candidate.candidate_id: candidate for candidate in build_truetrace_concept_batch().candidates}
    return tuple(by_id[candidate_id] for candidate_id in TRUE_TRACE_TOP3_CANDIDATE_IDS)

def _build_artifact(candidate: VideoCandidate, *, output_dir: Path, approval_language: str) -> TrueTracePreviewArtifact:


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
@@ -103,6 +103,10 @@
     return tuple(by_id[candidate_id] for candidate_id in TRUE_TRACE_TOP3_CANDIDATE_IDS)

+def _voiceover_text(script: ScriptDraft) -> str:
+    return " ".join((script.hook, *script.retention_beats, script.cta))
+
+
 def _build_artifact(candidate: VideoCandidate, *, output_dir: Path, approval_language: str) -> TrueTracePreviewArtifact:
     script = _script_from_candidate(candidate)
     shot_plan = create_shot_plan(script)


def _build_artifact(candidate: VideoCandidate, *, output_dir: Path, approval_language: str) -> TrueTracePreviewArtifact:
    script = _script_from_candidate(candidate)
    subtitle_track = build_hormozi_subtitle_track(
        _voiceover_text(script),
        total_duration_seconds=candidate.duration_seconds,
    )
    tts_plan = build_default_truetrace_tts_plan()
    shot_plan = create_shot_plan(script)

def _build_artifact(candidate: VideoCandidate, *, output_dir: Path, approval_language: str) -> TrueTracePreviewArtifact:
    script = _script_from_candidate(candidate)
    shot_plan = create_shot_plan(script)


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
@@ -109,6 +109,11 @@

 def _build_artifact(candidate: VideoCandidate, *, output_dir: Path, approval_language: str) -> TrueTracePreviewArtifact:
     script = _script_from_candidate(candidate)
+    subtitle_track = build_hormozi_subtitle_track(
+        _voiceover_text(script),
+        total_duration_seconds=candidate.duration_seconds,
+    )
+    tts_plan = build_default_truetrace_tts_plan()
     shot_plan = create_shot_plan(script)
     manifest = build_render_manifest(shot_plan)
     html_preview = build_html_preview(manifest)


        candidate=candidate,
        script=script,
        subtitle_track=subtitle_track,
        tts_plan=tts_plan,
        draft=draft,

        candidate=candidate,
        script=script,
        draft=draft,


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
@@ -141,6 +141,8 @@
     return TrueTracePreviewArtifact(
         candidate=candidate,
         script=script,
+        subtitle_track=subtitle_track,
+        tts_plan=tts_plan,
         draft=draft,
         package=package,
         manual_posting_pack=manual_pack,


                f"Preview video: {artifact.package.preview_video_path}",
                f"Subtitle cues: {len(artifact.subtitle_track.cues)}",
                f"TTS baseline: {artifact.tts_plan.provider} / {artifact.tts_plan.voice}",
                f"Hook: {artifact.candidate.hook}",

                f"Preview video: {artifact.package.preview_video_path}",
                f"Hook: {artifact.candidate.hook}",


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/demo/truetrace_preview_packages.py
@@ -189,6 +189,8 @@
                 f"Candidate: {artifact.candidate.candidate_id}",
                 f"Duration: {artifact.candidate.duration_seconds}s",
                 f"Preview video: {artifact.package.preview_video_path}",
+                f"Subtitle cues: {len(artifact.subtitle_track.cues)}",
+                f"TTS baseline: {artifact.tts_plan.provider} / {artifact.tts_plan.voice}",
                 f"Hook: {artifact.candidate.hook}",
                 f"Manual upload pack prepared: {'yes' if artifact.manual_posting_pack.requires_human_manual_upload else 'no'}",
                 f"Approval commands: {commands}",


terminal

pytest tests/test_truetrace_preview_packages.py tests/test_subtitle_cues.py tests/test_tts_plan.py -q


Output:
..........                                                               [100%]
10 passed in 23.06s


Explanation:
**Considering documentation updates**

I'm thinking about whether I should update the docs or readme for the integration. It seems like a small patch, but I want to make sure it's clear and helpful for anyone who might reference it later. I wonder if there are any specific areas that need extra attention or if I can just summarize the changes simply. I’ll proceed and make sure it’s thorough but concise so users can understand it easily.

patch

`autoshorts.demo.truetrace_preview_packages.build_top_truetrace_preview_packages()` turns the top three concepts into local German approval timing previews and side-effect-free approval/manual-upload packages. It writes PNG scene cards and MP4 timing previews under the selected output directory, then prepares `ContentPreviewApprovalPackage` and `ManualPostingPack` artifacts for each candidate. Each preview artifact also now carries a deterministic English subtitle cue track and the phase-1 EdgeTTS provider plan, so audio/caption decisions travel with the candidate before any real TTS call happens.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -74,7 +74,7 @@

 `render_truetrace_concept_summary()` renders those candidates with hooks, outlines, visuals, hypotheses, YouTube descriptions, TikTok captions, and tags. It performs no platform action and avoids action language.

-`autoshorts.demo.truetrace_preview_packages.build_top_truetrace_preview_packages()` turns the top three concepts into local German approval timing previews and side-effect-free approval/manual-upload packages. It writes PNG scene cards and MP4 timing previews under the selected output directory, then prepares `ContentPreviewApprovalPackage` and `ManualPostingPack` artifacts for each candidate.
+`autoshorts.demo.truetrace_preview_packages.build_top_truetrace_preview_packages()` turns the top three concepts into local German approval timing previews and side-effect-free approval/manual-upload packages. It writes PNG scene cards and MP4 timing previews under the selected output directory, then prepares `ContentPreviewApprovalPackage` and `ManualPostingPack` artifacts for each candidate. Each preview artifact also now carries a deterministic English subtitle cue track and the phase-1 EdgeTTS provider plan, so audio/caption decisions travel with the candidate before any real TTS call happens.

 Subtitle/TTS strategy is documented in `docs/subtitle_tts_strategy.md`: use deterministic Hormozi-inspired renderer-owned captions; start TTS with EdgeTTS English baseline; A/B test NVIDIA Magpie later; use Whisper for alignment/QA, not voice generation.

No linter for .md files


- `autoshorts.audio.tts_plan.build_tts_ab_test_plan()` keeps NVIDIA Magpie as a hosted A/B candidate and Whisper as `alignment_qa`, not voice generation.
- `autoshorts.demo.truetrace_preview_packages.build_top_truetrace_preview_packages()` attaches the subtitle track and EdgeTTS baseline plan to each top-3 preview artifact before any real audio generation.

## Caption pipeline

- `autoshorts.audio.tts_plan.build_tts_ab_test_plan()` keeps NVIDIA Magpie as a hosted A/B candidate and Whisper as `alignment_qa`, not voice generation.

## Caption pipeline


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/docs/subtitle_tts_strategy.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/docs/subtitle_tts_strategy.md
@@ -56,6 +56,7 @@
 - `autoshorts.audio.subtitles.validate_subtitle_track()` rejects non-renderer-owned cues, overcrowded captions, bad timing, and missing active words.
 - `autoshorts.audio.tts_plan.build_default_truetrace_tts_plan()` selects `edge_tts` with `en-GB-RyanNeural` for phase 1.
 - `autoshorts.audio.tts_plan.build_tts_ab_test_plan()` keeps NVIDIA Magpie as a hosted A/B candidate and Whisper as `alignment_qa`, not voice generation.
+- `autoshorts.demo.truetrace_preview_packages.build_top_truetrace_preview_packages()` attaches the subtitle track and EdgeTTS baseline plan to each top-3 preview artifact before any real audio generation.

 ## Caption pipeline

No linter for .md files


terminal

Output:
........................................................................ [ 48%]
........................................................................ [ 96%]
.....                                                                    [100%]
149 passed in 32.09s


git status --short && git add autoshorts/demo/truetrace_preview_packages.py tests/test_truetrace_preview_packages.py README.md docs/subtitle_tts_strategy.md && git commit -m "feat: attach audio plans to TrueTrace previews" && git status --short && git log --oneline -4


Output:
M README.md
 M autoshorts/demo/truetrace_preview_packages.py
 M docs/subtitle_tts_strategy.md
 M tests/test_truetrace_preview_packages.py
[feat/core-approval-quality-gates 066201d] feat: attach audio plans to TrueTrace previews
 4 files changed, 29 insertions(+), 1 deletion(-)
066201d feat: attach audio plans to TrueTrace previews
30a8129 feat: add subtitle and tts planning
6efc4dc feat: add TrueTrace top preview packages
2d6af9f feat: add TrueTrace concept testbatch


Verstanden, Sir — und ja: Das mit den 3 Videos war unsauber im Delivery. Ich habe geprüft: die Dateien selbst sind **nicht identisch**, ihre SHA256-Hashes unterscheiden sich. Telegram/Preview-Layer hat sie offenbar trotzdem praktisch gleich wirken lassen. Ich sende sie jetzt nicht nochmal ungefragt als Medien-Spam. Einmal Blechlawine reicht.

## Weitergemacht: Subtitle + TTS Planning eingebaut

Ich habe jetzt den nächsten Audio/Captions-Layer implementiert.

Neue Module:

```text
autoshorts/audio/__init__.py
autoshorts/audio/subtitles.py
autoshorts/audio/tts_plan.py
```

Neue Tests:

```text
tests/test_subtitle_cues.py
tests/test_tts_plan.py
```

Commit:

```text
30a8129 feat: add subtitle and tts planning
```

## Subtitles: Hormozi-inspired, renderer-owned

Neue Funktion:

```python
build_hormozi_subtitle_track()
```

Sie erzeugt deterministische Caption Cues:

- 2–5 Wörter pro Cue
- durchgehendes Timing von `0ms` bis Videodauer
- active-word Highlight-Metadaten
- max. 2 Zeilen
- lower-middle safe-zone Style
- keine Side Effects
- keine externen Calls
- renderer-owned, also **nicht** im AI-Bild/Video gebacken

Validierung:

```python
validate_subtitle_track()
```

Rejects:

- nicht renderer-owned
- zu viele Wörter
- kaputtes Timing
- fehlende active words
- Side Effects / externe Calls

Kurz: Wir bauen keine Untertitel mit „mach mal Text ins Bild“-Prompting. Wir bauen sie kontrolliert. Zivilisation, Sir.

## TTS Plan: EdgeTTS baseline, Magpie später A/B

Neue Funktion:

```python
build_default_truetrace_tts_plan()
```

Default:

```text
Provider: edge_tts
Voice: en-GB-RyanNeural
Language: en
Requires local GPU: no
Blocks video GPU: no
Requires API key: no
```

Magpie ist als späterer A/B-Kandidat modelliert:

```python
build_tts_ab_test_plan()
```

Darin:

```text
1. edge_tts
2. nvidia_magpie_tts_multilingual
3. whisper_alignment
```

Wichtig sauber codiert:

```text
Whisper = alignment_qa
Whisper ≠ voice_generation
```

Also keine Verwechslung zwischen STT und TTS.

## Danach integriert: Audio-Pläne hängen jetzt an den Top-3 Preview Packages

Ich habe die bestehenden TrueTrace Top-3 Preview Packages erweitert.

Commit:

```text
066201d feat: attach audio plans to TrueTrace previews
```

Jedes Preview Artifact enthält jetzt zusätzlich:

```python
subtitle_track
tts_plan
```

Also:

```text
Candidate
→ ScriptDraft
→ SubtitleTrack
→ TTSProviderPlan
→ ShotPlan
→ RenderManifest
→ MP4 Timing Preview
→ Approval Package
→ Manual Posting Pack
```

Die Summary zeigt jetzt auch:

```text
Subtitle cues: <n>
TTS baseline: edge_tts / en-GB-RyanNeural
```

## Tests

Full Suite:

```text
149 passed in 32.09s
```

## Aktuelle Commits

```text
066201d feat: attach audio plans to TrueTrace previews
30a8129 feat: add subtitle and tts planning
6efc4dc feat: add TrueTrace top preview packages
2d6af9f feat: add TrueTrace concept testbatch
```

## Entscheidung zu deiner Frage

Für jetzt bleibt die Architektur:

```text
Subtitles:
deterministic renderer-owned Hormozi-inspired captions

TTS:
EdgeTTS baseline first

NVIDIA Magpie:
später A/B testen, nicht Default

Whisper:
später Alignment/QA, nicht TTS

GPU:
für Video/Image reservieren
```

Nächster sinnvoller Schritt:

```text
EdgeTTS Adapter
→ echte .mp3/.wav Voiceover-Datei
→ FFmpeg Audio-Mux
→ Subtitle burn-in
→ erster audio+caption Preview-MP4
```

Dann bekommen wir nicht nur Timing-Karten, sondern einen echten ersten „final-ish“ Review Clip mit Stimme und Captions — weiterhin ohne Upload, ohne Platform Call, ohne versehentliche Content-Kanone.


[M G] Ok, mach weiter.


Explanation:
**Planning for TTS implementation**

I need to take action regarding the skills for text-to-speech (TTS). I've got TDD set up, but I need to scan and load the relevant components, including edge-tts. It seems I should inspect the project first to see if I should implement the EdgeTTS adapter. I might need to use the edge-tts CLI/package for this, and it sounds like I have to ensure TDD is in place. The user wants me to continue to the next step, so it’s time to implement the EdgeTTS adapter, along with audio mux and subtitle burn-in.


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'


edge-tts

---
name: edge-tts
description: "Text-to-speech using Microsoft Edge TTS. Supports multiple voices including German male/female options. Used for voice messages and audio output."
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
  hermes:
    tags: [TTS, Voice, Audio, Edge]
    related_skills: [sherpa-onnx-tts]
---

# Edge TTS

Text-to-speech using Microsoft Edge TTS (`edge-tts` Python package). No API key required — uses Microsoft's free Edge browser TTS engine.

## When to Use

Use this skill when:
- You need to convert text to speech for voice messages
- You want to test or compare different TTS voices
- The user asks for a voice message or speech output
- TTS configuration needs to be changed

## Prerequisites

```bash
pip install edge-tts
```

## User Voice Preference

**Default voice: `de-DE-KillianNeural`** (German male, natural, clear)
Set in `~/.hermes/config.yaml` → `tts.edge.voice`.

## German Voices Available

| Voice | Gender | Style | Description |
|-------|--------|-------|-------------|
| `de-DE-KillianNeural` | Male | Natural | Clear, natural, slightly youthful (DEFAULT) |
| `de-DE-ConradNeural` | Male | Friendly | Warm, positive, slightly melodic |
| `de-DE-FlorianMultilingualNeural` | Male | Professional | Clear, neutral, professional |
| `de-DE-AmalaNeural` | Female | Friendly | Positive, warm |
| `de-DE-KatjaNeural` | Female | Friendly | Positive, clear |
| `de-DE-SeraphinaMultilingualNeural` | Female | General | Natural, clear |

## Usage

### Basic Synthesis

```bash
edge-tts --voice de-DE-ConradNeural --text "Guten Tag, Sir." --write-media output.mp3
```

### Testing a Voice

```bash
edge-tts --voice de-DE-FlorianMultilingualNeural --text "Testnachricht" --write-media /tmp/test.mp3
```

### Listing Available Voices

```bash
python3 -m edge_tts --list-voices | grep "de-DE"
```

### Configuring in Hermes

Update `~/.hermes/config.yaml`:

```yaml
tts:
  provider: edge
  edge:
    voice: de-DE-ConradNeural
```

## Pitfalls

- **Voice compatibility**: Not all voices work with all text. German text requires a German voice (`de-DE-*`).
- **File format**: Edge TTS outputs MP3 by default. Convert to OGG for Telegram voice messages if needed: `ffmpeg -i input.mp3 -codec:a libopus -b:a 128k output.ogg`
- **Gateway restart**: After changing the TTS voice in config, restart the gateway for changes to take effect.
- **Character limits**: Very long texts may produce unusually long audio. Split long messages before TTS.
- **Config verification**: After updating `tts.edge.voice` in config, always verify the change took effect by reading the config back. YAML writes can silently fail or be overwritten by subsequent edits. Check the TTS response field `voice_compatible: true` — if `false`, the voice may not have been applied.
- **Voice switching workflow**: Always test a voice with `edge-tts --voice <voice> --text "Test" --write-media /tmp/test.mp3` before committing to it. Multiple voices can sound similar; listening is the only reliable way to choose.
- **Config persistence**: Python one-liner YAML edits may not persist — always verify with a separate read-back step.

## Shorts / AutoShorts Baseline Voice Use

For short-form content pipelines where the GPU is needed for ComfyUI/Wan/Qwen image or video generation, use EdgeTTS as the first baseline voice because it does not consume local GPU resources and is quick to iterate.

Recommended first English A/B voices for a precise, non-guru AI/workflow channel tone:

```bash
edge-tts --voice en-GB-RyanNeural --text "Most AI automations do not fail dramatically. They quietly rot." --write-media /tmp/ryan.mp3
edge-tts --voice en-US-ChristopherNeural --text "Most AI automations do not fail dramatically. They quietly rot." --write-media /tmp/christopher.mp3
```

Compare against any external/cloud TTS such as NVIDIA Magpie with the same 15–20 second script sample. Score clarity, pronunciation of AI/workflow terms, retention energy, artifacts, latency, cost, and operational reliability. Whisper is not a TTS model; use it later for transcript/alignment QA only.

## Linked Resources

- `references/german-voice-comparison.md` — German male voice comparison data from session 2026-05-07, with recommendations for voice selection.

## Voice Switching Workflow

1. List available voices: `python3 -m edge_tts --list-voices | grep "de-DE"`
2. Test candidate voice: `edge-tts --voice <voice> --text "Testnachricht" --write-media /tmp/test.mp3`
3. Listen and decide
4. Update config: `tts.edge.voice: <chosen_voice>`
5. Restart gateway

## Related Skills

- `sherpa-onnx-tts` — Offline TTS with local German female voice (Eva). No internet required.


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.

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


Explanation:
**Inspecting ffmpeg implementation**

I need to focus on implementing something here, but it sounds like I also need to inspect the existing ffmpeg setup. There are probably details that I should verify before moving forward. I wonder what features or configurations might be essential. I want to ensure everything is set up correctly to avoid any potential issues later on. Time to dig in and see what’s already in place!


     1|"""FFmpeg rendering boundary for local preview videos."""
     2|
     3|from __future__ import annotations
     4|
     5|from dataclasses import dataclass
     6|from pathlib import Path
     7|import subprocess
     8|
     9|from autoshorts.rendering.ffmpeg_plan import FFmpegConcatPlan, validate_ffmpeg_concat_plan
    10|
    11|
    12|@dataclass(frozen=True)
    13|class FFmpegRenderResult:
    14|    script_id: str
    15|    output_path: str
    16|    concat_file_path: str
    17|    returncode: int
    18|    stdout: str
    19|    stderr: str
    20|    side_effects: tuple[str, ...] = ("write_concat", "call_ffmpeg", "write_mp4")
    21|    external_calls: tuple[str, ...] = ("ffmpeg",)
    22|
    23|
    24|def render_ffmpeg_preview_video(plan: FFmpegConcatPlan) -> FFmpegRenderResult:
    25|    """Write the concat file and invoke FFmpeg to create a local MP4 preview."""
    26|
    27|    validation = validate_ffmpeg_concat_plan(plan)
    28|    if not validation.is_valid:
    29|        raise ValueError("; ".join(validation.errors))
    30|
    31|    concat_path = Path(plan.concat_file_path)
    32|    output_path = Path(plan.output_path)
    33|    concat_path.parent.mkdir(parents=True, exist_ok=True)
    34|    output_path.parent.mkdir(parents=True, exist_ok=True)
    35|    concat_path.write_text(plan.concat_file_content)
    36|
    37|    command = [
    38|        "ffmpeg",
    39|        "-y",
    40|        "-f",
    41|        "concat",
    42|        "-safe",
    43|        "0",
    44|        "-i",
    45|        str(concat_path),
    46|        "-vf",
    47|        f"fps={plan.fps},format=yuv420p",
    48|        "-movflags",
    49|        "+faststart",
    50|        str(output_path),
    51|    ]
    52|    completed = subprocess.run(command, capture_output=True, text=True, check=False)
    53|    if completed.returncode != 0:
    54|        raise RuntimeError(completed.stderr or "ffmpeg failed")
    55|
    56|    return FFmpegRenderResult(
    57|        script_id=plan.script_id,
    58|        output_path=str(output_path),
    59|        concat_file_path=str(concat_path),
    60|        returncode=completed.returncode,
    61|        stdout=completed.stdout,
    62|        stderr=completed.stderr,
    63|        side_effects=("write_concat", "call_ffmpeg", "write_mp4"),
    64|        external_calls=("ffmpeg",),
    65|    )
    66|