Explanation:
**Creating a generation script**

I need to write a generation script and make sure the performance is okay. I might use ffmpeg to handle an image sequence and audio together. I'll set the render at 1080x1920 and 30fps, which could take some time, but that's fine. I’ll need to store frames temporarily and ensure there are no extra black bars. Visual quality assurance will be important, so I'll make sure to create the script effectively.

#!/usr/bin/env python3
"""Render next post candidate ai-004 as a 9:16 final-near approval video.

Deterministic renderer owns all readable UI/cards/subtitles. EdgeTTS provides
English voiceover and word-boundary timing; no platform publishing side effects.
"""

from __future__ import annotations

import asyncio
import hashlib
import json
import math
import shutil
import subprocess
import tempfile
from pathlib import Path

import edge_tts
from PIL import Image, ImageDraw, ImageFont

ROOT = Path(__file__).resolve().parents[1]
OUT_DIR = ROOT / "data" / "post_candidates" / "ai-004-stop-asking-ai-to-think-for-you"
AUDIO = OUT_DIR / "voiceover_en_gb_ryan.mp3"
VIDEO_NO_AUDIO = OUT_DIR / "video_no_audio.mp4"
FINAL = OUT_DIR / "ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4"
PACKAGE = OUT_DIR / "review_package.json"
PACKAGE_MD = OUT_DIR / "review_package.md"
QA_FRAME = OUT_DIR / "qa_frame_00_12.png"

WIDTH = 1080
HEIGHT = 1920
FPS = 30
VOICE = "en-GB-RyanNeural"

SCRIPT = (
    "Stop asking AI to think for you. Ask it to argue with you. "
    "When you hand AI a messy idea and say, what should I do, "
    "it can sound confident before you are ready. Use it as a critic instead. "
    "First: list my assumptions. Second: find the weak spots. Third: give me two alternatives. "
    "Fourth: tell me what I still need to decide myself. Now AI is not replacing your judgment. "
    "It is pressure-testing it. The rule is simple: never ask AI for the final answer "
    "before you ask it for the argument."
)

SCENES = [
    (0.00, 4.50, "HOOK", "Stop asking AI to think for you.", "Ask it to argue with you."),
    (4.50, 9.50, "VISIBLE FAIL", "Messy idea → instant answer", "Confidence arrives too early."),
    (9.50, 14.50, "MECHANISM", "Bad prompt: What should I do?", "The model fills missing judgment."),
    (14.50, 20.00, "FIX 1", "List my assumptions", "Make hidden guesses visible."),
    (20.00, 25.50, "FIX 2", "Find the weak spots", "Attack the plan before reality does."),
    (25.50, 31.50, "FIX 3", "Give two alternatives", "Options beat obedient answers."),
    (31.50, 37.50, "FIX 4", "What must I decide myself?", "Keep authority with the human."),
    (37.50, 44.50, "RULE", "Answer last. Argument first.", "Save this prompt structure."),
]

def font(size: int, bold: bool = False):
    candidates = [
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
    ]
    for c in candidates:
        if Path(c).exists():
            return ImageFont.truetype(c, size=size)
    return ImageFont.load_default()

def text_size(draw: ImageDraw.ImageDraw, text: str, fnt) -> tuple[int, int]:
    box = draw.textbbox((0, 0), text, font=fnt)
    return box[2] - box[0], box[3] - box[1]

def wrap_by_pixels(draw: ImageDraw.ImageDraw, text: str, fnt, max_width: int) -> list[str]:
    words = text.split()
    lines: list[str] = []
    line = ""
    for word in words:
        cand = (line + " " + word).strip()
        if text_size(draw, cand, fnt)[0] <= max_width or not line:
            line = cand
        else:
            lines.append(line)
            line = word
    if line:
        lines.append(line)
    return lines

async def synthesize() -> list[dict[str, float | str]]:
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    words: list[dict[str, float | str]] = []
    communicate = edge_tts.Communicate(SCRIPT, VOICE, rate="+8%", boundary="WordBoundary")
    with AUDIO.open("wb") as fh:
        async for chunk in communicate.stream():
            if chunk["type"] == "audio":
                fh.write(chunk["data"])
            elif chunk["type"] == "WordBoundary":
                words.append(
                    {
                        "word": str(chunk["text"]),
                        "start": chunk["offset"] / 10_000_000,
                        "duration": max(0.05, chunk["duration"] / 10_000_000),
                    }
                )
    return words

def ffprobe_duration(path: Path) -> float:
    raw = subprocess.check_output(
        ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=nw=1:nk=1", str(path)],
        text=True,
    ).strip()
    return float(raw)

def build_segments(words: list[dict[str, float | str]]) -> list[list[dict[str, float | str]]]:
    segments: list[list[dict[str, float | str]]] = []
    current: list[dict[str, float | str]] = []
    for w in words:
        current.append(w)
        token = str(w["word"])
        if len(current) >= 5 or token.endswith(('.', '?', '!')):
            segments.append(current)
            current = []
    if current:
        segments.append(current)
    return segments

def scene_for_time(t: float, duration: float):
    # Stretch final scene if TTS runs longer than the planned visual beat.
    for scene in SCENES:
        if scene[0] <= t < scene[1]:
            return scene
    return SCENES[-1] if t <= duration + 1 else SCENES[0]

