@event4u/agent-config 2.23.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 (58) 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/character-consistency/SKILL.md +120 -0
  11. package/.agent-src/skills/motion-choreographer/SKILL.md +149 -0
  12. package/.agent-src/skills/pixar-storyteller/SKILL.md +107 -0
  13. package/.agent-src/skills/scene-expander/SKILL.md +122 -0
  14. package/.agent-src/skills/scene-expander/scene-blueprint.schema.yaml +108 -0
  15. package/.agent-src/skills/subagent-orchestration/SKILL.md +17 -15
  16. package/.agent-src/skills/video-director/SKILL.md +113 -0
  17. package/.agent-src/templates/agent-settings.md +19 -0
  18. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/.claude-plugin/marketplace.json +11 -1
  20. package/CHANGELOG.md +22 -0
  21. package/README.md +4 -4
  22. package/config/agent-settings.template.yml +28 -0
  23. package/docs/adrs/caveman/0001-default-off-until-bench.md +2 -2
  24. package/docs/adrs/cost/0001-hard-stop-hook.md +1 -1
  25. package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +2 -2
  26. package/docs/architecture.md +2 -2
  27. package/docs/catalog.md +14 -4
  28. package/docs/contracts/command-clusters.md +1 -0
  29. package/docs/contracts/compression-default-kill-criterion.md +1 -1
  30. package/docs/contracts/file-ownership-matrix.json +337 -0
  31. package/docs/getting-started.md +1 -1
  32. package/docs/parity/ruflo.md +3 -3
  33. package/package.json +1 -1
  34. package/scripts/ai-video/adapters/gemini-veo.sh +57 -0
  35. package/scripts/ai-video/adapters/higgsfield.sh +82 -0
  36. package/scripts/ai-video/adapters/kling.sh +54 -0
  37. package/scripts/ai-video/adapters/openai-images.sh +52 -0
  38. package/scripts/ai-video/adapters/sora.sh +54 -0
  39. package/scripts/ai-video/lib/adapter-common.sh +116 -0
  40. package/scripts/ai-video/lib/adapter-contract.md +163 -0
  41. package/scripts/ai-video/lib/fixtures/gemini-veo/result.json +1 -0
  42. package/scripts/ai-video/lib/fixtures/gemini-veo/scene-0001.mp4 +1 -0
  43. package/scripts/ai-video/lib/fixtures/higgsfield/result.json +1 -0
  44. package/scripts/ai-video/lib/fixtures/higgsfield/scene-0001.mp4 +1 -0
  45. package/scripts/ai-video/lib/fixtures/kling/result.json +1 -0
  46. package/scripts/ai-video/lib/fixtures/kling/scene-0001.mp4 +1 -0
  47. package/scripts/ai-video/lib/fixtures/openai-images/result.json +1 -0
  48. package/scripts/ai-video/lib/fixtures/openai-images/scene-0001.png +3 -0
  49. package/scripts/ai-video/lib/fixtures/sora/result.json +1 -0
  50. package/scripts/ai-video/lib/fixtures/sora/scene-0001.mp4 +1 -0
  51. package/scripts/ai-video/lib/load-config.sh +140 -0
  52. package/scripts/ai-video/lib/operator-pick.sh +119 -0
  53. package/scripts/ai-video/lib/parse-blueprint.sh +122 -0
  54. package/scripts/ai-video/lib/redact.sh +85 -0
  55. package/scripts/ai-video/lib/validate-deps.sh +132 -0
  56. package/scripts/ai-video/stitch.sh +154 -0
  57. package/scripts/ai-video/test-pipeline.sh +169 -0
  58. package/scripts/schemas/command.schema.json +8 -0
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # kling.sh — Kuaishou Kling motion-tuned video generation adapter.
3
+ #
4
+ # Capability: audio=none by default (Kling current generation does not
5
+ # emit native audio); the orchestrator muxes operator-supplied dialogue
6
+ # / ambient at stitch time. Max duration is model-dependent; clip-time
7
+ # clamping happens in motion-choreographer (it tunes the prompt to fit).
8
+ #
9
+ # Contract: scripts/ai-video/lib/adapter-contract.md
10
+ # Provider: top-level <provider id="kling" kind="video"> in
11
+ # agents/.ai-video.xml.
12
+
13
+ set -euo pipefail
14
+
15
+ # shellcheck source=../lib/adapter-common.sh
16
+ . "$(dirname "$0")/../lib/adapter-common.sh"
17
+
18
+ ADAPTER_ID="kling"
19
+ KLING_MAX_DURATION="${KLING_MAX_DURATION:-10}"
20
+
21
+ aiv_cmd_submit() {
22
+ aiv_assert_dryrun
23
+ aiv_require_cmd curl jq
24
+ aiv_load_provider "${ADAPTER_ID}"
25
+ [ "$(aiv_key_status)" = "present" ] \
26
+ || aiv_die 6 "${ADAPTER_ID}: api key missing in agents/.ai-video.xml"
27
+
28
+ local stdin_json duration
29
+ stdin_json="$(cat)"
30
+ duration="$(printf '%s' "${stdin_json}" | jq -r '.duration // empty')"
31
+ if [ -n "${duration}" ]; then
32
+ awk -v d="${duration}" -v m="${KLING_MAX_DURATION}" \
33
+ 'BEGIN { if (d+0 > m+0) exit 1; exit 0 }' \
34
+ || aiv_die 3 "${ADAPTER_ID}: duration ${duration}s exceeds model max ${KLING_MAX_DURATION}s"
35
+ fi
36
+
37
+ aiv_die 9 "${ADAPTER_ID}: live submit not yet wired (max duration ${KLING_MAX_DURATION}s honored)"
38
+ }
39
+
40
+ aiv_cmd_poll() {
41
+ local job_id="${1:-}"
42
+ [ -n "${job_id}" ] || aiv_die 2 "${ADAPTER_ID}: poll <job_id> required"
43
+ aiv_assert_dryrun
44
+ aiv_die 9 "${ADAPTER_ID}: live poll not yet wired (job=${job_id})"
45
+ }
46
+
47
+ aiv_cmd_fetch() {
48
+ local job_id="${1:-}"
49
+ [ -n "${job_id}" ] || aiv_die 2 "${ADAPTER_ID}: fetch <job_id> required"
50
+ aiv_assert_dryrun
51
+ aiv_die 9 "${ADAPTER_ID}: live fetch not yet wired (job=${job_id})"
52
+ }
53
+
54
+ aiv_dispatch "${ADAPTER_ID}" "none" "$@"
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # openai-images.sh — image generation adapter (DALL-E / gpt-image-1).
3
+ #
4
+ # Capability: audio=none (still images only — motion handled by a
5
+ # downstream video adapter). Ref-image and seed reuse are passed
6
+ # through verbatim so character-consistency can lock a face across
7
+ # scenes by reusing the same seed.
8
+ #
9
+ # Contract: scripts/ai-video/lib/adapter-contract.md
10
+ # Provider: top-level <provider id="openai-images" kind="image"> in
11
+ # agents/.ai-video.xml.
12
+
13
+ set -euo pipefail
14
+
15
+ # shellcheck source=../lib/adapter-common.sh
16
+ . "$(dirname "$0")/../lib/adapter-common.sh"
17
+
18
+ ADAPTER_ID="openai-images"
19
+
20
+ aiv_cmd_run() {
21
+ aiv_assert_dryrun
22
+ aiv_require_cmd curl jq
23
+ aiv_load_provider "${ADAPTER_ID}"
24
+ [ "$(aiv_key_status)" = "present" ] \
25
+ || aiv_die 6 "${ADAPTER_ID}: api key missing in agents/.ai-video.xml"
26
+
27
+ # Read contract stdin JSON; compose a single text prompt from the
28
+ # eight prose blocks. Ref-image + seed are passed verbatim.
29
+ local stdin_json prompt seed ref_first
30
+ stdin_json="$(cat)"
31
+ prompt="$(printf '%s' "${stdin_json}" | jq -r '
32
+ [.prompt.style, .prompt.subject, .prompt.environment, .prompt.action,
33
+ .prompt.camera, .prompt.lens, .prompt.lighting, .prompt.mood]
34
+ | map(select(. != null and . != ""))
35
+ | join(". ")
36
+ ')"
37
+ seed="$(printf '%s' "${stdin_json}" | jq -r '.seed // empty')"
38
+ ref_first="$(printf '%s' "${stdin_json}" | jq -r '.ref_images[0] // empty')"
39
+
40
+ # Live mode is implementation-scaffolded: we do NOT yet POST to the
41
+ # OpenAI Images API from inside this adapter. The cost-floor gate
42
+ # (Phase 5 Step 6) refuses live mode unless the operator explicitly
43
+ # confirms in-turn; this branch surfaces a clear stub error so the
44
+ # contract surface stays stable while the live path is wired later.
45
+ aiv_die 9 "${ADAPTER_ID}: live mode not yet wired (prompt=${#prompt} chars, seed=${seed:-unset}, ref=${ref_first:-none})"
46
+ }
47
+
48
+ aiv_cmd_submit() { aiv_cmd_run "$@"; }
49
+ aiv_cmd_fetch() { aiv_cmd_run "$@"; }
50
+ aiv_cmd_poll() { printf '{"status":"done"}\n'; }
51
+
52
+ aiv_dispatch "${ADAPTER_ID}" "none" "$@"
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # sora.sh — OpenAI Sora structural-prompt video adapter.
3
+ #
4
+ # Capability: audio=native. Sora-class models produce muxed MP4 with
5
+ # dialogue + ambient sound; we pass the audio block straight through.
6
+ # Structural-prompt path informed by upstream `awesome-sora-prompts`
7
+ # (attribution in agents/ai-video/prompts/cinematic-blueprint.md).
8
+ #
9
+ # Contract: scripts/ai-video/lib/adapter-contract.md
10
+ # Provider: top-level <provider id="sora" kind="video"> in
11
+ # agents/.ai-video.xml.
12
+
13
+ set -euo pipefail
14
+
15
+ # shellcheck source=../lib/adapter-common.sh
16
+ . "$(dirname "$0")/../lib/adapter-common.sh"
17
+
18
+ ADAPTER_ID="sora"
19
+
20
+ aiv_cmd_submit() {
21
+ aiv_assert_dryrun
22
+ aiv_require_cmd curl jq
23
+ aiv_load_provider "${ADAPTER_ID}"
24
+ [ "$(aiv_key_status)" = "present" ] \
25
+ || aiv_die 6 "${ADAPTER_ID}: api key missing in agents/.ai-video.xml"
26
+
27
+ local stdin_json
28
+ stdin_json="$(cat)"
29
+ # Sora's structural-prompt path expects all eight blocks present;
30
+ # fail loud if the blueprint parser dropped any.
31
+ printf '%s' "${stdin_json}" \
32
+ | jq -e '.prompt.style and .prompt.subject and .prompt.environment and
33
+ .prompt.action and .prompt.camera and .prompt.lens and
34
+ .prompt.lighting and .prompt.mood' >/dev/null \
35
+ || aiv_die 3 "${ADAPTER_ID}: stdin JSON missing one or more required prompt blocks"
36
+
37
+ aiv_die 9 "${ADAPTER_ID}: live submit not yet wired"
38
+ }
39
+
40
+ aiv_cmd_poll() {
41
+ local job_id="${1:-}"
42
+ [ -n "${job_id}" ] || aiv_die 2 "${ADAPTER_ID}: poll <job_id> required"
43
+ aiv_assert_dryrun
44
+ aiv_die 9 "${ADAPTER_ID}: live poll not yet wired (job=${job_id})"
45
+ }
46
+
47
+ aiv_cmd_fetch() {
48
+ local job_id="${1:-}"
49
+ [ -n "${job_id}" ] || aiv_die 2 "${ADAPTER_ID}: fetch <job_id> required"
50
+ aiv_assert_dryrun
51
+ aiv_die 9 "${ADAPTER_ID}: live fetch not yet wired (job=${job_id})"
52
+ }
53
+
54
+ aiv_dispatch "${ADAPTER_ID}" "native" "$@"
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env bash
2
+ # adapter-common.sh — shared boilerplate sourced by every adapter under
3
+ # scripts/ai-video/adapters/. Keeps individual adapters thin and makes
4
+ # the contract surface (subcommand dispatch, dry-run plumbing, stdout
5
+ # shape) uniform across providers.
6
+ #
7
+ # Sourced; never executed directly. Strict-mode flags are the caller's
8
+ # responsibility (every adapter starts with `set -euo pipefail`).
9
+
10
+ if [ -n "${AIV_ADAPTER_COMMON_LOADED:-}" ]; then
11
+ return 0 2>/dev/null || exit 0
12
+ fi
13
+ AIV_ADAPTER_COMMON_LOADED=1
14
+
15
+ # Resolve the lib dir from this file's location so adapters can source
16
+ # us via the canonical `$(dirname "$0")/../lib/...` path.
17
+ _aiv_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ # shellcheck source=/dev/null
19
+ . "${_aiv_lib_dir}/redact.sh"
20
+ # shellcheck source=/dev/null
21
+ . "${_aiv_lib_dir}/load-config.sh"
22
+
23
+ AIV_LIB_DIR="${_aiv_lib_dir}"
24
+ AIV_FIXTURE_ROOT="${_aiv_lib_dir}/fixtures"
25
+ export AIV_LIB_DIR AIV_FIXTURE_ROOT
26
+
27
+ # aiv_die <exit-code> <message> — write redacted message to stderr.
28
+ aiv_die() {
29
+ local code="${1:-1}"; shift || true
30
+ printf 'adapter: %s\n' "$*" | aiv_redact_stream >&2
31
+ exit "${code}"
32
+ }
33
+
34
+ # aiv_require_cmd <cmd> [<cmd> ...] — fail fast on missing dependency.
35
+ aiv_require_cmd() {
36
+ local cmd
37
+ for cmd in "$@"; do
38
+ command -v "${cmd}" >/dev/null 2>&1 \
39
+ || aiv_die 3 "required tool not found: ${cmd}"
40
+ done
41
+ }
42
+
43
+ # aiv_emit_dry_run <adapter-id> — print contract-shaped stdout JSON
44
+ # pointing at the adapter's fixture directory. Used by every adapter's
45
+ # `dry-run` subcommand.
46
+ aiv_emit_dry_run() {
47
+ local adapter="${1:-}"
48
+ [ -n "${adapter}" ] || aiv_die 3 "aiv_emit_dry_run: adapter id required"
49
+ local fixture_dir="${AIV_FIXTURE_ROOT}/${adapter}"
50
+ local result="${fixture_dir}/result.json"
51
+ if [ ! -f "${result}" ]; then
52
+ aiv_die 3 "fixture missing: ${result}"
53
+ fi
54
+ cat "${result}"
55
+ }
56
+
57
+ # aiv_capability <flag> — print the capability JSON for `--help`-style
58
+ # discovery: `{"audio": "native|none|per-model"}`.
59
+ aiv_capability() {
60
+ local flag="${1:-none}"
61
+ printf '{"audio":"%s"}\n' "${flag}"
62
+ }
63
+
64
+ # aiv_assert_dryrun — refuse to run a network path unless the caller
65
+ # explicitly opted in (AIV_DRYRUN=false set in this turn).
66
+ aiv_assert_dryrun() {
67
+ case "${AIV_DRYRUN:-true}" in
68
+ false|FALSE|0|no|NO) return 0 ;;
69
+ *) aiv_die 4 "live call refused: AIV_DRYRUN=${AIV_DRYRUN:-true}; use dry-run subcommand or set AIV_DRYRUN=false" ;;
70
+ esac
71
+ }
72
+
73
+ # aiv_dispatch <adapter-id> <capability> "$@" — generic subcommand router.
74
+ # Adapters override individual subcommands by defining bash functions
75
+ # named `aiv_cmd_submit` / `aiv_cmd_poll` / `aiv_cmd_fetch` / `aiv_cmd_run`
76
+ # BEFORE calling aiv_dispatch. Anything not overridden falls through to
77
+ # a stub that exits non-zero with a clear message.
78
+ aiv_dispatch() {
79
+ local adapter="${1:-}"; shift || true
80
+ local cap="${1:-none}"; shift || true
81
+ local sub="${1:-}"; shift || true
82
+ case "${sub}" in
83
+ capability)
84
+ aiv_capability "${cap}"
85
+ ;;
86
+ dry-run)
87
+ aiv_emit_dry_run "${adapter}"
88
+ ;;
89
+ submit)
90
+ declare -F aiv_cmd_submit >/dev/null \
91
+ && aiv_cmd_submit "$@" \
92
+ || aiv_die 5 "${adapter}: submit not implemented"
93
+ ;;
94
+ poll)
95
+ declare -F aiv_cmd_poll >/dev/null \
96
+ && aiv_cmd_poll "$@" \
97
+ || aiv_die 5 "${adapter}: poll not implemented"
98
+ ;;
99
+ fetch)
100
+ declare -F aiv_cmd_fetch >/dev/null \
101
+ && aiv_cmd_fetch "$@" \
102
+ || aiv_die 5 "${adapter}: fetch not implemented"
103
+ ;;
104
+ run)
105
+ declare -F aiv_cmd_run >/dev/null \
106
+ && aiv_cmd_run "$@" \
107
+ || aiv_die 5 "${adapter}: run not implemented"
108
+ ;;
109
+ "")
110
+ aiv_die 2 "${adapter}: subcommand required (capability|dry-run|submit|poll|fetch|run)"
111
+ ;;
112
+ *)
113
+ aiv_die 2 "${adapter}: unknown subcommand '${sub}'"
114
+ ;;
115
+ esac
116
+ }
@@ -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