@event4u/agent-config 2.21.0 → 2.24.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.
Files changed (67) hide show
  1. package/.agent-src/commands/video/from-script.md +123 -0
  2. package/.agent-src/commands/video/scene.md +92 -0
  3. package/.agent-src/commands/video/stitch.md +83 -0
  4. package/.agent-src/commands/video/storyboard.md +95 -0
  5. package/.agent-src/commands/video.md +59 -0
  6. package/.agent-src/personas/README.md +3 -0
  7. package/.agent-src/personas/ai-video-technical-director.md +81 -0
  8. package/.agent-src/personas/hollywood-director.md +99 -0
  9. package/.agent-src/personas/pixar-storyboard-artist.md +98 -0
  10. package/.agent-src/skills/adversarial-review/SKILL.md +2 -1
  11. package/.agent-src/skills/canvas-design/SKILL.md +11 -6
  12. package/.agent-src/skills/character-consistency/SKILL.md +120 -0
  13. package/.agent-src/skills/fe-design/SKILL.md +8 -0
  14. package/.agent-src/skills/motion-choreographer/SKILL.md +149 -0
  15. package/.agent-src/skills/pixar-storyteller/SKILL.md +107 -0
  16. package/.agent-src/skills/prompt-optimizer/SKILL.md +29 -5
  17. package/.agent-src/skills/react-shadcn-ui/SKILL.md +9 -0
  18. package/.agent-src/skills/refine-prompt/SKILL.md +57 -0
  19. package/.agent-src/skills/scene-expander/SKILL.md +122 -0
  20. package/.agent-src/skills/scene-expander/scene-blueprint.schema.yaml +108 -0
  21. package/.agent-src/skills/subagent-orchestration/SKILL.md +17 -15
  22. package/.agent-src/skills/tailwind-engineer/SKILL.md +14 -0
  23. package/.agent-src/skills/video-director/SKILL.md +113 -0
  24. package/.agent-src/templates/agent-settings.md +19 -0
  25. package/.agent-src/templates/agents/agent-project-settings.example.yml +53 -1
  26. package/.claude-plugin/marketplace.json +11 -1
  27. package/CHANGELOG.md +88 -138
  28. package/README.md +4 -4
  29. package/config/agent-settings.template.yml +28 -0
  30. package/docs/adrs/caveman/0001-default-off-until-bench.md +2 -2
  31. package/docs/adrs/cost/0001-hard-stop-hook.md +1 -1
  32. package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +2 -2
  33. package/docs/architecture.md +3 -3
  34. package/docs/archive/CHANGELOG-pre-2.20.0.md +159 -0
  35. package/docs/catalog.md +16 -5
  36. package/docs/contracts/command-clusters.md +1 -0
  37. package/docs/contracts/compression-default-kill-criterion.md +1 -1
  38. package/docs/contracts/file-ownership-matrix.json +344 -0
  39. package/docs/getting-started.md +1 -1
  40. package/docs/guidelines/prompt-templates.md +166 -0
  41. package/docs/parity/ruflo.md +3 -3
  42. package/package.json +1 -1
  43. package/scripts/ai-video/adapters/gemini-veo.sh +57 -0
  44. package/scripts/ai-video/adapters/higgsfield.sh +82 -0
  45. package/scripts/ai-video/adapters/kling.sh +54 -0
  46. package/scripts/ai-video/adapters/openai-images.sh +52 -0
  47. package/scripts/ai-video/adapters/sora.sh +54 -0
  48. package/scripts/ai-video/lib/adapter-common.sh +116 -0
  49. package/scripts/ai-video/lib/adapter-contract.md +163 -0
  50. package/scripts/ai-video/lib/fixtures/gemini-veo/result.json +1 -0
  51. package/scripts/ai-video/lib/fixtures/gemini-veo/scene-0001.mp4 +1 -0
  52. package/scripts/ai-video/lib/fixtures/higgsfield/result.json +1 -0
  53. package/scripts/ai-video/lib/fixtures/higgsfield/scene-0001.mp4 +1 -0
  54. package/scripts/ai-video/lib/fixtures/kling/result.json +1 -0
  55. package/scripts/ai-video/lib/fixtures/kling/scene-0001.mp4 +1 -0
  56. package/scripts/ai-video/lib/fixtures/openai-images/result.json +1 -0
  57. package/scripts/ai-video/lib/fixtures/openai-images/scene-0001.png +3 -0
  58. package/scripts/ai-video/lib/fixtures/sora/result.json +1 -0
  59. package/scripts/ai-video/lib/fixtures/sora/scene-0001.mp4 +1 -0
  60. package/scripts/ai-video/lib/load-config.sh +140 -0
  61. package/scripts/ai-video/lib/operator-pick.sh +119 -0
  62. package/scripts/ai-video/lib/parse-blueprint.sh +122 -0
  63. package/scripts/ai-video/lib/redact.sh +85 -0
  64. package/scripts/ai-video/lib/validate-deps.sh +132 -0
  65. package/scripts/ai-video/stitch.sh +154 -0
  66. package/scripts/ai-video/test-pipeline.sh +169 -0
  67. package/scripts/schemas/command.schema.json +8 -0
