@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.
- 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/adversarial-review/SKILL.md +2 -1
- package/.agent-src/skills/canvas-design/SKILL.md +11 -6
- package/.agent-src/skills/character-consistency/SKILL.md +120 -0
- package/.agent-src/skills/fe-design/SKILL.md +8 -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/prompt-optimizer/SKILL.md +29 -5
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +9 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +57 -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/tailwind-engineer/SKILL.md +14 -0
- 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 +53 -1
- package/.claude-plugin/marketplace.json +11 -1
- package/CHANGELOG.md +88 -138
- 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 +3 -3
- package/docs/archive/CHANGELOG-pre-2.20.0.md +159 -0
- package/docs/catalog.md +16 -5
- 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 +344 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/prompt-templates.md +166 -0
- 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,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}"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-pipeline.sh — offline smoke test for /video:* against
|
|
3
|
+
# agents/ai-video/examples/banana-arc/. Dry-run only; no network.
|
|
4
|
+
#
|
|
5
|
+
# Asserts (per agents/roadmaps/ai-video-pipeline.md Phase 6 Step 3):
|
|
6
|
+
# 1. parse-blueprint.sh output matches the committed expected.json
|
|
7
|
+
# per scene (3 scenes, 3 tiers).
|
|
8
|
+
# 2. character.json descriptor tokens (silhouette, palette, wardrobe,
|
|
9
|
+
# prop) appear verbatim in each scene's prompt.subject — the
|
|
10
|
+
# load-bearing character-lock substring assertion.
|
|
11
|
+
# 3. audio.* branching is correct: scene 2 → enable_native_audio=true,
|
|
12
|
+
# scenes 1+3 → false.
|
|
13
|
+
# 4. native-audio-capable adapter (gemini-veo) advertises capability
|
|
14
|
+
# audio=native; non-audio adapter (openai-images) advertises
|
|
15
|
+
# audio=none; stitch.sh sees audio_embedded per scene.
|
|
16
|
+
# 5. stitch.sh dry-run returns the committed manifest's stitch_output
|
|
17
|
+
# path without invoking ffmpeg or any network.
|
|
18
|
+
# 6. visual regression: each scene's locked.png is non-empty + has
|
|
19
|
+
# PNG magic; pairwise NCC ≥ 0.95 when `compare` is available.
|
|
20
|
+
# When unavailable, asserts byte-identity (the three frames are
|
|
21
|
+
# committed identical for the offline path).
|
|
22
|
+
#
|
|
23
|
+
# Exit 0 = all assertions pass; 1 = at least one failure (counted +
|
|
24
|
+
# summarized at the end).
|
|
25
|
+
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
29
|
+
ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
30
|
+
PROJECT="$ROOT/agents/ai-video/examples/banana-arc"
|
|
31
|
+
|
|
32
|
+
PASS=0
|
|
33
|
+
FAIL=0
|
|
34
|
+
FAILS=""
|
|
35
|
+
|
|
36
|
+
ok() { PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
|
|
37
|
+
fail() { FAIL=$((FAIL+1)); FAILS="$FAILS\n ❌ $1"; printf ' ❌ %s\n' "$1"; }
|
|
38
|
+
|
|
39
|
+
require() {
|
|
40
|
+
if command -v "$1" >/dev/null 2>&1; then return 0; fi
|
|
41
|
+
fail "required tool missing: $1"
|
|
42
|
+
return 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
require jq || exit 1
|
|
46
|
+
|
|
47
|
+
printf '\n== test-pipeline.sh — banana-arc golden run (offline) ==\n\n'
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------- 1
|
|
50
|
+
printf '[1/6] parse-blueprint vs. expected.json\n'
|
|
51
|
+
SCENES="01-simple 02-dialogue-native-audio 03-edge-duration"
|
|
52
|
+
for s in $SCENES; do
|
|
53
|
+
actual="$(bash "$ROOT/scripts/ai-video/lib/parse-blueprint.sh" "$PROJECT/scenes/$s/blueprint.txt" 2>/dev/null \
|
|
54
|
+
| jq -S . 2>/dev/null || true)"
|
|
55
|
+
expected="$(jq -S . "$PROJECT/scenes/$s/expected.json" 2>/dev/null || true)"
|
|
56
|
+
if [ -z "$actual" ]; then
|
|
57
|
+
fail "$s: parse-blueprint produced no output"
|
|
58
|
+
elif [ "$actual" = "$expected" ]; then
|
|
59
|
+
ok "$s: parse-blueprint matches expected.json"
|
|
60
|
+
else
|
|
61
|
+
fail "$s: parse-blueprint mismatch (diff actual vs. expected.json)"
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------- 2
|
|
66
|
+
printf '\n[2/6] character.json descriptors verbatim in prompt.subject\n'
|
|
67
|
+
SILHOUETTE="$(jq -r '.characters[0].silhouette' "$PROJECT/character.json")"
|
|
68
|
+
PALETTE="$(jq -r '.characters[0].palette' "$PROJECT/character.json")"
|
|
69
|
+
WARDROBE="$(jq -r '.characters[0].wardrobe' "$PROJECT/character.json")"
|
|
70
|
+
PROP="$(jq -r '.characters[0].prop' "$PROJECT/character.json")"
|
|
71
|
+
|
|
72
|
+
for s in $SCENES; do
|
|
73
|
+
subj="$(jq -r '.prompt.subject' "$PROJECT/scenes/$s/expected.json")"
|
|
74
|
+
miss=""
|
|
75
|
+
case "$subj" in *"$SILHOUETTE"*) :;; *) miss="$miss silhouette";; esac
|
|
76
|
+
case "$subj" in *"$PALETTE"*) :;; *) miss="$miss palette";; esac
|
|
77
|
+
case "$subj" in *"$WARDROBE"*) :;; *) miss="$miss wardrobe";; esac
|
|
78
|
+
case "$subj" in *"$PROP"*) :;; *) miss="$miss prop";; esac
|
|
79
|
+
if [ -z "$miss" ]; then
|
|
80
|
+
ok "$s: silhouette + palette + wardrobe + prop verbatim in prompt.subject"
|
|
81
|
+
else
|
|
82
|
+
fail "$s: missing verbatim tokens in prompt.subject:$miss"
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------- 3
|
|
87
|
+
printf '\n[3/6] audio.* branching matches manifest\n'
|
|
88
|
+
for entry in 01-simple:false 02-dialogue-native-audio:true 03-edge-duration:false; do
|
|
89
|
+
s="${entry%%:*}"
|
|
90
|
+
want="${entry##*:}"
|
|
91
|
+
got="$(jq -r '.audio.enable_native_audio' "$PROJECT/scenes/$s/expected.json")"
|
|
92
|
+
manifest_got="$(jq -r --arg id "$s" '.scenes[] | select(.id==$id) | .audio_embedded' "$PROJECT/manifest.json")"
|
|
93
|
+
if [ "$got" = "$want" ] && [ "$manifest_got" = "$want" ]; then
|
|
94
|
+
ok "$s: enable_native_audio=$got, manifest.audio_embedded=$manifest_got"
|
|
95
|
+
else
|
|
96
|
+
fail "$s: audio branching drift (expected=$want, blueprint=$got, manifest=$manifest_got)"
|
|
97
|
+
fi
|
|
98
|
+
done
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------- 4
|
|
101
|
+
printf '\n[4/6] adapter capability declarations\n'
|
|
102
|
+
declare_caps() {
|
|
103
|
+
local adapter="$1"; local expected="$2"
|
|
104
|
+
local out got
|
|
105
|
+
out="$(AIV_DRYRUN=true bash "$ROOT/scripts/ai-video/adapters/$adapter.sh" capability 2>/dev/null || true)"
|
|
106
|
+
got="$(printf '%s' "$out" | jq -r '.audio // empty' 2>/dev/null || true)"
|
|
107
|
+
if [ "$got" = "$expected" ]; then
|
|
108
|
+
ok "$adapter: capability.audio=$got"
|
|
109
|
+
else
|
|
110
|
+
fail "$adapter: capability mismatch (want audio=$expected, got: ${got:-<unparseable: $out>})"
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
declare_caps gemini-veo "native"
|
|
114
|
+
declare_caps openai-images "none"
|
|
115
|
+
declare_caps sora "native"
|
|
116
|
+
declare_caps kling "none"
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------- 5
|
|
119
|
+
printf '\n[5/6] stitch.sh dry-run returns manifest output path\n'
|
|
120
|
+
STITCH_OUT="$(jq -r '.stitch_output' "$PROJECT/manifest.json")"
|
|
121
|
+
stitch_log="$(AIV_DRYRUN=true bash "$ROOT/scripts/ai-video/stitch.sh" \
|
|
122
|
+
"$PROJECT/manifest.json" "$PROJECT/$STITCH_OUT" 2>&1 || true)"
|
|
123
|
+
case "$stitch_log" in
|
|
124
|
+
*"$STITCH_OUT"*) ok "stitch.sh dry-run referenced $STITCH_OUT";;
|
|
125
|
+
*) fail "stitch.sh dry-run did not reference $STITCH_OUT (log: $stitch_log)";;
|
|
126
|
+
esac
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------- 6
|
|
129
|
+
printf '\n[6/6] visual regression (locked.png pairwise)\n'
|
|
130
|
+
PNG_MAGIC="$(printf '\x89PNG\r\n\x1a\n')"
|
|
131
|
+
prev=""
|
|
132
|
+
have_compare=0
|
|
133
|
+
command -v compare >/dev/null 2>&1 && have_compare=1
|
|
134
|
+
for s in $SCENES; do
|
|
135
|
+
f="$PROJECT/scenes/$s/fixtures/frames/locked.png"
|
|
136
|
+
if [ ! -s "$f" ]; then
|
|
137
|
+
fail "$s: locked.png missing or empty"
|
|
138
|
+
continue
|
|
139
|
+
fi
|
|
140
|
+
head -c 8 "$f" | od -An -c | tr -d ' \n' | grep -q '211PNG' \
|
|
141
|
+
&& ok "$s: locked.png is a valid PNG ($(wc -c < "$f" | tr -d ' ') bytes)" \
|
|
142
|
+
|| fail "$s: locked.png lacks PNG magic"
|
|
143
|
+
if [ -n "$prev" ]; then
|
|
144
|
+
if [ "$have_compare" -eq 1 ]; then
|
|
145
|
+
ncc="$(compare -metric NCC "$prev" "$f" null: 2>&1 || true)"
|
|
146
|
+
awk_ok="$(awk -v v="$ncc" 'BEGIN { exit !(v+0 >= 0.95) }' && echo yes || echo no)"
|
|
147
|
+
if [ "$awk_ok" = "yes" ]; then
|
|
148
|
+
ok "$s: NCC vs. previous = $ncc (≥ 0.95)"
|
|
149
|
+
else
|
|
150
|
+
fail "$s: NCC vs. previous = $ncc (< 0.95)"
|
|
151
|
+
fi
|
|
152
|
+
else
|
|
153
|
+
if cmp -s "$prev" "$f"; then
|
|
154
|
+
ok "$s: byte-identical to previous (compare unavailable; offline fallback)"
|
|
155
|
+
else
|
|
156
|
+
fail "$s: differs from previous frame and compare is unavailable"
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
prev="$f"
|
|
161
|
+
done
|
|
162
|
+
|
|
163
|
+
# ----------------------------------------------------------------
|
|
164
|
+
printf '\n----------------\nresult: %d passed · %d failed\n' "$PASS" "$FAIL"
|
|
165
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
166
|
+
printf '%b\n' "$FAILS"
|
|
167
|
+
exit 1
|
|
168
|
+
fi
|
|
169
|
+
exit 0
|
|
@@ -34,6 +34,14 @@
|
|
|
34
34
|
"pattern": "^[a-z][a-z0-9-]*$"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
+
"personas": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"pattern": "^[a-z][a-z0-9-]*$"
|
|
42
|
+
},
|
|
43
|
+
"description": "Personas this command invokes (see .agent-src.uncompressed/personas/). Mirrors `skills:`; each entry must match a persona slug."
|
|
44
|
+
},
|
|
37
45
|
"cluster": {
|
|
38
46
|
"type": "string",
|
|
39
47
|
"pattern": "^[a-z][a-z0-9-]*$",
|