@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.
- package/.agent-src/commands/video/from-script.md +123 -0
- package/.agent-src/commands/video/scene.md +92 -0
- package/.agent-src/commands/video/stitch.md +83 -0
- package/.agent-src/commands/video/storyboard.md +95 -0
- package/.agent-src/commands/video.md +59 -0
- package/.agent-src/personas/README.md +3 -0
- package/.agent-src/personas/ai-video-technical-director.md +81 -0
- package/.agent-src/personas/hollywood-director.md +99 -0
- package/.agent-src/personas/pixar-storyboard-artist.md +98 -0
- package/.agent-src/skills/character-consistency/SKILL.md +120 -0
- package/.agent-src/skills/motion-choreographer/SKILL.md +149 -0
- package/.agent-src/skills/pixar-storyteller/SKILL.md +107 -0
- package/.agent-src/skills/scene-expander/SKILL.md +122 -0
- package/.agent-src/skills/scene-expander/scene-blueprint.schema.yaml +108 -0
- package/.agent-src/skills/subagent-orchestration/SKILL.md +17 -15
- package/.agent-src/skills/video-director/SKILL.md +113 -0
- package/.agent-src/templates/agent-settings.md +19 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.claude-plugin/marketplace.json +11 -1
- package/CHANGELOG.md +22 -0
- package/README.md +4 -4
- package/config/agent-settings.template.yml +28 -0
- package/docs/adrs/caveman/0001-default-off-until-bench.md +2 -2
- package/docs/adrs/cost/0001-hard-stop-hook.md +1 -1
- package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +2 -2
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +14 -4
- package/docs/contracts/command-clusters.md +1 -0
- package/docs/contracts/compression-default-kill-criterion.md +1 -1
- package/docs/contracts/file-ownership-matrix.json +337 -0
- package/docs/getting-started.md +1 -1
- package/docs/parity/ruflo.md +3 -3
- package/package.json +1 -1
- package/scripts/ai-video/adapters/gemini-veo.sh +57 -0
- package/scripts/ai-video/adapters/higgsfield.sh +82 -0
- package/scripts/ai-video/adapters/kling.sh +54 -0
- package/scripts/ai-video/adapters/openai-images.sh +52 -0
- package/scripts/ai-video/adapters/sora.sh +54 -0
- package/scripts/ai-video/lib/adapter-common.sh +116 -0
- package/scripts/ai-video/lib/adapter-contract.md +163 -0
- package/scripts/ai-video/lib/fixtures/gemini-veo/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/gemini-veo/scene-0001.mp4 +1 -0
- package/scripts/ai-video/lib/fixtures/higgsfield/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/higgsfield/scene-0001.mp4 +1 -0
- package/scripts/ai-video/lib/fixtures/kling/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/kling/scene-0001.mp4 +1 -0
- package/scripts/ai-video/lib/fixtures/openai-images/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/openai-images/scene-0001.png +3 -0
- package/scripts/ai-video/lib/fixtures/sora/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/sora/scene-0001.mp4 +1 -0
- package/scripts/ai-video/lib/load-config.sh +140 -0
- package/scripts/ai-video/lib/operator-pick.sh +119 -0
- package/scripts/ai-video/lib/parse-blueprint.sh +122 -0
- package/scripts/ai-video/lib/redact.sh +85 -0
- package/scripts/ai-video/lib/validate-deps.sh +132 -0
- package/scripts/ai-video/stitch.sh +154 -0
- package/scripts/ai-video/test-pipeline.sh +169 -0
- package/scripts/schemas/command.schema.json +8 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-deps.sh — startup validator invoked by every /video:* command
|
|
3
|
+
# BEFORE any adapter or network call. Reads the YAML frontmatter from
|
|
4
|
+
# the command file, resolves every declared persona + skill against
|
|
5
|
+
# .agent-src/personas/ and .agent-src/skills/, and fails fast with the
|
|
6
|
+
# missing-id list.
|
|
7
|
+
#
|
|
8
|
+
# Scope (per roadmap Phase 5 Step 5): existence + frontmatter `id` match
|
|
9
|
+
# only. No version pinning (scope-control § rules); no schema validation
|
|
10
|
+
# of persona / skill bodies — that lives in `task lint-skills`.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# validate-deps.sh <path-to-command.md>
|
|
14
|
+
#
|
|
15
|
+
# Exit codes:
|
|
16
|
+
# 0 all declared personas + skills resolve
|
|
17
|
+
# 2 command file missing or no frontmatter
|
|
18
|
+
# 3 one or more declared ids do not resolve (list on stderr)
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
if [ "$#" -ne 1 ]; then
|
|
23
|
+
printf 'validate-deps: usage: %s <path-to-command.md>\n' "$0" >&2
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
cmd_file="$1"
|
|
28
|
+
if [ ! -f "${cmd_file}" ]; then
|
|
29
|
+
printf 'validate-deps: command file not found: %s\n' "${cmd_file}" >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Resolve repo root from this script's location (scripts/ai-video/lib/).
|
|
34
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
repo_root="$(cd "${script_dir}/../../.." && pwd)"
|
|
36
|
+
|
|
37
|
+
# AIV_PERSONAS_DIR / AIV_SKILLS_DIR allow overrides for tests; defaults
|
|
38
|
+
# point at the generated mirrors (skills+personas live there at runtime).
|
|
39
|
+
personas_dir="${AIV_PERSONAS_DIR:-${repo_root}/.agent-src/personas}"
|
|
40
|
+
skills_dir="${AIV_SKILLS_DIR:-${repo_root}/.agent-src/skills}"
|
|
41
|
+
|
|
42
|
+
# Extract the frontmatter block (between the first two `---` lines).
|
|
43
|
+
fm="$(awk '
|
|
44
|
+
BEGIN { in_fm=0; count=0 }
|
|
45
|
+
/^---[[:space:]]*$/ { count++; if (count==1) { in_fm=1; next } if (count==2) { exit } }
|
|
46
|
+
in_fm { print }
|
|
47
|
+
' "${cmd_file}")"
|
|
48
|
+
|
|
49
|
+
if [ -z "${fm}" ]; then
|
|
50
|
+
printf 'validate-deps: no YAML frontmatter in %s\n' "${cmd_file}" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Parse `personas:` and `skills:` lines. We accept inline-list form only
|
|
55
|
+
# (the template we ship), e.g. `personas: [a, b]`. No multi-line block
|
|
56
|
+
# form — keeps the validator dependency-free (no yq / python).
|
|
57
|
+
_extract_list() {
|
|
58
|
+
# $1 = key name (personas|skills)
|
|
59
|
+
printf '%s\n' "${fm}" \
|
|
60
|
+
| awk -v key="$1" '
|
|
61
|
+
$0 ~ "^" key ":" {
|
|
62
|
+
line=$0
|
|
63
|
+
sub("^" key ":[[:space:]]*", "", line)
|
|
64
|
+
sub("^\\[", "", line)
|
|
65
|
+
sub("\\][[:space:]]*$", "", line)
|
|
66
|
+
gsub(",", " ", line)
|
|
67
|
+
print line
|
|
68
|
+
}' \
|
|
69
|
+
| tr -s ' ' '\n' \
|
|
70
|
+
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \
|
|
71
|
+
| sed '/^$/d'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Portable to macOS bash 3.2 — no `mapfile`.
|
|
75
|
+
personas=()
|
|
76
|
+
while IFS= read -r _id; do
|
|
77
|
+
personas+=("${_id}")
|
|
78
|
+
done < <(_extract_list personas)
|
|
79
|
+
skills=()
|
|
80
|
+
while IFS= read -r _id; do
|
|
81
|
+
skills+=("${_id}")
|
|
82
|
+
done < <(_extract_list skills)
|
|
83
|
+
|
|
84
|
+
missing=()
|
|
85
|
+
|
|
86
|
+
# bash 3.2 treats `"${arr[@]}"` on an empty array as unbound under
|
|
87
|
+
# `set -u`. Guard each loop with the length check.
|
|
88
|
+
|
|
89
|
+
# A persona is "present" if .agent-src/personas/<id>.md exists OR
|
|
90
|
+
# .agent-src/personas/<id>/persona.md exists (template-specialist shape).
|
|
91
|
+
if [ "${#personas[@]}" -gt 0 ]; then
|
|
92
|
+
for p in "${personas[@]}"; do
|
|
93
|
+
[ -z "${p}" ] && continue
|
|
94
|
+
if [ ! -f "${personas_dir}/${p}.md" ] && [ ! -f "${personas_dir}/${p}/persona.md" ]; then
|
|
95
|
+
missing+=("persona:${p}")
|
|
96
|
+
continue
|
|
97
|
+
fi
|
|
98
|
+
# Frontmatter id match: read `id:` (optional) or `name:` from the
|
|
99
|
+
# persona file; if present it must equal the declared slug.
|
|
100
|
+
src_file="${personas_dir}/${p}.md"
|
|
101
|
+
[ -f "${src_file}" ] || src_file="${personas_dir}/${p}/persona.md"
|
|
102
|
+
declared_id="$(awk '/^id:[[:space:]]*/ { sub("^id:[[:space:]]*", ""); print; exit } /^name:[[:space:]]*/ { sub("^name:[[:space:]]*", ""); print; exit }' "${src_file}" | tr -d '"' | tr -d "'")"
|
|
103
|
+
if [ -n "${declared_id}" ] && [ "${declared_id}" != "${p}" ]; then
|
|
104
|
+
missing+=("persona:${p} (file declares id=${declared_id})")
|
|
105
|
+
fi
|
|
106
|
+
done
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# A skill is "present" if .agent-src/skills/<id>/SKILL.md exists.
|
|
110
|
+
if [ "${#skills[@]}" -gt 0 ]; then
|
|
111
|
+
for s in "${skills[@]}"; do
|
|
112
|
+
[ -z "${s}" ] && continue
|
|
113
|
+
skill_file="${skills_dir}/${s}/SKILL.md"
|
|
114
|
+
if [ ! -f "${skill_file}" ]; then
|
|
115
|
+
missing+=("skill:${s}")
|
|
116
|
+
continue
|
|
117
|
+
fi
|
|
118
|
+
declared_id="$(awk '/^name:[[:space:]]*/ { sub("^name:[[:space:]]*", ""); print; exit }' "${skill_file}" | tr -d '"' | tr -d "'")"
|
|
119
|
+
if [ -n "${declared_id}" ] && [ "${declared_id}" != "${s}" ]; then
|
|
120
|
+
missing+=("skill:${s} (file declares name=${declared_id})")
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if [ "${#missing[@]}" -gt 0 ]; then
|
|
126
|
+
printf 'validate-deps: unresolved declarations in %s:\n' "${cmd_file}" >&2
|
|
127
|
+
printf ' - %s\n' "${missing[@]}" >&2
|
|
128
|
+
exit 3
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Silent success — callers can `set -e` and continue.
|
|
132
|
+
exit 0
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# stitch.sh — ffmpeg-based clip concatenator for /video:* pipelines.
|
|
3
|
+
#
|
|
4
|
+
# Drives the cut from <project>/manifest.json (an ordered array of
|
|
5
|
+
# `{scene_id, clip_path, audio_embedded, audio_path?, duration}`).
|
|
6
|
+
# Pass-through for audio_embedded=true; ffmpeg mux for audio_embedded
|
|
7
|
+
# =false; operator-supplied bed for video-only clips.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# stitch.sh <manifest.json> <output.mp4>
|
|
11
|
+
# [--skip-scene <id> ...]
|
|
12
|
+
# [--abort-on-missing | --continue]
|
|
13
|
+
# [--crossfade <seconds>]
|
|
14
|
+
#
|
|
15
|
+
# Failure semantics:
|
|
16
|
+
# - Missing clip → fail loud with scene_id and re-render hint
|
|
17
|
+
# unless --skip-scene <id> drops the clip from the cut or
|
|
18
|
+
# --continue is passed (the dropped scenes are written to stderr).
|
|
19
|
+
# - --abort-on-missing is the default.
|
|
20
|
+
# - Adapter-failure rollback contract: a failed adapter writes
|
|
21
|
+
# <project>/scenes/<id>/error.json; stitch.sh checks for that
|
|
22
|
+
# file alongside the clip and surfaces it instead of an opaque
|
|
23
|
+
# ffmpeg error.
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
# shellcheck source=lib/adapter-common.sh
|
|
28
|
+
. "$(dirname "$0")/lib/adapter-common.sh"
|
|
29
|
+
|
|
30
|
+
aiv_require_cmd jq
|
|
31
|
+
|
|
32
|
+
DRYRUN="${AIV_DRYRUN:-true}"
|
|
33
|
+
case "${DRYRUN}" in
|
|
34
|
+
false|FALSE|0|no|NO) DRYRUN_FLAG=0 ;;
|
|
35
|
+
*) DRYRUN_FLAG=1 ;;
|
|
36
|
+
esac
|
|
37
|
+
[ "${DRYRUN_FLAG}" -eq 0 ] && aiv_require_cmd ffmpeg
|
|
38
|
+
|
|
39
|
+
MANIFEST="${1:-}"
|
|
40
|
+
OUTPUT="${2:-}"
|
|
41
|
+
[ -n "${MANIFEST}" ] && [ -n "${OUTPUT}" ] \
|
|
42
|
+
|| aiv_die 2 "usage: stitch.sh <manifest.json> <output.mp4> [--skip-scene <id>] [--abort-on-missing|--continue] [--crossfade <s>]"
|
|
43
|
+
[ -r "${MANIFEST}" ] || aiv_die 2 "manifest not readable: ${MANIFEST}"
|
|
44
|
+
shift 2 || true
|
|
45
|
+
|
|
46
|
+
SKIP_IDS=""
|
|
47
|
+
MISSING_POLICY="abort"
|
|
48
|
+
CROSSFADE=""
|
|
49
|
+
while [ "$#" -gt 0 ]; do
|
|
50
|
+
case "$1" in
|
|
51
|
+
--skip-scene) SKIP_IDS="${SKIP_IDS} ${2:-}"; shift 2 ;;
|
|
52
|
+
--abort-on-missing) MISSING_POLICY="abort"; shift ;;
|
|
53
|
+
--continue) MISSING_POLICY="continue"; shift ;;
|
|
54
|
+
--crossfade) CROSSFADE="${2:-}"; shift 2 ;;
|
|
55
|
+
*) aiv_die 2 "stitch.sh: unknown flag '$1'" ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
skip_match() {
|
|
60
|
+
local id="$1" s
|
|
61
|
+
for s in ${SKIP_IDS}; do
|
|
62
|
+
[ "${s}" = "${id}" ] && return 0
|
|
63
|
+
done
|
|
64
|
+
return 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
PROJECT_DIR="$(cd "$(dirname "${MANIFEST}")" && pwd)"
|
|
68
|
+
WORK_DIR="$(mktemp -d -t aiv-stitch-XXXXXX)"
|
|
69
|
+
trap 'rm -rf "${WORK_DIR}"' EXIT
|
|
70
|
+
CONCAT_LIST="${WORK_DIR}/concat.txt"
|
|
71
|
+
: > "${CONCAT_LIST}"
|
|
72
|
+
|
|
73
|
+
# Accept both bare-array manifests and the documented object shape
|
|
74
|
+
# (`{ "scenes": [...] }`). Field names tolerate the legacy
|
|
75
|
+
# `scene_id` / `clip_path` keys and the example `id` / `expected_clip`.
|
|
76
|
+
SCENES_JQ='if type == "array" then .[] else .scenes[] end'
|
|
77
|
+
|
|
78
|
+
count=0
|
|
79
|
+
missing=0
|
|
80
|
+
plan=""
|
|
81
|
+
while IFS= read -r row; do
|
|
82
|
+
[ -n "${row}" ] || continue
|
|
83
|
+
scene_id="$(printf '%s' "${row}" | jq -r '.scene_id // .id')"
|
|
84
|
+
clip_path="$(printf '%s' "${row}" | jq -r '.clip_path // .expected_clip')"
|
|
85
|
+
audio_embedded="$(printf '%s' "${row}" | jq -r '.audio_embedded // false')"
|
|
86
|
+
audio_path="$(printf '%s' "${row}" | jq -r '.audio_path // empty')"
|
|
87
|
+
|
|
88
|
+
case "${clip_path}" in
|
|
89
|
+
/*) ;;
|
|
90
|
+
*) clip_path="${PROJECT_DIR}/${clip_path}" ;;
|
|
91
|
+
esac
|
|
92
|
+
|
|
93
|
+
if skip_match "${scene_id}"; then
|
|
94
|
+
printf 'stitch: skip scene=%s (operator --skip-scene)\n' "${scene_id}" >&2
|
|
95
|
+
continue
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [ "${DRYRUN_FLAG}" -eq 1 ]; then
|
|
99
|
+
plan="${plan}${scene_id}\taudio_embedded=${audio_embedded}\t${clip_path}\n"
|
|
100
|
+
count=$((count + 1))
|
|
101
|
+
continue
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [ ! -f "${clip_path}" ]; then
|
|
105
|
+
err_json="${PROJECT_DIR}/scenes/${scene_id}/error.json"
|
|
106
|
+
if [ -f "${err_json}" ]; then
|
|
107
|
+
printf 'stitch: scene=%s adapter-error: ' "${scene_id}" >&2
|
|
108
|
+
jq -r '"adapter=\(.adapter) exit=\(.exit_code) user_action=\(.user_action)"' "${err_json}" >&2
|
|
109
|
+
fi
|
|
110
|
+
if [ "${MISSING_POLICY}" = "continue" ]; then
|
|
111
|
+
printf 'stitch: continue past missing scene=%s clip=%s (re-render or --skip-scene next time)\n' \
|
|
112
|
+
"${scene_id}" "${clip_path}" >&2
|
|
113
|
+
missing=$((missing + 1))
|
|
114
|
+
continue
|
|
115
|
+
fi
|
|
116
|
+
aiv_die 7 "missing clip for scene=${scene_id} at ${clip_path} (re-render the scene, pass --skip-scene ${scene_id}, or use --continue)"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# For clips without embedded audio that ship a sibling track, mux
|
|
120
|
+
# via ffmpeg into a tmp file before concat. Pure pass-through for
|
|
121
|
+
# the embedded-audio case.
|
|
122
|
+
if [ "${audio_embedded}" = "false" ] && [ -n "${audio_path}" ] && [ -f "${audio_path}" ]; then
|
|
123
|
+
muxed="${WORK_DIR}/${scene_id}.mp4"
|
|
124
|
+
ffmpeg -loglevel error -y -i "${clip_path}" -i "${audio_path}" \
|
|
125
|
+
-c:v copy -c:a aac -shortest "${muxed}" >/dev/null \
|
|
126
|
+
|| aiv_die 8 "ffmpeg mux failed for scene=${scene_id}"
|
|
127
|
+
printf "file '%s'\n" "${muxed}" >> "${CONCAT_LIST}"
|
|
128
|
+
else
|
|
129
|
+
printf "file '%s'\n" "${clip_path}" >> "${CONCAT_LIST}"
|
|
130
|
+
fi
|
|
131
|
+
count=$((count + 1))
|
|
132
|
+
done <<EOF
|
|
133
|
+
$(jq -c "${SCENES_JQ}" "${MANIFEST}")
|
|
134
|
+
EOF
|
|
135
|
+
|
|
136
|
+
[ "${count}" -gt 0 ] || aiv_die 7 "stitch: no usable clips after manifest scan (missing=${missing}, skipped via --skip-scene)"
|
|
137
|
+
|
|
138
|
+
if [ "${DRYRUN_FLAG}" -eq 1 ]; then
|
|
139
|
+
printf 'stitch: dry-run plan (output=%s, scenes=%d):\n' "${OUTPUT}" "${count}" >&2
|
|
140
|
+
printf '%b' "${plan}" >&2
|
|
141
|
+
printf '{"output":"%s","scenes":%d,"missing":0,"dry_run":true}\n' "${OUTPUT}" "${count}"
|
|
142
|
+
exit 0
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [ -n "${CROSSFADE}" ]; then
|
|
146
|
+
printf 'stitch: crossfade=%ss requested — passing through ffmpeg xfade filter not yet implemented (concat path used)\n' \
|
|
147
|
+
"${CROSSFADE}" >&2
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
ffmpeg -loglevel error -y -f concat -safe 0 -i "${CONCAT_LIST}" \
|
|
151
|
+
-c copy "${OUTPUT}" >/dev/null \
|
|
152
|
+
|| aiv_die 8 "ffmpeg concat failed (manifest=${MANIFEST}, output=${OUTPUT})"
|
|
153
|
+
|
|
154
|
+
printf '{"output":"%s","scenes":%d,"missing":%d}\n' "${OUTPUT}" "${count}" "${missing}"
|