@@ -0,0 +1,163 @@
1
+ # Adapter Contract (v1)
2
+
3
+ > Single source of truth for every adapter under
4
+ > `scripts/ai-video/adapters/`. Phase 4 Step 1 finalizes any open
5
+ > point in this draft; **breaking changes bump the version pin
6
+ > and require every adapter to be rerun against the fixture set**
7
+ > (`scripts/ai-video/lib/fixtures/`).
8
+
9
+ ## Scope
10
+
11
+ Every backend (OpenAI Images, Gemini Veo, Kling, Higgsfield, Sora,
12
+ …) ships as one executable shell entry under
13
+ `scripts/ai-video/adapters/<id>.sh`. The orchestrator never speaks
14
+ to a network API directly — it speaks to one of these scripts.
15
+
16
+ ## Capability flag
17
+
18
+ Each adapter declares its audio capability via a top-of-file
19
+ comment and via the `capability` subcommand:
20
+
21
+ ```
22
+ # capability: audio=native | audio=none
23
+ ```
24
+
25
+ `audio=native` — the provider returns a muxed MP4 with synchronized
26
+ dialogue / ambient. `audio=none` — video-only output; the
27
+ orchestrator muxes the operator-supplied track at stitch time.
28
+
29
+ A per-model adapter (e.g. Higgsfield) MAY declare `audio=per-model`
30
+ and surface the capability via `capability --model <id>`.
31
+
32
+ ## Subcommands
33
+
34
+ Every adapter implements four:
35
+
36
+ | Subcommand | Purpose | I/O |
37
+ |---|---|---|
38
+ | `submit` | Submit a render job to the backend | stdin JSON · stdout `{job_id}` |
39
+ | `poll <job_id>` | Poll job status | stdout `{status: queued\|running\|done\|failed, progress?}` |
40
+ | `fetch <job_id>` | Download artifacts after `status=done` | stdout `{video_path, audio_path?, audio_embedded}` |
41
+ | `dry-run` | Return a deterministic fixture path without network | stdout same shape as `fetch` |
42
+
43
+ `submit` + `poll` + `fetch` may be collapsed into a single
44
+ `run` for synchronous backends (OpenAI Images) — the wrapper exposes
45
+ the same stdout shape on stdout and an exit code on completion.
46
+
47
+ ## Stdin JSON (consumed by `submit` / `run`)
48
+
49
+ ```json
50
+ {
51
+ "prompt": {
52
+ "style": "...",
53
+ "subject": "...",
54
+ "environment": "...",
55
+ "action": "...",
56
+ "camera": "...",
57
+ "lens": "...",
58
+ "lighting": "...",
59
+ "mood": "..."
60
+ },
61
+ "ref_images": ["/abs/path/frame.png", "..."],
62
+ "duration": 4.5,
63
+ "aspect": "16:9",
64
+ "seed": 1234567,
65
+ "audio": {
66
+ "dialogue": ["speaker: \"line\"", "..."],
67
+ "ambient": ["rain on metal", "..."],
68
+ "language": "en",
69
+ "enable_native_audio": true
70
+ },
71
+ "negative": ["centered framing", "..."]
72
+ }
73
+ ```
74
+
75
+ `ref_images`, `duration`, `aspect`, `seed`, `audio`, `negative` are
76
+ optional. `prompt.*` blocks are mandatory. Unknown top-level keys
77
+ are logged to stderr and ignored.
78
+
79
+ ## Stdout JSON (emitted by `fetch` / `run` / `dry-run`)
80
+
81
+ ```json
82
+ {
83
+ "video_path": "/abs/path/scene-0001.mp4",
84
+ "audio_path": "/abs/path/scene-0001.wav",
85
+ "audio_embedded": true
86
+ }
87
+ ```
88
+
89
+ Semantics:
90
+
91
+ - `audio_embedded: true` — `video_path` is a muxed MP4 with audio;
92
+ `audio_path` is omitted or echoes the same path. Stitcher
93
+ pass-through.
94
+ - `audio_embedded: false` with `audio_path` — separate track to mux
95
+ at stitch time via `ffmpeg`.
96
+ - `audio_embedded: false` without `audio_path` — video-only;
97
+ operator supplies the audio bed at stitch time.
98
+
99
+ ## Error contract
100
+
101
+ Adapter failure: **non-zero exit code** AND a JSON file at
102
+ `<project>/scenes/<scene_id>/error.json`:
103
+
104
+ ```json
105
+ {
106
+ "adapter": "gemini-veo",
107
+ "subcommand": "fetch",
108
+ "job_id": "...",
109
+ "exit_code": 7,
110
+ "stderr_tail": "...",
111
+ "retryable": true,
112
+ "user_action": "regenerate-prompt | retry | skip | abort"
113
+ }
114
+ ```
115
+
116
+ `retryable: false` means a prompt or input change is required —
117
+ the orchestrator MUST NOT auto-retry. `retryable: true` does **not**
118
+ authorize auto-retry either; the orchestrator surfaces a single
119
+ numbered-options block (retry · regenerate prompt · skip · abort)
120
+ per `non-destructive-by-default` and waits.
121
+
122
+ ## Dry-run
123
+
124
+ `AIV_DRYRUN=true` (set by command default; see Phase 5 Step 6) OR
125
+ the explicit `dry-run` subcommand:
126
+
127
+ - No network call.
128
+ - Stdout JSON points at a deterministic fixture under
129
+ `scripts/ai-video/lib/fixtures/<adapter-id>/`.
130
+ - Exit code 0.
131
+
132
+ Fixtures are committed and cover one happy path per adapter.
133
+ Phase 6 golden runs assert byte-identical stdout under
134
+ `AIV_DRYRUN=true`.
135
+
136
+ ## Logging & redaction
137
+
138
+ - **stderr only** for log output. Stdout is reserved for the
139
+ contract JSON.
140
+ - Every adapter MUST source `scripts/ai-video/lib/redact.sh` and
141
+ pipe network responses through `aiv_redact_stream` before any
142
+ `>&2` write. API keys, bearer tokens, and operator-registered
143
+ secrets are masked.
144
+
145
+ ## Strict-mode shell
146
+
147
+ Every adapter:
148
+
149
+ ```bash
150
+ #!/usr/bin/env bash
151
+ set -euo pipefail
152
+ . "$(dirname "$0")/../lib/redact.sh"
153
+ . "$(dirname "$0")/../lib/load-config.sh"
154
+ ```
155
+
156
+ `shellcheck` clean (Phase 4 Step 9).
157
+
158
+ ## Versioning
159
+
160
+ This contract is `v1`. Backward-incompatible changes (renamed
161
+ field, removed subcommand, changed stdout shape) bump to `v2` and
162
+ require an `adapter-contract` migration note plus a rerun of every
163
+ adapter against the fixture set.
@@ -0,0 +1 @@
1
+ {"video_path":"scripts/ai-video/lib/fixtures/gemini-veo/scene-0001.mp4","audio_embedded":true}
@@ -0,0 +1 @@
1
+ FIXTURE-gemini-veo-mp4
@@ -0,0 +1 @@
1
+ {"video_path":"scripts/ai-video/lib/fixtures/higgsfield/scene-0001.mp4","audio_embedded":false}
@@ -0,0 +1 @@
1
+ FIXTURE-higgsfield-mp4
@@ -0,0 +1 @@
1
+ {"video_path":"scripts/ai-video/lib/fixtures/kling/scene-0001.mp4","audio_embedded":false}
@@ -0,0 +1 @@
1
+ FIXTURE-kling-mp4
@@ -0,0 +1 @@
1
+ {"video_path":"scripts/ai-video/lib/fixtures/openai-images/scene-0001.png","audio_embedded":false}
@@ -0,0 +1,3 @@
1
+ �PNG
2
+ 
3
+ FIXTURE-openai-images
@@ -0,0 +1 @@
1
+ {"video_path":"scripts/ai-video/lib/fixtures/sora/scene-0001.mp4","audio_embedded":true}
@@ -0,0 +1 @@
1
+ FIXTURE-sora-mp4
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/ai-video/lib/load-config.sh — provider config loader for /video:*.
3
+ #
4
+ # Parses agents/.ai-video.xml with xmllint --xpath and surfaces a single
5
+ # provider's settings as AIV_KEY / AIV_ENDPOINT / AIV_MODEL / AIV_DRYRUN
6
+ # env vars to its caller. The key is NEVER echoed — status output is only
7
+ # `present` or `missing`.
8
+ #
9
+ # Usage (sourced):
10
+ #
11
+ # . scripts/ai-video/lib/load-config.sh
12
+ # aiv_load_provider gemini-veo
13
+ # echo "key: $(aiv_key_status)" # → present | missing
14
+ # echo "endpoint: $AIV_ENDPOINT"
15
+ # echo "dryrun: $AIV_DRYRUN"
16
+ #
17
+ # Usage (CLI status check, no key echoed):
18
+ #
19
+ # bash scripts/ai-video/lib/load-config.sh status gemini-veo
20
+ # → provider=gemini-veo key=present dryrun=true model=veo-3.0-generate-001
21
+ #
22
+ # Defaults:
23
+ # - AIV_DRYRUN defaults to `true` when not set in XML
24
+ # - AIV_DRYRUN can be overridden by the AIV_DRYRUN env var set by caller
25
+ # - missing file is non-fatal in `status` mode (prints all-missing line)
26
+
27
+ set -u
28
+
29
+ AIV_CONFIG_PATH="${AIV_CONFIG_PATH:-agents/.ai-video.xml}"
30
+
31
+ _aiv_xpath() {
32
+ # $1 = xpath expression; reads from $AIV_CONFIG_PATH
33
+ # Returns text content or empty string. Never errors on missing path.
34
+ if [ ! -f "${AIV_CONFIG_PATH}" ]; then
35
+ printf ''
36
+ return 0
37
+ fi
38
+ if ! command -v xmllint >/dev/null 2>&1; then
39
+ printf ''
40
+ return 0
41
+ fi
42
+ xmllint --xpath "string(${1})" "${AIV_CONFIG_PATH}" 2>/dev/null || printf ''
43
+ }
44
+
45
+ aiv_default_image_provider() {
46
+ _aiv_xpath '/ai-video/default-image-provider'
47
+ }
48
+
49
+ aiv_default_video_provider() {
50
+ _aiv_xpath '/ai-video/default-video-provider'
51
+ }
52
+
53
+ aiv_load_provider() {
54
+ local pid="${1:-}"
55
+ if [ -z "${pid}" ]; then
56
+ echo "aiv_load_provider: provider id required" >&2
57
+ return 2
58
+ fi
59
+
60
+ # Resolve from the top-level <provider> blocks OR the <extra> slot.
61
+ local base="(/ai-video/provider[@id='${pid}']|/ai-video/extra/provider[@id='${pid}'])"
62
+
63
+ AIV_PROVIDER_ID="${pid}"
64
+ AIV_KEY="$(_aiv_xpath "${base}/api-key")"
65
+ AIV_ENDPOINT="$(_aiv_xpath "${base}/endpoint")"
66
+ AIV_MODEL="$(_aiv_xpath "${base}/default-model")"
67
+ AIV_KIND="$(_aiv_xpath "${base}/@kind")"
68
+
69
+ local dryrun_xml
70
+ dryrun_xml="$(_aiv_xpath "${base}/dry-run")"
71
+ # Caller-set AIV_DRYRUN takes precedence; otherwise XML; otherwise true.
72
+ if [ -n "${AIV_DRYRUN:-}" ]; then
73
+ : # respect caller
74
+ elif [ -n "${dryrun_xml}" ]; then
75
+ AIV_DRYRUN="${dryrun_xml}"
76
+ else
77
+ AIV_DRYRUN="true"
78
+ fi
79
+ export AIV_DRYRUN
80
+
81
+ # Tuning fields — adapters read these directly when present.
82
+ AIV_TUNING_ASPECT="$(_aiv_xpath "${base}/tuning/aspect")"
83
+ AIV_TUNING_FPS="$(_aiv_xpath "${base}/tuning/fps")"
84
+ AIV_TUNING_MAX_DURATION="$(_aiv_xpath "${base}/tuning/max-duration")"
85
+ AIV_TUNING_AUDIO_NATIVE="$(_aiv_xpath "${base}/tuning/audio-native")"
86
+ AIV_TUNING_PRESET="$(_aiv_xpath "${base}/tuning/preset")"
87
+ AIV_TUNING_QUALITY="$(_aiv_xpath "${base}/tuning/quality")"
88
+ AIV_TUNING_BEST_OF_N="$(_aiv_xpath "${base}/tuning/best-of-n")"
89
+ export AIV_TUNING_ASPECT AIV_TUNING_FPS AIV_TUNING_MAX_DURATION \
90
+ AIV_TUNING_AUDIO_NATIVE AIV_TUNING_PRESET AIV_TUNING_QUALITY \
91
+ AIV_TUNING_BEST_OF_N
92
+
93
+ # Register key with redact.sh if loaded — adapters always source both.
94
+ if command -v aiv_redact_register >/dev/null 2>&1; then
95
+ aiv_redact_register "${AIV_KEY}"
96
+ fi
97
+
98
+ return 0
99
+ }
100
+
101
+ aiv_key_status() {
102
+ case "${AIV_KEY:-}" in
103
+ ""|"REPLACE-ME"|*"-REPLACE-ME") printf 'missing' ;;
104
+ *) printf 'present' ;;
105
+ esac
106
+ }
107
+
108
+ # CLI mode — only `status <provider-id>` is supported; never echoes the key.
109
+ if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then
110
+ cmd="${1:-}"
111
+ case "${cmd}" in
112
+ status)
113
+ pid="${2:-}"
114
+ if [ -z "${pid}" ]; then
115
+ echo "usage: load-config.sh status <provider-id>" >&2
116
+ exit 2
117
+ fi
118
+ aiv_load_provider "${pid}" >/dev/null
119
+ printf 'provider=%s key=%s dryrun=%s model=%s endpoint=%s kind=%s\n' \
120
+ "${AIV_PROVIDER_ID}" \
121
+ "$(aiv_key_status)" \
122
+ "${AIV_DRYRUN:-true}" \
123
+ "${AIV_MODEL:-}" \
124
+ "${AIV_ENDPOINT:-}" \
125
+ "${AIV_KIND:-}"
126
+ ;;
127
+ defaults)
128
+ printf 'default-image-provider=%s\n' "$(aiv_default_image_provider)"
129
+ printf 'default-video-provider=%s\n' "$(aiv_default_video_provider)"
130
+ ;;
131
+ "")
132
+ echo "usage: load-config.sh {status <id> | defaults}" >&2
133
+ exit 2
134
+ ;;
135
+ *)
136
+ echo "load-config.sh: unknown subcommand '${cmd}'" >&2
137
+ exit 2
138
+ ;;
139
+ esac
140
+ fi
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # operator-pick.sh — best-of-N selection checkpoint invoked after image
3
+ # render by /video:from-script and /video:scene. Renders a thumbnail
4
+ # contact-sheet PNG of the candidates under a scene, then waits for the
5
+ # operator to write <project>/scenes/<id>/selection.json.
6
+ #
7
+ # Dry-run mode (AIV_DRYRUN=true, default) auto-picks candidate 1 and
8
+ # writes the same selection.json so smoke tests stay unattended.
9
+ #
10
+ # Usage:
11
+ # operator-pick.sh <project-dir> <scene-id>
12
+ #
13
+ # Inputs:
14
+ # <project-dir>/scenes/<scene-id>/candidates/*.png (N>=1 image files)
15
+ #
16
+ # Outputs (stdout, one path per line so callers can capture both):
17
+ # sheet=<project-dir>/scenes/<scene-id>/contact-sheet.png
18
+ # selected=<absolute path to locked image>
19
+ #
20
+ # Exit codes:
21
+ # 0 selection.json present, locked image path emitted
22
+ # 2 missing candidates directory or empty
23
+ # 3 selection.json malformed or names an unknown candidate
24
+ # 4 operator declined (selection.json absent in live mode)
25
+
26
+ set -euo pipefail
27
+
28
+ if [ "$#" -ne 2 ]; then
29
+ printf 'operator-pick: usage: %s <project-dir> <scene-id>\n' "$0" >&2
30
+ exit 2
31
+ fi
32
+
33
+ project_dir="$1"
34
+ scene_id="$2"
35
+ scene_dir="${project_dir}/scenes/${scene_id}"
36
+ cand_dir="${scene_dir}/candidates"
37
+ sheet="${scene_dir}/contact-sheet.png"
38
+ sel_file="${scene_dir}/selection.json"
39
+
40
+ if [ ! -d "${cand_dir}" ]; then
41
+ printf 'operator-pick: missing candidate dir: %s\n' "${cand_dir}" >&2
42
+ exit 2
43
+ fi
44
+
45
+ # Collect candidates in deterministic order (lexical). Portable to
46
+ # macOS bash 3.2 — no `mapfile`.
47
+ candidates=()
48
+ while IFS= read -r _p; do
49
+ candidates+=("${_p}")
50
+ done < <(find "${cand_dir}" -maxdepth 1 -type f -name '*.png' | LC_ALL=C sort)
51
+ if [ "${#candidates[@]}" -eq 0 ]; then
52
+ printf 'operator-pick: no candidate PNGs under %s\n' "${cand_dir}" >&2
53
+ exit 2
54
+ fi
55
+
56
+ # Build the contact-sheet PNG via ffmpeg. ceil(sqrt(N)) columns.
57
+ n="${#candidates[@]}"
58
+ cols=1
59
+ while [ $((cols * cols)) -lt "${n}" ]; do cols=$((cols + 1)); done
60
+
61
+ # ffmpeg's `tile` filter needs an input concat; use `-pattern_type glob`.
62
+ if command -v ffmpeg >/dev/null 2>&1; then
63
+ ffmpeg -y -loglevel error \
64
+ -pattern_type glob -i "${cand_dir}/*.png" \
65
+ -filter_complex "tile=${cols}x0:padding=8:margin=16" \
66
+ "${sheet}" || {
67
+ printf 'operator-pick: ffmpeg tile failed; falling back to first candidate as sheet\n' >&2
68
+ cp "${candidates[0]}" "${sheet}"
69
+ }
70
+ else
71
+ # ffmpeg absent (e.g. minimal CI). Copy the first candidate as the sheet.
72
+ cp "${candidates[0]}" "${sheet}"
73
+ fi
74
+
75
+ # Dry-run: auto-select candidate 1 and write selection.json verbatim so
76
+ # downstream resume reads it identically to a real operator pick.
77
+ if [ "${AIV_DRYRUN:-true}" = "true" ]; then
78
+ first="$(basename "${candidates[0]}")"
79
+ cat > "${sel_file}" <<JSON
80
+ {
81
+ "selected": "${first}",
82
+ "reason": "auto-selected by operator-pick.sh (AIV_DRYRUN=true)"
83
+ }
84
+ JSON
85
+ fi
86
+
87
+ # Wait-loop is not appropriate here — the caller (command file) decides
88
+ # whether to poll or hand back. We assert selection.json exists and
89
+ # resolve it; if missing in live mode, we exit non-zero so the caller
90
+ # can pause and re-invoke.
91
+ if [ ! -f "${sel_file}" ]; then
92
+ printf 'operator-pick: selection.json absent; write %s with {"selected":"<filename>"} and re-run\n' "${sel_file}" >&2
93
+ exit 4
94
+ fi
95
+
96
+ # Read selection.json (jq required — already a hard dep for adapters).
97
+ if ! command -v jq >/dev/null 2>&1; then
98
+ printf 'operator-pick: jq is required\n' >&2
99
+ exit 3
100
+ fi
101
+ selected_name="$(jq -r '.selected // empty' "${sel_file}")"
102
+ if [ -z "${selected_name}" ]; then
103
+ printf 'operator-pick: selection.json missing "selected" field\n' >&2
104
+ exit 3
105
+ fi
106
+
107
+ selected_path="${cand_dir}/${selected_name}"
108
+ if [ ! -f "${selected_path}" ]; then
109
+ printf 'operator-pick: selected candidate not found: %s\n' "${selected_path}" >&2
110
+ exit 3
111
+ fi
112
+
113
+ # Also write a stable locked.png symlink/copy at the scene root so the
114
+ # motion step can resolve the locked image without re-reading selection.json.
115
+ locked="${scene_dir}/locked.png"
116
+ cp -f "${selected_path}" "${locked}"
117
+
118
+ printf 'sheet=%s\n' "${sheet}"
119
+ printf 'selected=%s\n' "${locked}"
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ # parse-blueprint.sh — read a 12-block Cinematic Scene Blueprint
3
+ # from stdin (or a file arg) and emit adapter-contract JSON on
4
+ # stdout. Pure POSIX-compatible bash (no associative arrays, runs
5
+ # on macOS bash 3.2); jq required for JSON safety.
6
+ #
7
+ # Schema: .agent-src.uncompressed/skills/scene-expander/scene-blueprint.schema.yaml
8
+ # Contract: scripts/ai-video/lib/adapter-contract.md
9
+ #
10
+ # Usage:
11
+ # parse-blueprint.sh < prompt.txt > blueprint.json
12
+ # parse-blueprint.sh prompt.txt > blueprint.json
13
+ #
14
+ # Exit codes:
15
+ # 0 valid blueprint, JSON emitted on stdout
16
+ # 2 missing required block — name on stderr
17
+ # 3 parse error (unknown block, malformed DURATION, etc.)
18
+
19
+ set -euo pipefail
20
+
21
+ command -v jq >/dev/null 2>&1 || {
22
+ echo "parse-blueprint: jq required" >&2
23
+ exit 3
24
+ }
25
+
26
+ input="${1:-/dev/stdin}"
27
+ [ -r "$input" ] || { echo "parse-blueprint: cannot read $input" >&2; exit 3; }
28
+
29
+ V_STYLE=""; V_SUBJECT=""; V_ENVIRONMENT=""; V_ACTION=""
30
+ V_CAMERA=""; V_LENS=""; V_LIGHTING=""; V_MOOD=""
31
+ V_DIALOGUE=""; V_AMBIENT_SOUND=""; V_DURATION=""; V_NEGATIVE=""
32
+
33
+ append_to() {
34
+ local name="$1"; local line="$2"
35
+ local cur; eval "cur=\${$name}"
36
+ if [ -z "$cur" ]; then
37
+ eval "$name=\$line"
38
+ else
39
+ eval "$name=\"\${$name}
40
+ \$line\""
41
+ fi
42
+ }
43
+
44
+ set_var() {
45
+ eval "$1=\$2"
46
+ }
47
+
48
+ current=""
49
+ while IFS= read -r line || [ -n "$line" ]; do
50
+ stripped="$(printf '%s' "$line" | sed -E 's/^[[:space:]]*//; s/^\*\*//; s/\*\*[[:space:]]*$//')"
51
+ label="$(printf '%s' "$stripped" | sed -nE 's/^([A-Z][A-Z ]+)[[:space:]]*:.*$/\1/p')"
52
+ if [ -n "$label" ]; then
53
+ key="$(printf '%s' "$label" | tr ' ' '_' | tr '[:lower:]' '[:upper:]')"
54
+ case "$key" in
55
+ STYLE|SUBJECT|ENVIRONMENT|ACTION|CAMERA|LENS|LIGHTING|MOOD|DIALOGUE|AMBIENT_SOUND|DURATION|NEGATIVE)
56
+ current="V_$key"
57
+ rest="$(printf '%s' "$stripped" | sed -E "s/^${label}[[:space:]]*:[[:space:]]*//")"
58
+ set_var "$current" "$rest"
59
+ continue
60
+ ;;
61
+ esac
62
+ fi
63
+ if [ -n "$current" ] && [ -n "$line" ]; then
64
+ append_to "$current" "$line"
65
+ fi
66
+ done < "$input"
67
+
68
+ for short in STYLE SUBJECT ENVIRONMENT ACTION CAMERA LENS LIGHTING MOOD DURATION NEGATIVE; do
69
+ eval "v=\$V_$short"
70
+ if [ -z "$v" ]; then
71
+ echo "parse-blueprint: missing required block: $short" >&2
72
+ exit 2
73
+ fi
74
+ done
75
+
76
+ if ! printf '%s' "$V_DURATION" | grep -Eq '^[0-9]+(\.[0-9])?$'; then
77
+ echo "parse-blueprint: DURATION not numeric: $V_DURATION" >&2
78
+ exit 3
79
+ fi
80
+
81
+ negative_json="$(printf '%s' "$V_NEGATIVE" | tr ',\n' '\n\n' | sed '/^[[:space:]]*$/d' | sed -E 's/^[[:space:]]*//; s/[[:space:]]*$//' | jq -R . | jq -s .)"
82
+
83
+ to_array_or_null() {
84
+ if [ -z "$1" ]; then
85
+ printf '%s' "null"
86
+ else
87
+ printf '%s\n' "$1" | sed '/^[[:space:]]*$/d' | jq -R . | jq -s .
88
+ fi
89
+ }
90
+ dialogue_json="$(to_array_or_null "$V_DIALOGUE")"
91
+ ambient_json="$(to_array_or_null "$V_AMBIENT_SOUND")"
92
+
93
+ audio_native=false
94
+ if [ "$dialogue_json" != "null" ] || [ "$ambient_json" != "null" ]; then
95
+ audio_native=true
96
+ fi
97
+
98
+ jq -n \
99
+ --arg style "$V_STYLE" \
100
+ --arg subject "$V_SUBJECT" \
101
+ --arg environment "$V_ENVIRONMENT" \
102
+ --arg action "$V_ACTION" \
103
+ --arg camera "$V_CAMERA" \
104
+ --arg lens "$V_LENS" \
105
+ --arg lighting "$V_LIGHTING" \
106
+ --arg mood "$V_MOOD" \
107
+ --argjson dialogue "$dialogue_json" \
108
+ --argjson ambient "$ambient_json" \
109
+ --argjson duration "$V_DURATION" \
110
+ --argjson negative "$negative_json" \
111
+ --argjson native "$audio_native" \
112
+ '{
113
+ prompt: {
114
+ style: $style, subject: $subject, environment: $environment,
115
+ action: $action, camera: $camera, lens: $lens,
116
+ lighting: $lighting, mood: $mood
117
+ },
118
+ audio: { dialogue: $dialogue, ambient: $ambient, enable_native_audio: $native },
119
+ duration: $duration,
120
+ negative: $negative,
121
+ requires: { audio_native: $native }
122
+ }'
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/ai-video/lib/redact.sh — secret-scrubbing helper for /video:* adapters.
3
+ #
4
+ # Sourced by every adapter under scripts/ai-video/adapters/*.sh. Provides
5
+ # two helpers:
6
+ #
7
+ # aiv_redact_register <secret> — register a string to scrub (idempotent)
8
+ # aiv_redact <text> — print text with every registered
9
+ # secret replaced by ***REDACTED***
10
+ #
11
+ # Iron Law: an API key must never reach stdout/stderr verbatim. Adapters
12
+ # pipe every network response, curl error, and trace through aiv_redact
13
+ # before printing. Empty / unset values are skipped silently — an unset
14
+ # secret cannot leak.
15
+ #
16
+ # Pure bash, no external dependencies. Safe under `set -euo pipefail`.
17
+
18
+ # Guard against double-source.
19
+ if [ -n "${AIV_REDACT_LOADED:-}" ]; then
20
+ return 0 2>/dev/null || exit 0
21
+ fi
22
+ AIV_REDACT_LOADED=1
23
+
24
+ # Registry of secrets to scrub. Newline-separated; populated by
25
+ # aiv_redact_register. Never echoed.
26
+ AIV_REDACT_SECRETS=""
27
+
28
+ aiv_redact_register() {
29
+ local secret="${1:-}"
30
+ # Treat empty / placeholder values as no-op so adapters can call this
31
+ # unconditionally without leaking the placeholder string.
32
+ case "${secret}" in
33
+ ""|"REPLACE-ME"|*"-REPLACE-ME") return 0 ;;
34
+ esac
35
+ # Require a minimum length so single characters do not nuke the log.
36
+ if [ "${#secret}" -lt 8 ]; then
37
+ return 0
38
+ fi
39
+ # Idempotent — skip if already registered.
40
+ case "${AIV_REDACT_SECRETS}" in
41
+ *"${secret}"*) return 0 ;;
42
+ esac
43
+ AIV_REDACT_SECRETS="${AIV_REDACT_SECRETS}${secret}
44
+ "
45
+ }
46
+
47
+ aiv_redact() {
48
+ local input
49
+ if [ "$#" -gt 0 ]; then
50
+ input="$*"
51
+ else
52
+ input="$(cat)"
53
+ fi
54
+ if [ -z "${AIV_REDACT_SECRETS}" ]; then
55
+ printf '%s\n' "${input}"
56
+ return 0
57
+ fi
58
+ # Apply replacements one secret at a time using awk so special chars
59
+ # in the secret cannot break a sed expression. Use a here-string to
60
+ # avoid running the loop in a subshell (which would discard mutations
61
+ # to ${input}).
62
+ local secret
63
+ while IFS= read -r secret; do
64
+ [ -z "${secret}" ] && continue
65
+ input="$(printf '%s' "${input}" | awk -v s="${secret}" '
66
+ {
67
+ out = ""
68
+ rest = $0
69
+ while ((i = index(rest, s)) > 0) {
70
+ out = out substr(rest, 1, i - 1) "***REDACTED***"
71
+ rest = substr(rest, i + length(s))
72
+ }
73
+ print out rest
74
+ }')"
75
+ done <<< "${AIV_REDACT_SECRETS}"
76
+ printf '%s\n' "${input}"
77
+ }
78
+
79
+ # Convenience wrapper: pipe stdin through aiv_redact line by line so
80
+ # adapters can do `curl … 2>&1 | aiv_redact_stream`.
81
+ aiv_redact_stream() {
82
+ while IFS= read -r line; do
83
+ aiv_redact "${line}"
84
+ done
85
+ }