def segment_for_time(t: float, segments):
    for seg in segments:
        start = float(seg[0]["start"])
        end = float(seg[-1]["start"]) + float(seg[-1]["duration"]) + 0.2
        if start <= t <= end:
            return seg
    return []

def active_word_index(t: float, seg) -> int:
    for i, w in enumerate(seg):
        start = float(w["start"])
        end = start + float(w["duration"]) + 0.08
        if start <= t <= end:
            return i
    return max(0, min(len(seg) - 1, sum(1 for w in seg if float(w["start"]) <= t) - 1))

def rounded(draw, box, radius, fill, outline=None, width=1):
    draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width)

def draw_gradient(draw: ImageDraw.ImageDraw, frame_idx: int, total_frames: int):
    p = frame_idx / max(1, total_frames - 1)
    for y in range(0, HEIGHT, 4):
        r = y / HEIGHT
        base = (
            int(4 + 11 * r + 8 * math.sin(p * math.tau)),
            int(8 + 20 * r),
            int(28 + 55 * r),
        )
        draw.rectangle([0, y, WIDTH, y + 4], fill=base)
    # soft moving blobs
    for i, col in enumerate([(37, 99, 235), (14, 165, 233), (168, 85, 247)]):
        cx = int(WIDTH * (0.22 + 0.28 * i + 0.05 * math.sin(p * math.tau + i)))
        cy = int(HEIGHT * (0.15 + 0.18 * i + 0.03 * math.cos(p * math.tau * 0.7 + i)))
        rr = int(260 + 70 * math.sin(p * math.tau + i))
        draw.ellipse([cx-rr, cy-rr, cx+rr, cy+rr], outline=tuple(min(255, c+30) for c in col), width=5)

