agentic-ads-engine 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,230 @@
1
+ [
2
+ {
3
+ "id": "screen-voiceover",
4
+ "kinds": ["captions"],
5
+ "feedbackRules": ["R1", "R6", "R7", "R10", "R12", "R14", "R17", "R18", "R19", "R23"],
6
+ "name": "Screen recording + voiceover",
7
+ "description": "Record a real or generic checklist UI with timed narration.",
8
+ "aspectRatios": ["9:16"],
9
+ "durationSeconds": {"min": 15, "max": 25},
10
+ "hookPattern": "Cursor clicks the key step on frame one.",
11
+ "stages": ["script", "still", "tts", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
12
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "supplied art / SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "tts": {"model": "Kokoro local", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
13
+ "costClass": "free",
14
+ "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
15
+ "policyRisk": "low",
16
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "No real people or brands."],
17
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation"],
18
+ "learned": ["Record only generic UI; exclude real addresses and prices.", "Cut on measured voice beats; dead air must stay below 0.4 seconds."],
19
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md"]
20
+ },
21
+ {
22
+ "id": "text-broll",
23
+ "kinds": ["captions"],
24
+ "feedbackRules": ["R1", "R6", "R7", "R10", "R12", "R14", "R17", "R18", "R19", "R23"],
25
+ "name": "Text over b-roll",
26
+ "description": "Animate exact caption cards over supplied moving footage.",
27
+ "aspectRatios": ["9:16"],
28
+ "durationSeconds": {"min": 15, "max": 25},
29
+ "hookPattern": "Bold hook text appears on frame one.",
30
+ "stages": ["script", "still", "tts", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
31
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "bytedance/seedream-v5.5", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0.02, "note": "estimate: supplied art or 1 text-to-image still at $0.020; videos6/spend.json and videos4/spend.json"}}, "tts": {"model": "Kokoro local", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
32
+ "costClass": "cheap",
33
+ "estimatedCostUsd": {"low": 0, "high": 0.02, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
34
+ "policyRisk": "low",
35
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "No real people or brands."],
36
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation"],
37
+ "learned": ["Render text as HTML overlays, never generated pixels.", "Use at most eight words per card; hold the end card for 1.5 seconds."],
38
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md", "videos6/spend.json", "videos4/spend.json"]
39
+ },
40
+ {
41
+ "id": "whiteboard",
42
+ "kinds": ["captions"],
43
+ "feedbackRules": ["R1", "R6", "R7", "R10", "R12", "R14", "R17", "R18", "R19", "R23"],
44
+ "name": "Whiteboard doodle",
45
+ "description": "Draw SVG ideas to narration word times.",
46
+ "aspectRatios": ["9:16"],
47
+ "durationSeconds": {"min": 15, "max": 25},
48
+ "hookPattern": "The marker draws before the first word ends.",
49
+ "stages": ["script", "still", "tts", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
50
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "supplied art / SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "tts": {"model": "Kokoro local", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
51
+ "costClass": "free",
52
+ "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
53
+ "policyRisk": "low",
54
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "No real people or brands."],
55
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation"],
56
+ "learned": ["Use a faceless renderer; the whiteboard face recipe needs a face plate.", "Trigger strokes within three frames of their words; avoid static boards over two seconds."],
57
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md"]
58
+ },
59
+ {
60
+ "id": "stickman",
61
+ "kinds": ["captions", "story", "character"],
62
+ "feedbackRules": ["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R14", "R15", "R17", "R18", "R19", "R20", "R23", "R26"],
63
+ "name": "Stickman explainer",
64
+ "description": "Animate a shared SVG character through a problem and answer.",
65
+ "aspectRatios": ["9:16"],
66
+ "durationSeconds": {"min": 15, "max": 25},
67
+ "hookPattern": "The figure moves under a question mark in the first second.",
68
+ "stages": ["script", "still", "tts", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
69
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "supplied art / SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "tts": {"model": "Kokoro local", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
70
+ "costClass": "free",
71
+ "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
72
+ "policyRisk": "low",
73
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "No real people or brands."],
74
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation"],
75
+ "learned": ["Reuse one SVG character asset for consistent proportions.", "Include motion every two seconds; avoid holds over 2.5 seconds."],
76
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md"]
77
+ },
78
+ {
79
+ "id": "pov",
80
+ "kinds": ["captions", "story", "character"],
81
+ "feedbackRules": ["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R14", "R15", "R17", "R18", "R19", "R20", "R23", "R26"],
82
+ "name": "POV discovery",
83
+ "description": "Use consistent first-person clips with a title and checklist reveal.",
84
+ "aspectRatios": ["9:16"],
85
+ "durationSeconds": {"min": 15, "max": 25},
86
+ "hookPattern": "POV title and moving hands appear on frame one.",
87
+ "stages": ["script", "set", "character", "still", "angle_still", "tts", "image_to_video", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
88
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "set": {"model": "reuse supplied set", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "character": {"model": "reuse character bible", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "bytedance/seedream-v5.5", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.02, "high": 0.06, "note": "estimate: 1-3 text-to-image stills at $0.020; videos6/spend.json and videos4/spend.json"}}, "angle_still": {"model": "bytedance/seedream-v5.5/edit", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.056, "high": 0.196, "note": "estimate: 2-7 image edits at $0.028; sitcom3/spend.json, videos6/spend.json, videos4/spend.json"}}, "tts": {"model": "wavespeed-ai/qwen3-tts/text-to-speech", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.03, "high": 0.055, "note": "estimate: 6-11 calls at $0.005; sitcom3/spend.json, videos5/spend.json, videos4/spend.json"}}, "image_to_video": {"model": "wavespeed-ai/wan-2.2/i2v-480p-ultra-fast", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.15, "high": 0.3, "note": "estimate: 3-6 five-second clips at $0.05; sitcom3/spend.json, videos4/spend.json, videos6/spend.json"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
89
+ "costClass": "mid",
90
+ "estimatedCostUsd": {"low": 0.256, "high": 0.611, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
91
+ "policyRisk": "medium",
92
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "Use a DRAMATIZATION label for fictional people and skits.", "No real people. Synthetic people only; no real or named person; no children; no likeness of a real person."],
93
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation", "continuity", "words_per_second", "black_corners", "eyes"],
94
+ "learned": ["Chain the final frame into the next shot for continuity.", "Cut on voice beats; keep real listing data out of backgrounds."],
95
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md", "videos6/spend.json", "videos4/spend.json", "sitcom3/spend.json", "videos5/spend.json"]
96
+ },
97
+ {
98
+ "id": "telestrator",
99
+ "kinds": ["captions"],
100
+ "feedbackRules": ["R1", "R6", "R7", "R10", "R12", "R14", "R17", "R18", "R19", "R23"],
101
+ "name": "Telestrator annotation",
102
+ "description": "Circle relevant UI details with word-timed SVG strokes.",
103
+ "aspectRatios": ["9:16"],
104
+ "durationSeconds": {"min": 15, "max": 25},
105
+ "hookPattern": "The pen circles the problem in the first second.",
106
+ "stages": ["script", "still", "tts", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
107
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "supplied art / SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "tts": {"model": "Kokoro local", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
108
+ "costClass": "free",
109
+ "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
110
+ "policyRisk": "low",
111
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "No real people or brands."],
112
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation"],
113
+ "learned": ["Keep annotations clear of the element they explain.", "Use deterministic strokes to avoid drift between frames."],
114
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md"]
115
+ },
116
+ {
117
+ "id": "before-after",
118
+ "kinds": ["captions", "story"],
119
+ "feedbackRules": ["R1", "R2", "R5", "R6", "R7", "R9", "R10", "R12", "R14", "R17", "R18", "R19", "R20", "R23", "R26"],
120
+ "name": "Before and after",
121
+ "description": "Wipe from confusing paperwork to an actual checklist.",
122
+ "aspectRatios": ["9:16"],
123
+ "durationSeconds": {"min": 15, "max": 25},
124
+ "hookPattern": "A split wipe begins on frame one.",
125
+ "stages": ["script", "set", "character", "still", "angle_still", "tts", "image_to_video", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
126
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "set": {"model": "reuse supplied set", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "character": {"model": "reuse character bible", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "bytedance/seedream-v5.5", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.02, "high": 0.06, "note": "estimate: 1-3 text-to-image stills at $0.020; videos6/spend.json and videos4/spend.json"}}, "angle_still": {"model": "bytedance/seedream-v5.5/edit", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.056, "high": 0.196, "note": "estimate: 2-7 image edits at $0.028; sitcom3/spend.json, videos6/spend.json, videos4/spend.json"}}, "tts": {"model": "wavespeed-ai/qwen3-tts/text-to-speech", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.03, "high": 0.055, "note": "estimate: 6-11 calls at $0.005; sitcom3/spend.json, videos5/spend.json, videos4/spend.json"}}, "image_to_video": {"model": "wavespeed-ai/wan-2.2/i2v-480p-ultra-fast", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.15, "high": 0.3, "note": "estimate: 3-6 five-second clips at $0.05; sitcom3/spend.json, videos4/spend.json, videos6/spend.json"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
127
+ "costClass": "mid",
128
+ "estimatedCostUsd": {"low": 0.256, "high": 0.611, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
129
+ "policyRisk": "medium",
130
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "Use a DRAMATIZATION label for fictional people and skits.", "No real people. Synthetic people only; no real or named person; no children; no likeness of a real person."],
131
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation", "continuity", "words_per_second", "black_corners", "eyes"],
132
+ "learned": ["Align both views before the wipe; match their colour grade.", "Show an achievable checklist result, never promised savings."],
133
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md", "videos6/spend.json", "videos4/spend.json", "sitcom3/spend.json", "videos5/spend.json"]
134
+ },
135
+ {
136
+ "id": "news-parody",
137
+ "kinds": ["captions"],
138
+ "feedbackRules": ["R1", "R6", "R7", "R10", "R12", "R14", "R17", "R18", "R19", "R23"],
139
+ "name": "News parody",
140
+ "description": "Use branded news cards with sourced facts and narration.",
141
+ "aspectRatios": ["9:16"],
142
+ "durationSeconds": {"min": 15, "max": 25},
143
+ "hookPattern": "A sting and branded headline start on frame one.",
144
+ "stages": ["script", "still", "tts", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
145
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "supplied art / SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "tts": {"model": "Kokoro local", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
146
+ "costClass": "free",
147
+ "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
148
+ "policyRisk": "medium",
149
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "Label parody; do not impersonate a real news outlet.", "No real people. Synthetic people only; no real or named person; no children; no likeness of a real person."],
150
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation"],
151
+ "learned": ["Trace every fact to a source; never imply a real outlet endorsement.", "Use generic branding and make the parody clear."],
152
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md"]
153
+ },
154
+ {
155
+ "id": "lo-fi",
156
+ "kinds": ["captions", "story", "character", "lo-fi"],
157
+ "feedbackRules": ["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R14", "R15", "R16", "R17", "R18", "R19", "R20", "R23", "R26"],
158
+ "name": "Handheld lo-fi UGC",
159
+ "description": "Use synthetic handheld scenes and narration with fast reaction cuts.",
160
+ "aspectRatios": ["9:16"],
161
+ "durationSeconds": {"min": 15, "max": 25},
162
+ "hookPattern": "A character is already talking or reacting with camera motion.",
163
+ "stages": ["script", "set", "character", "still", "angle_still", "tts", "image_to_video", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
164
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "set": {"model": "reuse supplied set", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "character": {"model": "reuse character bible", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "bytedance/seedream-v5.5", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.02, "high": 0.06, "note": "estimate: 1-3 text-to-image stills at $0.020; videos6/spend.json and videos4/spend.json"}}, "angle_still": {"model": "bytedance/seedream-v5.5/edit", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.056, "high": 0.196, "note": "estimate: 2-7 image edits at $0.028; sitcom3/spend.json, videos6/spend.json, videos4/spend.json"}}, "tts": {"model": "wavespeed-ai/qwen3-tts/voice-design", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.03, "high": 0.08, "note": "estimate: 6-16 calls at $0.005; videos6/spend.json; includes regenerated lines"}}, "image_to_video": {"model": "wavespeed-ai/wan-2.2/i2v-480p-ultra-fast", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.15, "high": 0.3, "note": "estimate: 3-6 five-second clips at $0.05; sitcom3/spend.json, videos4/spend.json, videos6/spend.json"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
165
+ "costClass": "mid",
166
+ "estimatedCostUsd": {"low": 0.256, "high": 0.636, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries; videos6 $0.881 for three reworked ads with reuse; videos5 $0.165 TTS-only for three edits, not fresh generation"},
167
+ "policyRisk": "medium",
168
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "Use a DRAMATIZATION label for fictional people and skits.", "No real people. Synthetic people only; no real or named person; no children; no likeness of a real person."],
169
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation", "continuity", "words_per_second", "black_corners", "eyes"],
170
+ "learned": ["No still frames as shots; animate source stills before the edit.", "Keep voice at its native pace; check words per second below 3.4.", "Exclude knob-to-lever morphs and fused fingers before cutting."],
171
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md", "videos6/spend.json", "videos6/voices.md", "videos6/qa.json", "videos5/EDIT_NOTES.md", "videos4/spend.json", "sitcom3/spend.json", "videos5/spend.json"]
172
+ },
173
+ {
174
+ "id": "sitcom",
175
+ "kinds": ["captions", "story", "character", "dialogue"],
176
+ "feedbackRules": ["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15", "R17", "R18", "R19", "R20", "R21", "R22", "R23", "R26"],
177
+ "name": "Multi-camera sitcom",
178
+ "description": "Cut fictional dialogue between shared sets, angles, and silent reactions.",
179
+ "aspectRatios": ["9:16"],
180
+ "durationSeconds": {"min": 18, "max": 22},
181
+ "hookPattern": "A hand turns the knob before the door falls.",
182
+ "stages": ["script", "set", "character", "still", "angle_still", "tts", "image_to_video", "talking_head", "laugh_track", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
183
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "set": {"model": "reuse supplied set", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "character": {"model": "reuse character bible", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "bytedance/seedream-v5.5", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.02, "high": 0.06, "note": "estimate: 1-3 text-to-image stills at $0.020; videos6/spend.json and videos4/spend.json"}}, "angle_still": {"model": "bytedance/seedream-v5.5/edit", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.056, "high": 0.196, "note": "estimate: 2-7 image edits at $0.028; sitcom3/spend.json, videos6/spend.json, videos4/spend.json"}}, "tts": {"model": "wavespeed-ai/qwen3-tts/text-to-speech", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.03, "high": 0.055, "note": "estimate: 6-11 calls at $0.005; sitcom3/spend.json, videos5/spend.json, videos4/spend.json"}}, "image_to_video": {"model": "wavespeed-ai/wan-2.2/i2v-480p-ultra-fast", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.15, "high": 0.3, "note": "estimate: 3-6 five-second clips at $0.05; sitcom3/spend.json, videos4/spend.json, videos6/spend.json"}}, "talking_head": {"model": "wavespeed-ai/wan-2.2/speech-to-video", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 1.5, "high": 1.8, "note": "estimate: 10-12 calls at $0.15 (480p); sitcom3/spend.json; includes test and retry; other endpoint trials excluded"}}, "laugh_track": {"model": "Mixkit laugh/clap + CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "sitcom3: reused licensed sounds at $0"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
184
+ "costClass": "expensive",
185
+ "estimatedCostUsd": {"low": 1.756, "high": 2.411, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries; sitcom3 actual $2.466 includes trials and one retry, four mouth checks failed"},
186
+ "policyRisk": "medium",
187
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "Use a DRAMATIZATION label for fictional people and skits.", "No real people. Synthetic people only; no real or named person; no children; no likeness of a real person."],
188
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation", "continuity", "words_per_second", "black_corners", "eyes", "mouth_sync"],
189
+ "learned": ["Use mediums and close-ups for speech; inspect eyes at full size.", "Cut on action for the door fall: knob, crash, then aftermath.", "No still frames as shots; laughs belong on silent reaction shots.", "Trim talks to line plus 0.1 seconds; time captions from actual words.", "Wan passed only six of ten mouth-sync checks; a retry still failed."],
190
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md", "sitcom3/NOTES.md", "sitcom3/qa.json", "videos6/spend.json", "videos4/spend.json", "sitcom3/spend.json", "videos5/spend.json"]
191
+ },
192
+ {
193
+ "id": "storytime",
194
+ "kinds": ["captions", "story", "character"],
195
+ "feedbackRules": ["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R12", "R14", "R15", "R17", "R18", "R19", "R20", "R23", "R26"],
196
+ "name": "Storytime twist",
197
+ "description": "A fictional narrator moves from confession to checklist payoff.",
198
+ "aspectRatios": ["9:16"],
199
+ "durationSeconds": {"min": 15, "max": 25},
200
+ "hookPattern": "The narrator starts mid-story in the first second.",
201
+ "stages": ["script", "set", "character", "still", "angle_still", "tts", "image_to_video", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
202
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "set": {"model": "reuse supplied set", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "character": {"model": "reuse character bible", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "bytedance/seedream-v5.5", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.02, "high": 0.06, "note": "estimate: 1-3 text-to-image stills at $0.020; videos6/spend.json and videos4/spend.json"}}, "angle_still": {"model": "bytedance/seedream-v5.5/edit", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.056, "high": 0.196, "note": "estimate: 2-7 image edits at $0.028; sitcom3/spend.json, videos6/spend.json, videos4/spend.json"}}, "tts": {"model": "wavespeed-ai/qwen3-tts/text-to-speech", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.03, "high": 0.055, "note": "estimate: 6-11 calls at $0.005; sitcom3/spend.json, videos5/spend.json, videos4/spend.json"}}, "image_to_video": {"model": "wavespeed-ai/wan-2.2/i2v-480p-ultra-fast", "verified": "2026-11-12", "unverified": false, "estimatedCostUsd": {"low": 0.15, "high": 0.3, "note": "estimate: 3-6 five-second clips at $0.05; sitcom3/spend.json, videos4/spend.json, videos6/spend.json"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
203
+ "costClass": "mid",
204
+ "estimatedCostUsd": {"low": 0.256, "high": 0.611, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
205
+ "policyRisk": "medium",
206
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "Use a DRAMATIZATION label for fictional people and skits.", "No real people. Synthetic people only; no real or named person; no children; no likeness of a real person."],
207
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation", "continuity", "words_per_second", "black_corners", "eyes"],
208
+ "learned": ["Keep the character bible identical across shots.", "Use a fictional framing and a checklist payoff, not a testimonial."],
209
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md", "videos6/spend.json", "videos4/spend.json", "sitcom3/spend.json", "videos5/spend.json"]
210
+ },
211
+ {
212
+ "id": "talking-object",
213
+ "kinds": ["captions", "character", "speech"],
214
+ "feedbackRules": ["R1", "R3", "R4", "R6", "R7", "R8", "R10", "R11", "R12", "R14", "R15", "R17", "R18", "R19", "R20", "R23", "R26"],
215
+ "name": "Talking checklist",
216
+ "description": "Animate an illustrated object mouth to narration.",
217
+ "aspectRatios": ["9:16"],
218
+ "durationSeconds": {"min": 15, "max": 25},
219
+ "hookPattern": "The paper talks and moves on frame one.",
220
+ "stages": ["script", "still", "tts", "sfx", "captions", "edit", "end_card", "mute_test", "qa"],
221
+ "providers": {"script": {"model": "supplied script / human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "still": {"model": "supplied art / SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "tts": {"model": "Kokoro local", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "sfx": {"model": "ffmpeg lavfi / licensed CC0", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "captions": {"model": "whisper.cpp + HTML", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "edit": {"model": "ffmpeg + SVG", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "end_card": {"model": "HTML overlay", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "mute_test": {"model": "human review of muted render", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}, "qa": {"model": "ffmpeg + human review", "verified": "2026-11-12", "unverified": true, "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: supplied assets or local processing; excludes labour and compute"}}},
222
+ "costClass": "free",
223
+ "estimatedCostUsd": {"low": 0, "high": 0, "note": "estimate: sum of stage assumptions; styles catalog recipes; excludes labour, compute, and unbounded retries"},
224
+ "policyRisk": "low",
225
+ "riskNotes": ["Use the housing special ad category for housing ads.", "No fake testimonials, eligibility claims, or promised savings.", "No real people. Synthetic people only; no real or named person; no children; no likeness of a real person."],
226
+ "qaGates": ["caption_timing", "dead_air", "tail", "shot_length", "mute_test", "orientation", "mouth_sync"],
227
+ "learned": ["Use an illustrated object with simple mouth flaps.", "Keep all readable text in exact overlays."],
228
+ "sources": ["styles/out-styles.md", "storyboards/INDEX.md", "editing/eval3/engine-ideas.md"]
229
+ }
230
+ ]
@@ -0,0 +1,82 @@
1
+ import { getFormat } from '../formats/index.js';
2
+ import { readFileSync } from 'node:fs';
3
+ import { mkdir, readFile, open, rename, unlink } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ export const hooks = Object.freeze(JSON.parse(readFileSync(new URL('./library.json', import.meta.url), 'utf8'))
7
+ .map(hook => Object.freeze({ ...hook, tags: Object.freeze(hook.tags), formats: Object.freeze(hook.formats) })));
8
+ export const emotions = Object.freeze(['shock', 'fear', 'curiosity', 'relief', 'humor', 'pride']);
9
+ /** Filters combine with AND; returned descriptors are independent copies. */
10
+ export function listHooks(filter = {}) {
11
+ if (!filter || typeof filter !== 'object' || Array.isArray(filter) ||
12
+ Object.keys(filter).some(k => !['format', 'emotion', 'housing'].includes(k))) throw new Error('Invalid hook filters');
13
+ if (filter.format !== undefined) getFormat(filter.format);
14
+ if (filter.emotion !== undefined && !emotions.includes(filter.emotion)) throw new Error('Invalid emotion');
15
+ if (filter.housing !== undefined && typeof filter.housing !== 'boolean') throw new Error('Invalid housing filter');
16
+ return structuredClone(hooks.filter(h => (filter.format === undefined || h.formats.includes(filter.format)) &&
17
+ (filter.emotion === undefined || h.emotion === filter.emotion) && (!filter.housing || h.housingSafe)));
18
+ }
19
+ export function getHook(id) {
20
+ const hook = hooks.find(h => h.id === id);
21
+ if (!hook) throw new Error('Unknown hook: ' + String(id));
22
+ return structuredClone(hook);
23
+ }
24
+ /** Rank only matching hooks. Catalog order resolves equal scores. */
25
+ export function pickHooks(filter, scores = {}) {
26
+ if (!filter || filter.format === undefined) throw new Error('Missing required argument: format');
27
+ validate(scores);
28
+ return listHooks(filter).sort((a, b) => (scores[b.id] ?? 0) - (scores[a.id] ?? 0));
29
+ }
30
+ function validate(scores) {
31
+ if (!scores || typeof scores !== 'object' || Array.isArray(scores)) throw new Error('Invalid scores');
32
+ for (const [id, value] of Object.entries(scores)) {
33
+ if (!hooks.some(h => h.id === id) || !Number.isFinite(value) || value < 0) throw new Error('Invalid score');
34
+ }
35
+ return scores;
36
+ }
37
+ function pathFor(directory, site) {
38
+ if (typeof site !== 'string' || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(site)) throw new Error('Invalid site slug');
39
+ return join(directory, `${site}.json`);
40
+ }
41
+ /** Read an explicit site snapshot. Missing files are a cold start; corrupt files fail. */
42
+ export async function readScores(directory, site) {
43
+ const path = pathFor(directory, site);
44
+ try { return validate(JSON.parse(await readFile(path, 'utf8'))); }
45
+ catch (error) { if (error.code === 'ENOENT') return {}; throw new Error('Cannot read scores'); }
46
+ }
47
+ /** Replace a site's snapshot atomically. Callers must use a single writer. */
48
+ export async function writeScores(directory, site, scores) {
49
+ const path = pathFor(directory, site);
50
+ const data = JSON.stringify(validate(scores));
51
+ await mkdir(directory, { recursive: true });
52
+ const temp = `${path}.${randomUUID()}.tmp`;
53
+ let handle;
54
+ let created = false;
55
+ try {
56
+ handle = await open(temp, 'wx', 0o600);
57
+ created = true;
58
+ await handle.writeFile(data);
59
+ await handle.sync();
60
+ await handle.close();
61
+ handle = undefined;
62
+ await rename(temp, path);
63
+ } catch (error) {
64
+ // Cleanup must never replace the original write, sync, close, or rename error.
65
+ if (handle) await handle.close().catch(() => {});
66
+ if (created) await unlink(temp).catch(() => {});
67
+ throw error;
68
+ }
69
+ }
70
+ /** Pure selection; scores belong to the supplied site snapshot. No disk, time, or random input.
71
+ * @param {{id:string, scores?:Record<string,number>}} site
72
+ * @param {string} goal
73
+ */
74
+ export function selectFor(site, goal, filter = {}) {
75
+ if (!site || typeof site !== 'object' || Array.isArray(site)) throw new Error('Invalid site');
76
+ pathFor('.', site.id);
77
+ const scores = validate(site.scores ?? {});
78
+ const eligible = listHooks(filter).filter(h => h.risk === 'low' && h.tags.includes(goal));
79
+ const ranked = eligible.map((hook, order) => ({ hook, order, score: scores[hook.id] ?? 0 }));
80
+ ranked.sort((a, b) => b.score - a.score || a.order - b.order);
81
+ return ranked.length ? structuredClone(ranked[0].hook) : null;
82
+ }
@@ -0,0 +1,44 @@
1
+ [
2
+ {"id":"door","tags":["discovery"],"pattern":"Take a closer look at {{subject}}","risk":"low","kind":"spoken line","formats":["pov","lo-fi"],"emotion":"curiosity","housingSafe":true,"note":"A direct invitation opens a reveal without making a claim."},
3
+ {"id":"compare","tags":["comparison","discovery"],"pattern":"Compare {{subject}} with the details in view","risk":"low","kind":"caption","formats":["text-broll","telestrator"],"emotion":"curiosity","housingSafe":true,"note":"Visible details give a comparison something concrete to show."},
4
+ {"id":"help","tags":["discovery","help"],"pattern":"Need help exploring {{subject}}? Start here","risk":"low","kind":"spoken line","formats":["screen-voiceover","whiteboard"],"emotion":"relief","housingSafe":true,"note":"A clear question leads directly into the useful step."},
5
+ {"id":"ugc","tags":["experience"],"pattern":"Here is what I observed about {{subject}}","risk":"medium","kind":"spoken line","formats":["lo-fi","storytime"],"emotion":"pride","housingSafe":false,"note":"First-person delivery adds immediacy; observations need verification."},
6
+ {"id":"before-after","tags":["comparison"],"pattern":"Before and after: {{subject}}. Virtual staging preview.","risk":"medium","kind":"first-frame visual","formats":["before-after"],"emotion":"shock","housingSafe":false,"note":"A split view makes the change legible; staging needs review."},
7
+ {"id":"update","tags":["news"],"pattern":"Brand update: {{subject}} — {{source}}","risk":"high","kind":"caption","formats":["news-parody"],"emotion":"curiosity","housingSafe":false,"note":"An update creates a reason to listen; verify the source."},
8
+ {"id":"shocked-face","tags":["discovery"],"pattern":"Open on an illustrated shocked face; cut next to the cause: {{subject}}.","kind":"first-frame visual","formats":["pov","sitcom","storytime"],"emotion":"shock","risk":"medium","housingSafe":true,"note":"Reaction first creates a question that the next shot answers."},
9
+ {"id":"all-savings","tags":["discovery"],"pattern":"I spent all my savings on this house. {{subject}}","kind":"spoken line","formats":["storytime","lo-fi","sitcom"],"emotion":"fear","risk":"high","housingSafe":false,"note":"Personal stakes start a story quickly; do not present invented experience as testimony."},
10
+ {"id":"slick-agent","tags":["discovery"],"pattern":"The slick agent called {{subject}} risk-free. That broke my trust.","kind":"spoken line","formats":["sitcom","storytime"],"emotion":"shock","risk":"high","housingSafe":false,"note":"Broken trust supplies conflict; the quoted promise still fails policy lint."},
11
+ {"id":"someone-lives-there","tags":["discovery"],"pattern":"Wait, someone lives there? Check the occupancy details for {{subject}}.","kind":"spoken line","formats":["pov","storytime","sitcom"],"emotion":"shock","risk":"medium","housingSafe":false,"note":"An unexpected obstacle turns a reveal into a story; verify occupancy first."},
12
+ {"id":"scare-beat","tags":["discovery"],"pattern":"A brief suspense hit lands as {{subject}} appears; cut straight to the explanation.","kind":"sound","formats":["pov","sitcom","stickman"],"emotion":"fear","risk":"medium","housingSafe":true,"note":"A sound accent marks the obstacle without leaving dead air."},
13
+ {"id":"street-interview","tags":["discovery"],"pattern":"Street interview: What would you check first about {{subject}}?","kind":"spoken line","formats":["lo-fi","storytime"],"emotion":"curiosity","risk":"medium","housingSafe":false,"note":"A direct question opens a conversation; synthetic interviews can imply false testimony."},
14
+ {"id":"door-crack","tags":["discovery"],"pattern":"Start with a door already opening onto {{subject}}.","kind":"first-frame visual","formats":["pov","text-broll"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"Movement starts the reveal before an introduction slows it down."},
15
+ {"id":"key-turn","tags":["discovery"],"pattern":"A key clicks as the view opens onto {{subject}}.","kind":"sound","formats":["pov","text-broll"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"A sound tied to visible action focuses attention on the reveal."},
16
+ {"id":"caption-cut","tags":["discovery"],"pattern":"Pause here. Look closer at {{subject}}.","kind":"caption","formats":["text-broll"],"emotion":"shock","risk":"low","housingSafe":true,"note":"A short text interruption sets up a concrete visual payoff."},
17
+ {"id":"detail-circle","tags":["discovery"],"pattern":"Start with a pen circling a detail in {{subject}}.","kind":"first-frame visual","formats":["telestrator","whiteboard"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"An annotation directs attention to the evidence immediately."},
18
+ {"id":"cursor-open","tags":["discovery"],"pattern":"The cursor opens {{subject}} in the first frame.","kind":"first-frame visual","formats":["screen-voiceover"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"The useful action begins before the viewer has to wait."},
19
+ {"id":"marker-question","tags":["discovery"],"pattern":"Draw a question mark beside {{subject}} as the narration begins.","kind":"first-frame visual","formats":["whiteboard"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"Drawing and speech share a beat so the opening stays active."},
20
+ {"id":"paper-chaos","tags":["discovery"],"pattern":"Scattered notes slide into a checklist for {{subject}}.","kind":"first-frame visual","formats":["before-after","stickman"],"emotion":"relief","risk":"low","housingSafe":true,"note":"Visible organization supplies a payoff without promising a financial outcome."},
21
+ {"id":"tab-overload","tags":["discovery"],"pattern":"More tabs. Still questions about {{subject}}.","kind":"caption","formats":["text-broll","screen-voiceover"],"emotion":"humor","risk":"low","housingSafe":true,"note":"Dry humor makes a familiar research problem easy to recognize."},
22
+ {"id":"checklist-speaks","tags":["discovery"],"pattern":"I am the checklist. Ask me about {{subject}}.","kind":"spoken line","formats":["talking-object"],"emotion":"humor","risk":"low","housingSafe":true,"note":"A talking prop makes the useful tool part of the scene."},
23
+ {"id":"paper-eyebrow","tags":["discovery"],"pattern":"An illustrated checklist raises an eyebrow beside {{subject}}.","kind":"first-frame visual","formats":["talking-object","stickman"],"emotion":"humor","risk":"low","housingSafe":true,"note":"A small character reaction delivers a visual joke immediately."},
24
+ {"id":"mid-question","tags":["discovery"],"pattern":"And the next question is about {{subject}}.","kind":"spoken line","formats":["lo-fi","storytime"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"Starting inside a thought avoids a slow introduction."},
25
+ {"id":"look-again","tags":["discovery"],"pattern":"Look again at {{subject}}. What detail stands out?","kind":"spoken line","formats":["pov","telestrator"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"A focused question creates a reason to inspect the next shot."},
26
+ {"id":"prop-entry","tags":["discovery"],"pattern":"Hands bring a checklist for {{subject}} into the opening frame.","kind":"first-frame visual","formats":["lo-fi","pov"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"A useful prop creates movement and gives the narration a subject."},
27
+ {"id":"deadpan-paper","tags":["discovery"],"pattern":"{{subject}} brought paperwork to the party.","kind":"spoken line","formats":["sitcom","talking-object"],"emotion":"humor","risk":"low","housingSafe":true,"note":"A dry joke adds character without mocking people or communities."},
28
+ {"id":"question-stack","tags":["discovery"],"pattern":"{{subject}}: what needs a closer look?","kind":"caption","formats":["text-broll","pov"],"emotion":"fear","risk":"low","housingSafe":true,"note":"A question introduces uncertainty without inventing a threat."},
29
+ {"id":"slow-down","tags":["discovery"],"pattern":"Start with the details of {{subject}}.","kind":"spoken line","formats":["whiteboard","screen-voiceover"],"emotion":"relief","risk":"low","housingSafe":true,"note":"A simple next step turns uncertainty into a useful demonstration."},
30
+ {"id":"tick-reveal","tags":["discovery"],"pattern":"A soft tick lands as the checklist for {{subject}} opens.","kind":"sound","formats":["screen-voiceover","whiteboard"],"emotion":"relief","risk":"low","housingSafe":true,"note":"An action sound makes the tool reveal clear."},
31
+ {"id":"wipe-to-guide","tags":["discovery"],"pattern":"Wipe from loose notes to a guide for {{subject}}.","kind":"first-frame visual","formats":["before-after","text-broll"],"emotion":"relief","risk":"low","housingSafe":true,"note":"The contrast shows organization rather than a promised result."},
32
+ {"id":"side-by-side","tags":["discovery"],"pattern":"{{subject}}: put the details side by side.","kind":"caption","formats":["text-broll","telestrator"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"A comparison creates a clear visual task."},
33
+ {"id":"quiet-reveal","tags":["discovery"],"pattern":"A soft door creak introduces {{subject}}.","kind":"sound","formats":["pov","text-broll"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"A small physical sound supports the reveal without competing with speech."},
34
+ {"id":"record-scratch","tags":["discovery"],"pattern":"A short record scratch interrupts the gesture toward {{subject}}.","kind":"sound","formats":["sitcom","talking-object"],"emotion":"humor","risk":"low","housingSafe":true,"note":"A timed interruption gives the visual joke a clear beat."},
35
+ {"id":"heartbeat-question","tags":["discovery"],"pattern":"A quiet heartbeat underscores the question about {{subject}}; resolve it with the guide.","kind":"sound","formats":["pov","storytime"],"emotion":"fear","risk":"medium","housingSafe":true,"note":"Suspense leads into an answer rather than unsupported danger."},
36
+ {"id":"ready-notes","tags":["discovery"],"pattern":"Notes ready. Time to explore {{subject}}.","kind":"caption","formats":["text-broll","screen-voiceover"],"emotion":"pride","risk":"low","housingSafe":true,"note":"Preparation offers an achievable action rather than an outcome claim."},
37
+ {"id":"draw-the-route","tags":["discovery"],"pattern":"A drawn path connects the question about {{subject}} to a checklist.","kind":"first-frame visual","formats":["whiteboard","stickman"],"emotion":"relief","risk":"low","housingSafe":true,"note":"The visual explanation moves from problem to next step."},
38
+ {"id":"fictional-aside","tags":["discovery"],"pattern":"Fictional scene: the paperwork for {{subject}} gets its own chair.","kind":"spoken line","formats":["sitcom","storytime"],"emotion":"humor","risk":"medium","housingSafe":true,"note":"An obvious fictional joke gives the scene personality without false testimony."},
39
+ {"id":"envelope-open","tags":["discovery"],"pattern":"Open a generic envelope to reveal a guide to {{subject}}.","kind":"first-frame visual","formats":["pov","text-broll"],"emotion":"curiosity","risk":"medium","housingSafe":true,"note":"The physical reveal adds texture without making an offer claim."},
40
+ {"id":"empty-box","tags":["discovery"],"pattern":"{{subject}}: which detail fills this box?","kind":"caption","formats":["whiteboard","screen-voiceover"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"An unfinished visual invites attention to the explanation."},
41
+ {"id":"magnifier","tags":["discovery"],"pattern":"A drawn magnifier moves across the details of {{subject}}.","kind":"first-frame visual","formats":["stickman","telestrator"],"emotion":"curiosity","risk":"low","housingSafe":true,"note":"A moving focus point makes the inspection visible."},
42
+ {"id":"question-to-action","tags":["discovery"],"pattern":"Questions about {{subject}}? Open the guide alongside the details.","kind":"spoken line","formats":["screen-voiceover","whiteboard"],"emotion":"relief","risk":"low","housingSafe":true,"note":"A direct bridge connects the opening question to a useful action."},
43
+ {"id":"guide-in-hand","tags":["discovery"],"pattern":"A hand sets a guide beside the notes on {{subject}}.","kind":"first-frame visual","formats":["pov","lo-fi"],"emotion":"pride","risk":"low","housingSafe":true,"note":"An ordinary action shows preparation without claiming a purchase or result."}
44
+ ]
package/src/http.js ADDED
@@ -0,0 +1,104 @@
1
+ import { createServer } from 'node:http';
2
+ import { createHash, timingSafeEqual } from 'node:crypto';
3
+ import { execute } from './index.js';
4
+ const digest = value => createHash('sha256').update(value).digest();
5
+ const hasApply = value => value && typeof value === 'object' && Object.entries(value).some(([key, item]) => key === 'apply' || hasApply(item));
6
+ export async function serve(argv) {
7
+ const options = { host: '127.0.0.1', port: 8787 }, seen = new Set();
8
+ while (argv.length) {
9
+ const option = argv.shift(), split = option.indexOf('=');
10
+ const key = (split < 0 ? option : option.slice(0, split)).slice(2);
11
+ const value = split < 0 ? argv.shift() : option.slice(split + 1);
12
+ if (!option.startsWith('--') || !['host', 'port'].includes(key) || seen.has(key) || !value) throw new Error('Invalid serve option');
13
+ seen.add(key); options[key] = value;
14
+ }
15
+ if (!/^\d+$/.test(String(options.port)) || Number(options.port) > 65535) throw new Error('Invalid port');
16
+ options.port = Number(options.port);
17
+ const token = process.env.AGENTIC_ADS_TOKEN || '';
18
+ const loopback = ['localhost', '::1', '127.0.0.1'].includes(options.host);
19
+ if (!loopback && !token) throw new Error('Non-loopback host requires AGENTIC_ADS_TOKEN');
20
+ if (token && token !== token.trim()) throw new Error('AGENTIC_ADS_TOKEN must not start or end with spaces');
21
+ if (token && token.length < 32) throw new Error('AGENTIC_ADS_TOKEN needs 32 or more characters');
22
+ let boundPort = 0;
23
+ // Pin localhost to loopback rather than trusting hostname resolution.
24
+ if (options.host === 'localhost') options.host = '127.0.0.1';
25
+ const expected = digest('Bearer ' + token);
26
+ const cors = process.env.AGENTIC_ADS_CORS_ORIGIN;
27
+ if (cors) {
28
+ try { if (!/^https?:$/.test(new URL(cors).protocol) || new URL(cors).origin !== cors) throw new Error(); }
29
+ catch { throw new Error('Invalid CORS origin'); }
30
+ }
31
+ const server = createServer({ requestTimeout: 10000, headersTimeout: 10000, connectionsCheckingInterval: 1000 }, (req, res) => {
32
+ // Without a token, accept only loopback Host values. This blocks DNS rebinding.
33
+ const hostOk = token || [`127.0.0.1:${boundPort}`, `localhost:${boundPort}`, `[::1]:${boundPort}`].includes(String(req.headers.host).toLowerCase());
34
+ const timer = setTimeout(() => { send(408, { error: 'Request timeout' }); req.resume(); }, 10000);
35
+ res.on('close', () => clearTimeout(timer));
36
+ function send(status, value, headers = {}) {
37
+ if (res.writableEnded || res.destroyed) return;
38
+ let body = JSON.stringify(value);
39
+ if (token && (body.includes(token) || body.includes(JSON.stringify(token).slice(1, -1)))) {
40
+ status = 400; body = JSON.stringify({ error: 'Invalid request' });
41
+ }
42
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', ...(status >= 400 && status !== 413 ? { connection: 'close' } : {}), ...headers });
43
+ res.end(body);
44
+ }
45
+ if (cors && req.headers.origin === cors) {
46
+ res.setHeader('access-control-allow-origin', cors); res.setHeader('vary', 'Origin');
47
+ }
48
+ if (token && !timingSafeEqual(digest(req.headers.authorization ?? ''), expected)) { send(401, { error: 'Unauthorized' }); req.resume(); return; }
49
+ let url;
50
+ if (!hostOk) { send(403, { error: 'Forbidden host' }); req.resume(); return; }
51
+ try { url = new URL(req.url, 'http://localhost'); } catch { send(400, { error: 'Invalid URL' }); return; }
52
+ const routes = { '/health': null, '/v1/formats': 'list_formats', '/v1/hooks': 'list-hooks',
53
+ '/v1/templates': 'list-templates', '/v1/qa-rules': 'list_qa_rules', '/v1/lint': 'lint', '/v1/plan': 'plan_format' };
54
+ const detail = url.pathname.match(/^\/v1\/(formats|qa-rules)\/([^/]+)$/);
55
+ const action = detail ? (detail[1] === 'formats' ? 'get_format' : 'get_qa_rule') : routes[url.pathname];
56
+ if (!detail && !Object.hasOwn(routes, url.pathname)) { send(404, { error: 'Not found' }); req.resume(); return; }
57
+ const method = ['lint', 'plan_format'].includes(action) ? 'POST' : 'GET';
58
+ if (req.method !== method) { send(405, { error: 'Method not allowed' }, { allow: method }); req.resume(); return; }
59
+ if (url.searchParams.has('apply')) { send(400, { error: 'Apply is unavailable' }); req.resume(); return; }
60
+ function run(args) {
61
+ try { send(200, action === null ? { status: 'ok' } : execute(action, args)); }
62
+ catch { send(detail ? 404 : 400, { error: detail ? 'Not found' : 'Invalid arguments' }); }
63
+ }
64
+ if (method === 'GET') {
65
+ try { run(detail ? { id: decodeURIComponent(detail[2]) } : {}); }
66
+ catch { send(400, { error: 'Invalid URL' }); }
67
+ req.resume(); return;
68
+ }
69
+ if (req.headers['content-type']?.split(';')[0].trim().toLowerCase() !== 'application/json') {
70
+ send(415, { error: 'Use application/json' }); req.resume(); return;
71
+ }
72
+ let size = 0, chunks = [], rejected = false;
73
+ req.on('error', () => { rejected = true; chunks = []; });
74
+ req.on('data', chunk => {
75
+ if (rejected) return;
76
+ size += chunk.length;
77
+ if (size > 65536) { rejected = true; chunks = []; send(413, { error: 'Body too large' }); return; }
78
+ chunks.push(chunk);
79
+ });
80
+ req.on('end', () => {
81
+ if (rejected || res.writableEnded) return;
82
+ let args;
83
+ try {
84
+ args = JSON.parse(Buffer.concat(chunks).toString('utf8'));
85
+ if (hasApply(args)) { send(400, { error: 'Apply is unavailable' }); return; }
86
+ } catch { send(400, { error: 'Invalid JSON' }); return; }
87
+ run(args);
88
+ });
89
+ });
90
+ server.on('clientError', (error, socket) => {
91
+ if (!socket.writable) return;
92
+ const status = error.code === 'ERR_HTTP_REQUEST_TIMEOUT' ? '408 Request Timeout' : '400 Bad Request';
93
+ const body = JSON.stringify({ error: 'Invalid or incomplete request' });
94
+ socket.end(`HTTP/1.1 ${status}\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
95
+ });
96
+ await new Promise((resolve, reject) => {
97
+ server.once('error', () => reject(new Error('Unable to start HTTP server')));
98
+ server.listen(options, resolve);
99
+ });
100
+ boundPort = server.address().port;
101
+ console.log(JSON.stringify({ status: 'listening', port: server.address().port }));
102
+ const stop = () => { server.close(); server.closeAllConnections(); };
103
+ process.once('SIGTERM', stop); process.once('SIGINT', stop);
104
+ }