def draw_ui_scene(draw: ImageDraw.ImageDraw, scene, p: float):
    _, _, label, main, sub = scene
    small = font(34, False)
    med = font(48, True)
    large = font(68, True)
    card_x1, card_y1, card_x2, card_y2 = 90, 235, 990, 1265
    rounded(draw, [card_x1, card_y1, card_x2, card_y2], 44, (248, 250, 252), (59, 130, 246), 4)
    draw.text((115, 95), "TrueTraceShorts", font=font(36, True), fill=(226, 232, 240))
    draw.text((115, 145), "Trace the hidden system. Fix the outcome.", font=font(26, False), fill=(148, 163, 184))
    rounded(draw, [115, card_y1+45, 360, card_y1+105], 24, (15, 23, 42), None)
    draw.text((140, card_y1+58), label, font=font(28, True), fill=(125, 211, 252))
    # simulated prompt window
    top = card_y1 + 150
    rounded(draw, [135, top, 945, top+280], 32, (15, 23, 42), (203, 213, 225), 2)
    for i, c in enumerate([(248, 113, 113), (251, 191, 36), (34, 197, 94)]):
        draw.ellipse([165+i*42, top+28, 190+i*42, top+53], fill=c)
    draw.text((165, top+88), "USER PROMPT", font=font(27, True), fill=(148, 163, 184))
    for j, line in enumerate(wrap_by_pixels(draw, main, large, 690)[:3]):
        draw.text((165, top+135+j*82), line, font=large, fill=(248, 250, 252))
    # AI response/checklist cards
    base_y = top + 360
    items = ["ASSUMPTIONS", "WEAK SPOTS", "ALTERNATIVES", "HUMAN DECISION"]
    active = min(len(items)-1, int(p * len(items)))
    for i, item in enumerate(items):
        y = base_y + i * 105
        fill = (219, 234, 254) if i <= active else (226, 232, 240)
        outline = (59, 130, 246) if i == active else (203, 213, 225)
        rounded(draw, [165, y, 915, y+78], 24, fill, outline, 3)
        draw.text((200, y+19), item, font=font(30, True), fill=(15, 23, 42))
        if i <= active:
            draw.text((845, y+16), "✓", font=font(38, True), fill=(22, 163, 74))
    # subline / rule
    for j, line in enumerate(wrap_by_pixels(draw, sub, med, 760)[:2]):
        w, _ = text_size(draw, line, med)
        draw.text(((WIDTH-w)//2, 1190 + j*58), line, font=med, fill=(15, 23, 42))

def draw_subtitles(draw: ImageDraw.ImageDraw, seg, active: int):
    if not seg:
        return
    words = [str(w["word"]).strip() for w in seg]
    fnt = font(62, True)
    pad_x, pad_y = 42, 28
    y = 1485
    # compute word positions centered
    widths = [text_size(draw, w, fnt)[0] for w in words]
    gap = 18
    total = sum(widths) + gap * (len(words)-1)
    x = (WIDTH - total) // 2
    box = [max(35, x-pad_x), y-pad_y, min(WIDTH-35, x+total+pad_x), y+90+pad_y]
    rounded(draw, box, 32, (3, 7, 18), (59, 130, 246), 3)
    for i, (word, ww) in enumerate(zip(words, widths)):
        fill = (250, 204, 21) if i == active else (248, 250, 252)
        draw.text((x, y), word.upper(), font=fnt, fill=fill)
        x += ww + gap

def render_video(words: list[dict[str, float | str]], duration: float):
    segments = build_segments(words)
    total_duration = max(duration + 0.8, SCENES[-1][1])
    total_frames = math.ceil(total_duration * FPS)
    with tempfile.TemporaryDirectory(prefix="autoshorts_ai004_frames_") as tmp:
        frame_dir = Path(tmp) / "frames"
        frame_dir.mkdir()
        for idx in range(total_frames):
            t = idx / FPS
            img = Image.new("RGB", (WIDTH, HEIGHT), (3, 7, 18))
            draw = ImageDraw.Draw(img)
            draw_gradient(draw, idx, total_frames)
            scene = scene_for_time(t, total_duration)
            sp = (t - scene[0]) / max(0.001, scene[1] - scene[0])
            draw_ui_scene(draw, scene, max(0, min(1, sp)))
            seg = segment_for_time(t, segments)
            draw_subtitles(draw, seg, active_word_index(t, seg) if seg else 0)
            # progress rail
            draw.rounded_rectangle([95, 1810, 985, 1824], radius=7, fill=(30, 41, 59))
            draw.rounded_rectangle([95, 1810, 95 + int(890 * min(1, t/total_duration)), 1824], radius=7, fill=(59, 130, 246))
            img.save(frame_dir / f"frame_{idx:06d}.jpg", quality=92)
        subprocess.check_call([
            "ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
            "-framerate", str(FPS), "-i", str(frame_dir / "frame_%06d.jpg"),
            "-vf", "setsar=1,setdar=9/16,format=yuv420p",
            "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
            "-movflags", "+faststart", str(VIDEO_NO_AUDIO)
        ])
    subprocess.check_call([
        "ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
        "-i", str(VIDEO_NO_AUDIO), "-i", str(AUDIO),
        "-map", "0:v:0", "-map", "1:a:0",
        "-c:v", "copy", "-c:a", "aac", "-b:a", "160k",
        "-shortest", "-movflags", "+faststart", str(FINAL)
    ])
    subprocess.check_call([
        "ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
        "-ss", "12", "-i", str(FINAL), "-frames:v", "1", str(QA_FRAME)
    ])

def probe_video(path: Path) -> dict[str, object]:
    meta = json.loads(subprocess.check_output([
        "ffprobe", "-v", "error", "-select_streams", "v:0",
        "-show_entries", "stream=width,height,sample_aspect_ratio,display_aspect_ratio,r_frame_rate",
        "-show_entries", "format=duration", "-of", "json", str(path)
    ], text=True))
    s = meta["streams"][0]
    return {
        "width": s["width"], "height": s["height"],
        "sample_aspect_ratio": s.get("sample_aspect_ratio"),
        "display_aspect_ratio": s.get("display_aspect_ratio"),
        "frame_rate": s.get("r_frame_rate"),
        "duration": round(float(meta["format"]["duration"]), 3),
    }

def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()

def main() -> None:
    if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
        raise RuntimeError("ffmpeg/ffprobe required")
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    words = asyncio.run(synthesize())
    audio_duration = ffprobe_duration(AUDIO)
    render_video(words, audio_duration)
    vmeta = probe_video(FINAL)
    digest = sha256(FINAL)
    package = {
        "schema_version": "autoshorts_final_review.v1",
        "requires_human_approval": True,
        "side_effects": [],
        "candidate_id": "ai-004",
        "title": "Stop asking AI to think for you",
        "series": "AI Output Autopsy",
        "pillar": "ai_life_systems",
        "final_video_language": "en",
        "approval_language": "de",
        "media_path": str(FINAL.resolve()),
        "qa_frame_path": str(QA_FRAME.resolve()),
        "sha256": digest,
        "video_meta": vmeta,
        "voice": VOICE,
        "hook": "Stop asking AI to think for you. Ask it to argue with you.",
        "caption": "AI is more useful as a critic than as a guru.",
        "hashtags": ["#AI", "#Productivity", "#Workflow", "#DecisionMaking", "#TrueTraceShorts"],
        "platform_targets": ["YouTube Shorts", "TikTok", "Instagram Reels"],
        "quality_notes": [
            "1080x1920 export with SAR 1:1 and DAR 9:16",
            "EdgeTTS English voiceover with word-boundary driven active-word captions",
            "Readable UI/cards are deterministic renderer-owned, not AI-generated",
            "No platform posting side effects performed",
        ],
        "approval_commands": ["Freigabe YouTube", "Freigabe TikTok", "Freigabe Instagram", "Freigabe alle", "Ablehnen", "Ändern: <Wunsch>"],
        "script": SCRIPT,
    }
    PACKAGE.write_text(json.dumps(package, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    md = f"""## AutoShortsBot Review — ai-004\n\nMEDIA:{FINAL.resolve()}\n\n**Titel:** Stop asking AI to think for you\n**Serie:** AI Output Autopsy\n**Hook:** Stop asking AI to think for you. Ask it to argue with you.\n**Sprache final:** Englisch\n**Zielplattformen:** YouTube Shorts, TikTok, Instagram Reels\n**Format:** {vmeta['width']}x{vmeta['height']}, SAR {vmeta['sample_aspect_ratio']}, DAR {vmeta['display_aspect_ratio']}, {vmeta['duration']}s\n**SHA256:** `{digest}`\n\n**Caption:** AI is more useful as a critic than as a guru.\n**Hashtags:** {' '.join(package['hashtags'])}\n\n**QA:** 9:16 mobile-safe, aktive Wort-Untertitel, deterministic UI/cards, keine Posting-Side-Effects.\n**QA frame:** `{QA_FRAME.resolve()}`\n\n**Freigabe-Befehle:**\n- `Freigabe YouTube`\n- `Freigabe TikTok`\n- `Freigabe Instagram`\n- `Freigabe alle`\n- `Ablehnen`\n- `Ändern: <Wunsch>`\n"""
    PACKAGE_MD.write_text(md, encoding="utf-8")
    print(json.dumps({"final": str(FINAL), "package": str(PACKAGE), "meta": vmeta, "sha256": digest}, indent=2))

if __name__ == "__main__":
    main()


LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py">
ERROR [71:12] Type "tuple[float, float]" is not assignable to return type "tuple[int, int]"
  "float" is not assignable to "int"
  "float" is not assignable to "int" [reportReturnType] (Pyright)
ERROR [97:26] Could not access item in TypedDict
  "data" is not a required key in "TTSChunk", so access may result in runtime exception [reportTypedDictNotRequiredAccess] (Pyright)
ERROR [101:37] Could not access item in TypedDict
  "text" is not a required key in "TTSChunk", so access may result in runtime exception [reportTypedDictNotRequiredAccess] (Pyright)
ERROR [102:34] Could not access item in TypedDict
  "offset" is not a required key in "TTSChunk", so access may result in runtime exception [reportTypedDictNotRequiredAccess] (Pyright)
ERROR [103:47] Could not access item in TypedDict
  "duration" is not a required key in "TTSChunk", so access may result in runtime exception [reportTypedDictNotRequiredAccess] (Pyright)
</diagnostics>


Explanation:
**Fixing type errors**

I need to address some type errors I'm encountering. I think I should use a patch for this. The function `text_size` currently returns floats because of `textbbox`, so I should cast that to an integer. Also, when accessing chunks, it seems I need to use `get`. I'll make sure to implement these changes properly to ensure everything runs smoothly. This feels like a little puzzle to solve, so I'm on it!

patch

def text_size(draw: ImageDraw.ImageDraw, text: str, fnt) -> tuple[int, int]:
    box = draw.textbbox((0, 0), text, font=fnt)
    return box[2] - box[0], box[3] - box[1]

def text_size(draw: ImageDraw.ImageDraw, text: str, fnt) -> tuple[int, int]:
    box = draw.textbbox((0, 0), text, font=fnt)
    return int(box[2] - box[0]), int(box[3] - box[1])


--- a//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
+++ b//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@ -68,7 +68,7 @@

 def text_size(draw: ImageDraw.ImageDraw, text: str, fnt) -> tuple[int, int]:
     box = draw.textbbox((0, 0), text, font=fnt)
-    return box[2] - box[0], box[3] - box[1]
+    return int(box[2] - box[0]), int(box[3] - box[1])

 def wrap_by_pixels(draw: ImageDraw.ImageDraw, text: str, fnt, max_width: int) -> list[str]:


            if chunk["type"] == "audio":
                fh.write(bytes(chunk.get("data", b"")))
            elif chunk["type"] == "WordBoundary":
                words.append(
                    {
                        "word": str(chunk.get("text", "")),
                        "start": float(chunk.get("offset", 0)) / 10_000_000,
                        "duration": max(0.05, float(chunk.get("duration", 0)) / 10_000_000),
                    }
                )

            if chunk["type"] == "audio":
                fh.write(chunk["data"])
            elif chunk["type"] == "WordBoundary":
                words.append(
                    {
                        "word": str(chunk["text"]),
                        "start": chunk["offset"] / 10_000_000,
                        "duration": max(0.05, chunk["duration"] / 10_000_000),
                    }
                )


--- a//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
+++ b//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@ -94,13 +94,13 @@
     with AUDIO.open("wb") as fh:
         async for chunk in communicate.stream():
             if chunk["type"] == "audio":
-                fh.write(chunk["data"])
+                fh.write(bytes(chunk.get("data", b"")))
             elif chunk["type"] == "WordBoundary":
                 words.append(
                     {
-                        "word": str(chunk["text"]),
-                        "start": chunk["offset"] / 10_000_000,
-                        "duration": max(0.05, chunk["duration"] / 10_000_000),
+                        "word": str(chunk.get("text", "")),
+                        "start": float(chunk.get("offset", 0)) / 10_000_000,
+                        "duration": max(0.05, float(chunk.get("duration", 0)) / 10_000_000),
                     }
                 )
     return words


terminal

Output:
{
  "final": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4",
  "package": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/review_package.json",
  "meta": {
    "width": 1080,
    "height": 1920,
    "sample_aspect_ratio": "1:1",
    "display_aspect_ratio": "9:16",
    "frame_rate": "30/1",
    "duration": 35.064
  },
  "sha256": "5ea59b2dc9cbe313c076f6b3e8e42dc8b6dfe95fb9bebc9cce9d7c3d53705d65"
}


patch

SCENES = [
    (0.00, 3.60, "HOOK", "Stop asking AI to think for you.", "Ask it to argue with you."),
    (3.60, 7.80, "VISIBLE FAIL", "Messy idea → instant answer", "Confidence arrives too early."),
    (7.80, 12.00, "MECHANISM", "Bad prompt: What should I do?", "The model fills missing judgment."),
    (12.00, 16.20, "FIX 1", "List my assumptions", "Make hidden guesses visible."),
    (16.20, 20.40, "FIX 2", "Find the weak spots", "Attack the plan before reality does."),
    (20.40, 24.80, "FIX 3", "Give two alternatives", "Options beat obedient answers."),
    (24.80, 29.60, "FIX 4", "What must I decide myself?", "Keep authority with the human."),
    (29.60, 36.00, "RULE", "Answer last. Argument first.", "Save this prompt structure."),
]

SCENES = [
    (0.00, 4.50, "HOOK", "Stop asking AI to think for you.", "Ask it to argue with you."),
    (4.50, 9.50, "VISIBLE FAIL", "Messy idea → instant answer", "Confidence arrives too early."),
    (9.50, 14.50, "MECHANISM", "Bad prompt: What should I do?", "The model fills missing judgment."),
    (14.50, 20.00, "FIX 1", "List my assumptions", "Make hidden guesses visible."),
    (20.00, 25.50, "FIX 2", "Find the weak spots", "Attack the plan before reality does."),
    (25.50, 31.50, "FIX 3", "Give two alternatives", "Options beat obedient answers."),
    (31.50, 37.50, "FIX 4", "What must I decide myself?", "Keep authority with the human."),
    (37.50, 44.50, "RULE", "Answer last. Argument first.", "Save this prompt structure."),
]


--- a//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
+++ b//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@ -44,14 +44,14 @@
 )

 SCENES = [
-    (0.00, 4.50, "HOOK", "Stop asking AI to think for you.", "Ask it to argue with you."),
-    (4.50, 9.50, "VISIBLE FAIL", "Messy idea → instant answer", "Confidence arrives too early."),
-    (9.50, 14.50, "MECHANISM", "Bad prompt: What should I do?", "The model fills missing judgment."),
-    (14.50, 20.00, "FIX 1", "List my assumptions", "Make hidden guesses visible."),
-    (20.00, 25.50, "FIX 2", "Find the weak spots", "Attack the plan before reality does."),
-    (25.50, 31.50, "FIX 3", "Give two alternatives", "Options beat obedient answers."),
-    (31.50, 37.50, "FIX 4", "What must I decide myself?", "Keep authority with the human."),
-    (37.50, 44.50, "RULE", "Answer last. Argument first.", "Save this prompt structure."),
+    (0.00, 3.60, "HOOK", "Stop asking AI to think for you.", "Ask it to argue with you."),
+    (3.60, 7.80, "VISIBLE FAIL", "Messy idea → instant answer", "Confidence arrives too early."),
+    (7.80, 12.00, "MECHANISM", "Bad prompt: What should I do?", "The model fills missing judgment."),
+    (12.00, 16.20, "FIX 1", "List my assumptions", "Make hidden guesses visible."),
+    (16.20, 20.40, "FIX 2", "Find the weak spots", "Attack the plan before reality does."),
+    (20.40, 24.80, "FIX 3", "Give two alternatives", "Options beat obedient answers."),
+    (24.80, 29.60, "FIX 4", "What must I decide myself?", "Keep authority with the human."),
+    (29.60, 36.00, "RULE", "Answer last. Argument first.", "Save this prompt structure."),
 ]


terminal

Output:
{
  "final": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4",
  "package": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/review_package.json",
  "meta": {
    "width": 1080,
    "height": 1920,
    "sample_aspect_ratio": "1:1",
    "display_aspect_ratio": "9:16",
    "frame_rate": "30/1",
    "duration": 35.064
  },
  "sha256": "91727994a10e0ea53f7c8f4f41c10a44f8431dd5b65e1a9725e1f1ea47884648"
}


Question: QA this 9:16 Shorts frame: are subtitles and UI text readable, within safe zones, and are there black bars or obvious aspect-ratio distortion? Keep answer concise.


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

Question: QA this 9:16 Shorts frame: are subtitles and UI text readable, within safe zones, and are there black bars or obvious aspect-ratio distortion? Keep answer concise.


Explanation:
**Adjusting subtitle display**

I'm working on increasing or decreasing the window height to fit subtitles correctly. The current subtitle layout at y=1248 is clipped—so I might need to limit it to one line. I’ll reduce the y value and make it dynamic, breaking subtitles into smaller segments of three words max. If they exceed the width, I’ll shrink the font size from 54 down to 38 and clean up empty words and punctuation for better spacing. I’ll patch that up!

patch

*** Begin Patch
*** Update File: /home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@
-        if len(current) >= 5 or token.endswith(('.', '?', '!')):
+        # Keep active-word caption groups compact on mobile. Four/five word groups
+        # are readable in theory but collide in 1080-wide Shorts once uppercased.
+        if len(current) >= 3 or token.endswith(('.', '?', '!')):
@@
-    large = font(68, True)
+    large = font(58, True)
@@
-    rounded(draw, [135, top, 945, top+280], 32, (15, 23, 42), (203, 213, 225), 2)
+    rounded(draw, [135, top, 945, top+310], 32, (15, 23, 42), (203, 213, 225), 2)
@@
-    for j, line in enumerate(wrap_by_pixels(draw, main, large, 690)[:3]):
-        draw.text((165, top+135+j*82), line, font=large, fill=(248, 250, 252))
+    for j, line in enumerate(wrap_by_pixels(draw, main, large, 690)[:3]):
+        draw.text((165, top+132+j*70), line, font=large, fill=(248, 250, 252))
@@
-    base_y = top + 360
+    base_y = top + 385
@@
-    for j, line in enumerate(wrap_by_pixels(draw, sub, med, 760)[:2]):
+    for j, line in enumerate(wrap_by_pixels(draw, sub, med, 760)[:2]):
         w, _ = text_size(draw, line, med)
-        draw.text(((WIDTH-w)//2, 1190 + j*58), line, font=med, fill=(15, 23, 42))
+        draw.text(((WIDTH-w)//2, 1168 + j*54), line, font=med, fill=(15, 23, 42))
@@
-    words = [str(w["word"]).strip() for w in seg]
-    fnt = font(62, True)
+    words = [str(w["word"]).strip() for w in seg if str(w["word"]).strip()]
+    if not words:
+        return
+    fnt = font(62, True)
@@
-    widths = [text_size(draw, w, fnt)[0] for w in words]
-    gap = 18
-    total = sum(widths) + gap * (len(words)-1)
+    gap = 22
+    max_total = WIDTH - 220
+    for size in range(62, 39, -2):
+        fnt = font(size, True)
+        widths = [text_size(draw, w, fnt)[0] for w in words]
+        total = sum(widths) + gap * (len(words)-1)
+        if total <= max_total:
+            break
*** End Patch


--- a//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
+++ b//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@ -120,7 +120,9 @@
     for w in words:
         current.append(w)
         token = str(w["word"])
-        if len(current) >= 5 or token.endswith(('.', '?', '!')):
+        # Keep active-word caption groups compact on mobile. Four/five word groups
+        # are readable in theory but collide in 1080-wide Shorts once uppercased.
+        if len(current) >= 3 or token.endswith(('.', '?', '!')):
             segments.append(current)
             current = []
     if current:
@@ -180,7 +182,7 @@
     _, _, label, main, sub = scene
     small = font(34, False)
     med = font(48, True)
-    large = font(68, True)
+    large = font(58, True)
     card_x1, card_y1, card_x2, card_y2 = 90, 235, 990, 1265
     rounded(draw, [card_x1, card_y1, card_x2, card_y2], 44, (248, 250, 252), (59, 130, 246), 4)
     draw.text((115, 95), "TrueTraceShorts", font=font(36, True), fill=(226, 232, 240))
@@ -189,14 +191,14 @@
     draw.text((140, card_y1+58), label, font=font(28, True), fill=(125, 211, 252))
     # simulated prompt window
     top = card_y1 + 150
-    rounded(draw, [135, top, 945, top+280], 32, (15, 23, 42), (203, 213, 225), 2)
+    rounded(draw, [135, top, 945, top+310], 32, (15, 23, 42), (203, 213, 225), 2)
     for i, c in enumerate([(248, 113, 113), (251, 191, 36), (34, 197, 94)]):
         draw.ellipse([165+i*42, top+28, 190+i*42, top+53], fill=c)
     draw.text((165, top+88), "USER PROMPT", font=font(27, True), fill=(148, 163, 184))
     for j, line in enumerate(wrap_by_pixels(draw, main, large, 690)[:3]):
-        draw.text((165, top+135+j*82), line, font=large, fill=(248, 250, 252))
+        draw.text((165, top+132+j*70), line, font=large, fill=(248, 250, 252))
     # AI response/checklist cards
-    base_y = top + 360
+    base_y = top + 385
     items = ["ASSUMPTIONS", "WEAK SPOTS", "ALTERNATIVES", "HUMAN DECISION"]
     active = min(len(items)-1, int(p * len(items)))
     for i, item in enumerate(items):
@@ -210,20 +212,27 @@
     # subline / rule
     for j, line in enumerate(wrap_by_pixels(draw, sub, med, 760)[:2]):
         w, _ = text_size(draw, line, med)
-        draw.text(((WIDTH-w)//2, 1190 + j*58), line, font=med, fill=(15, 23, 42))
+        draw.text(((WIDTH-w)//2, 1168 + j*54), line, font=med, fill=(15, 23, 42))

 def draw_subtitles(draw: ImageDraw.ImageDraw, seg, active: int):
     if not seg:
         return
-    words = [str(w["word"]).strip() for w in seg]
+    words = [str(w["word"]).strip() for w in seg if str(w["word"]).strip()]
+    if not words:
+        return
     fnt = font(62, True)
     pad_x, pad_y = 42, 28
     y = 1485
     # compute word positions centered
-    widths = [text_size(draw, w, fnt)[0] for w in words]
-    gap = 18
-    total = sum(widths) + gap * (len(words)-1)
+    gap = 22
+    max_total = WIDTH - 220
+    for size in range(62, 39, -2):
+        fnt = font(size, True)
+        widths = [text_size(draw, w, fnt)[0] for w in words]
+        total = sum(widths) + gap * (len(words)-1)
+        if total <= max_total:
+            break
     x = (WIDTH - total) // 2
     box = [max(35, x-pad_x), y-pad_y, min(WIDTH-35, x+total+pad_x), y+90+pad_y]
     rounded(draw, box, 32, (3, 7, 18), (59, 130, 246), 3)


terminal

Output:
{
  "final": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4",
  "package": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/review_package.json",
  "meta": {
    "width": 1080,
    "height": 1920,
    "sample_aspect_ratio": "1:1",
    "display_aspect_ratio": "9:16",
    "frame_rate": "30/1",
    "duration": 35.064
  },
  "sha256": "a183f517dd99a3f97aff5b983c92c6cf4db70804d8aab99cef0ce784ae6eab0a"
}


ffmpeg -y -hide_banner -loglevel error -ss 12 -i data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4 -frames:v 1 data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/qa_frame_00_12.png
ffmpeg -y -hide_banner -loglevel error -ss 31 -i data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4 -frames:v 1 data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/qa_frame_00_31.png
python - <<'PY'
import json, subprocess, pathlib
p='data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4'
print(subprocess.check_output(['ffprobe','-v','error','-select_streams','v:0','-show_entries','stream=width,height,sample_aspect_ratio,display_aspect_ratio','-show_entries','format=duration','-of','json',p],text=True))
PY


Output:
{
    "programs": [

    ],
    "streams": [
        {
            "width": 1080,
            "height": 1920,
            "sample_aspect_ratio": "1:1",
            "display_aspect_ratio": "9:16"
        }
    ],
    "format": {
        "duration": "35.064000"
    }
}


Question: QA this revised 9:16 frame: are subtitles and UI text readable, not clipped, within safe zones, and no black bars/aspect distortion? Concise.


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

Question: QA this revised 9:16 frame: are subtitles and UI text readable, not clipped, within safe zones, and no black bars/aspect distortion? Concise.


patch

*** Begin Patch
*** Update File: /home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@
-        # Keep active-word caption groups compact on mobile. Four/five word groups
-        # are readable in theory but collide in 1080-wide Shorts once uppercased.
-        if len(current) >= 3 or token.endswith(('.', '?', '!')):
+        # Keep active-word caption groups compact on mobile and avoid visually
+        # merging adjacent uppercased words.
+        if len(current) >= 2 or token.endswith(('.', '?', '!')):
@@
-    small = font(34, False)
-    med = font(48, True)
+    small = font(34, False)
+    med = font(48, True)
+    sub_font = font(42, True)
@@
-    for j, line in enumerate(wrap_by_pixels(draw, sub, med, 760)[:2]):
-        w, _ = text_size(draw, line, med)
-        draw.text(((WIDTH-w)//2, 1168 + j*54), line, font=med, fill=(15, 23, 42))
+    for j, line in enumerate(wrap_by_pixels(draw, sub, sub_font, 760)[:2]):
+        w, _ = text_size(draw, line, sub_font)
+        draw.text(((WIDTH-w)//2, 1140 + j*48), line, font=sub_font, fill=(15, 23, 42))
@@
-    gap = 22
+    gap = 40
*** End Patch


--- a//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
+++ b//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@ -120,9 +120,9 @@
     for w in words:
         current.append(w)
         token = str(w["word"])
-        # Keep active-word caption groups compact on mobile. Four/five word groups
-        # are readable in theory but collide in 1080-wide Shorts once uppercased.
-        if len(current) >= 3 or token.endswith(('.', '?', '!')):
+        # Keep active-word caption groups compact on mobile and avoid visually
+        # merging adjacent uppercased words.
+        if len(current) >= 2 or token.endswith(('.', '?', '!')):
             segments.append(current)
             current = []
     if current:
@@ -182,6 +182,7 @@
     _, _, label, main, sub = scene
     small = font(34, False)
     med = font(48, True)
+    sub_font = font(42, True)
     large = font(58, True)
     card_x1, card_y1, card_x2, card_y2 = 90, 235, 990, 1265
     rounded(draw, [card_x1, card_y1, card_x2, card_y2], 44, (248, 250, 252), (59, 130, 246), 4)
@@ -210,9 +211,9 @@
         if i <= active:
             draw.text((845, y+16), "✓", font=font(38, True), fill=(22, 163, 74))
     # subline / rule
-    for j, line in enumerate(wrap_by_pixels(draw, sub, med, 760)[:2]):
-        w, _ = text_size(draw, line, med)
-        draw.text(((WIDTH-w)//2, 1168 + j*54), line, font=med, fill=(15, 23, 42))
+    for j, line in enumerate(wrap_by_pixels(draw, sub, sub_font, 760)[:2]):
+        w, _ = text_size(draw, line, sub_font)
+        draw.text(((WIDTH-w)//2, 1140 + j*48), line, font=sub_font, fill=(15, 23, 42))

 def draw_subtitles(draw: ImageDraw.ImageDraw, seg, active: int):
@@ -225,7 +226,7 @@
     pad_x, pad_y = 42, 28
     y = 1485
     # compute word positions centered
-    gap = 22
+    gap = 40
     max_total = WIDTH - 220
     for size in range(62, 39, -2):
         fnt = font(size, True)


terminal

python scripts/render_next_post_ai004.py && ffmpeg -y -hide_banner -loglevel error -ss 12 -i data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4 -frames:v 1 data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/qa_frame_00_12.png && ffmpeg -y -hide_banner -loglevel error -ss 31 -i data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4 -frames:v 1 data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/qa_frame_00_31.png


Output:
{
  "final": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4",
  "package": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/review_package.json",
  "meta": {
    "width": 1080,
    "height": 1920,
    "sample_aspect_ratio": "1:1",
    "display_aspect_ratio": "9:16",
    "frame_rate": "30/1",
    "duration": 35.064
  },
  "sha256": "57f210fcb99c3e666baa8aa6168ad7658de19247eb085d3cab03ab3eabd4df4c"
}


Question: QA final revised frame: subtitles and UI text readable/not clipped? Any black bars or squeezed aspect ratio?


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

Question: QA final revised frame: subtitles and UI text readable/not clipped? Any black bars or squeezed aspect ratio?


patch

*** Begin Patch
*** Update File: /home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@
-    for j, line in enumerate(wrap_by_pixels(draw, sub, sub_font, 760)[:2]):
+    sub_lines = wrap_by_pixels(draw, sub, sub_font, 760)[:2]
+    sub_start_y = 1188 if len(sub_lines) == 1 else 1138
+    for j, line in enumerate(sub_lines):
         w, _ = text_size(draw, line, sub_font)
-        draw.text(((WIDTH-w)//2, 1140 + j*48), line, font=sub_font, fill=(15, 23, 42))
+        draw.text(((WIDTH-w)//2, sub_start_y + j*48), line, font=sub_font, fill=(15, 23, 42))
@@
-    fnt = font(62, True)
+    fnt = font(52, True)
@@
-    gap = 40
+    gap = 74
     max_total = WIDTH - 220
-    for size in range(62, 39, -2):
+    for size in range(52, 37, -2):
*** End Patch


--- a//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
+++ b//home/agent/jarvis_runtime/AutoShortsBot/scripts/render_next_post_ai004.py
@@ -211,9 +211,11 @@
         if i <= active:
             draw.text((845, y+16), "✓", font=font(38, True), fill=(22, 163, 74))
     # subline / rule
-    for j, line in enumerate(wrap_by_pixels(draw, sub, sub_font, 760)[:2]):
+    sub_lines = wrap_by_pixels(draw, sub, sub_font, 760)[:2]
+    sub_start_y = 1188 if len(sub_lines) == 1 else 1138
+    for j, line in enumerate(sub_lines):
         w, _ = text_size(draw, line, sub_font)
-        draw.text(((WIDTH-w)//2, 1140 + j*48), line, font=sub_font, fill=(15, 23, 42))
+        draw.text(((WIDTH-w)//2, sub_start_y + j*48), line, font=sub_font, fill=(15, 23, 42))

 def draw_subtitles(draw: ImageDraw.ImageDraw, seg, active: int):
@@ -222,13 +224,13 @@
     words = [str(w["word"]).strip() for w in seg if str(w["word"]).strip()]
     if not words:
         return
-    fnt = font(62, True)
+    fnt = font(52, True)
     pad_x, pad_y = 42, 28
     y = 1485
     # compute word positions centered
-    gap = 40
+    gap = 74
     max_total = WIDTH - 220
-    for size in range(62, 39, -2):
+    for size in range(52, 37, -2):
         fnt = font(size, True)
         widths = [text_size(draw, w, fnt)[0] for w in words]
         total = sum(widths) + gap * (len(words)-1)


terminal

Output:
{
  "final": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/ai-004_stop_asking_ai_to_think_for_you_1080x1920_review.mp4",
  "package": "/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/review_package.json",
  "meta": {
    "width": 1080,
    "height": 1920,
    "sample_aspect_ratio": "1:1",
    "display_aspect_ratio": "9:16",
    "frame_rate": "30/1",
    "duration": 35.064
  },
  "sha256": "4503c32a0ec14b739b531b324bb78fb086d4bf74174b88d3e7394e2c99f7f42e"
}


Question: Final QA: are subtitles readable enough with visible spacing? Is UI text clipped? Any black bars or aspect distortion? Concise.


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

Question: Final QA: are subtitles readable enough with visible spacing? Is UI text clipped? Any black bars or aspect distortion? Concise.


Question: QA the final-rule frame: is the rule visual visible/readable and not clipped? Any black bars/aspect distortion?


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

Question: QA the final-rule frame: is the rule visual visible/readable and not clipped? Any black bars/aspect distortion?


Explanation:
**Reviewing package requirements**

I’m considering if I need to match the JSON hash after regeneration since it doesn't change with QA extraction. Maybe I should read the package hash and update the todo list. Running tests could be necessary since we added a script but no tests yet. I need to ensure the final version, especially the media, gets approved in German, while keeping the information about the published status private. Also, I might include the MEDIA path and relevant commands.

todo

Nächsten Post-Kandidaten auswählen und Produktionsartefakte vorbereiten

9:16/Audio/Subtitles/QA verifizieren und Review-Package liefern


Repo-Zustand und nächste verfügbare Video-Kandidaten prüfen


Erledigt, Sir. Ich habe als nächsten Post-Kandidaten **ai-004 — “Stop asking AI to think for you”** vorbereitet.

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

**Status:** Review-/Freigabe-Version, noch nicht gepostet.

**QA geprüft:**
- Format: **1080×1920**
- SAR: **1:1**
- DAR: **9:16**
- FPS: **30**
- Dauer: **35.064s**
- Keine schwarzen Balken / kein zusammengedrücktes Bild
- UI-Text und aktive Wort-Untertitel geprüft
- Finalsprache: **Englisch**
- Voice: `en-GB-RyanNeural`
- Posting-Side-Effects: **keine**

**SHA256:**
`4503c32a0ec14b739b531b324bb78fb086d4bf74174b88d3e7394e2c99f7f42e`

**Caption-Vorschlag:**
AI is more useful as a critic than as a guru.

**Hashtags:**
`#AI #Productivity #Workflow #DecisionMaking #TrueTraceShorts`

**Freigabe-Befehle:**
- `Freigabe YouTube`
- `Freigabe TikTok`
- `Freigabe Instagram`
- `Freigabe alle`
- `Ablehnen`
- `Ändern: <Wunsch>`

Review-Package liegt hier:
`/home/agent/jarvis_runtime/AutoShortsBot/data/post_candidates/ai-004-stop-asking-ai-to-think-for-you/review_package.json`