Skip to content

Commit 64ac74f

Browse files
Harden cinematic generation policies (#8)
Co-authored-by: Andrew Michael <AndrewMichael2020@users.noreply.github.com>
1 parent 52839a9 commit 64ac74f

15 files changed

Lines changed: 636 additions & 46 deletions

File tree

.github/workflows/dry-run.yml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
name: dry-run
22
on:
3-
# Keep development testing local. Do not consume GitHub Actions minutes
4-
# automatically on pushes or pull requests.
3+
push:
4+
branches: [main]
5+
pull_request:
56
workflow_dispatch:
67
permissions:
78
contents: read
9+
concurrency:
10+
group: dry-run-${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
812
jobs:
913
test:
1014
runs-on: ubuntu-latest
15+
timeout-minutes: 15
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
python-version: ['3.11', '3.12']
20+
env:
21+
# This workflow never receives provider credentials and never invokes --live.
22+
DEEPINFRA_TOKEN: ''
1123
steps:
1224
- uses: actions/checkout@v4
25+
with:
26+
persist-credentials: false
1327
- uses: actions/setup-python@v5
1428
with:
15-
python-version: '3.12'
29+
python-version: ${{ matrix.python-version }}
30+
cache: pip
1631
- run: python -m pip install -e . pytest
32+
- run: video-gen-secret-scan
1733
- run: pytest -q
1834
- run: video-gen preflight --profile cad_10
35+
- run: video-gen validate-stage2 sequences/clinic-reception-stage2.json

README.md

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Auditable Cinematic Production Lab
1+
# Cinematic Production Lab
22

3-
[![Dry-run CI](https://github.com/AndrewMichael2020/auditable-cinematic-production-lab/actions/workflows/dry-run.yml/badge.svg)](https://github.com/AndrewMichael2020/auditable-cinematic-production-lab/actions/workflows/dry-run.yml)
3+
[![Dry-run CI](https://github.com/AndrewMichael2020/cinematic-production-lab/actions/workflows/dry-run.yml/badge.svg)](https://github.com/AndrewMichael2020/cinematic-production-lab/actions/workflows/dry-run.yml)
44
[![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](pyproject.toml)
55
[![Tests: pytest](https://img.shields.io/badge/tests-pytest-0A9EDC?logo=pytest&logoColor=white)](tests)
66
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
@@ -26,8 +26,9 @@ open-ended generation demo:
2626
provider, cost, hash, lineage, and human-acceptance evidence.
2727
- **Honest QA:** accepted and rejected artifacts are retained separately; the latest rejected run
2828
has a documented voice-persona and lip-sync root-cause analysis.
29-
- **Safe automation:** GitHub workflows are manual-dispatch only, generation defaults to dry-run,
30-
and the paid smoke test requires an explicit `LIVE` confirmation.
29+
- **Safe automation:** push and pull-request CI runs tests, a tracked-credential scan, configuration
30+
preflight, and Stage 2 validation without provider credentials; the paid smoke test remains a
31+
separate manual workflow requiring explicit `LIVE` confirmation.
3132

3233
| Evidence | What it shows |
3334
|---|---|
@@ -54,6 +55,26 @@ orientation checks, face/mouth and action gates, perceptible ambience, outer fad
5455
human acceptance. The clinic v3 package is the first accepted Stage 2 delivery candidate; a second
5556
contrasting sequence is still required to exit the stage.
5657

58+
## Architecture
59+
60+
```text
61+
series + sequence manifests
62+
63+
model / price / secret policy validation
64+
65+
dry-run planner → reservation ledger → explicitly confirmed provider request
66+
↓ ↓
67+
typed provenance + media hashes reported-cost reconciliation
68+
69+
technical checks + human acceptance gates → FFmpeg delivery master
70+
```
71+
72+
The CLI is the policy boundary. Configuration is validated before orchestration; live requests
73+
require a current human-reviewed pricing snapshot, explicit confirmation, an approved registered
74+
model, and a reservation that fits both the run profile and provider-account ceiling. Voice
75+
generation additionally requires a versioned canonical realization and approved audition hash.
76+
Provider uncertainty never triggers an automatic retry or fallback.
77+
5778
## Current pause and latest-run decision
5879

5980
New generation is paused while production moves to an M5 Pro machine with 64 GB of memory. The
@@ -110,9 +131,11 @@ Choose exactly one run profile: **10, 15, or 20 dollars Canadian**, including a
110131
|---|---:|
111132
| CAD 10 | US$10.00 |
112133
| CAD 15 | US$9.75 |
113-
| CAD 20 | US$13.00 |
134+
| CAD 20 | US$15.00 |
114135

115-
The program must stop before the selected US-dollar cap. It must also respect a DeepInfra account spending limit of no more than US$13.00 for the proof. DeepInfra reports actual cost in `inference_status.cost`; the local append-only ledger records both reserved and reported cost.
136+
The program must stop before the selected US-dollar cap. It must also respect a DeepInfra account
137+
spending limit of no more than US$20.00. DeepInfra reports actual cost in
138+
`inference_status.cost`; the local append-only ledger records both reserved and reported cost.
116139

117140
Only sequential generation is allowed. Every request reserves its maximum expected cost before transmission. No recursive retries, parallel paid jobs, automatic provider fallback, or unbounded workflow reruns.
118141

@@ -159,6 +182,7 @@ test dependency, then validate the complete dry-run path:
159182

160183
```bash
161184
python -m pip install -e . pytest
185+
video-gen-secret-scan
162186
pytest -q
163187
video-gen preflight --profile cad_10
164188
video-gen validate-scene
@@ -183,7 +207,7 @@ perceptual lip sync, essential action, reference fidelity, persona/voice, ambien
183207
stitch integrity all have explicit human evidence.
184208

185209
Generated ledgers and media normally live under ignored `runs/` and `outputs/` directories. Selected
186-
auditable live runs may be force-added on a dedicated branch. Inspect and
210+
evidence-complete live runs may be force-added on a dedicated branch. Inspect and
187211
human-approve the four compiled prompts from `scenes/golden-scene.json` before any live draft run.
188212

189213
When moving to another machine, transfer ignored run folders separately from Git and verify their
@@ -217,9 +241,11 @@ Use `audit-artifacts` before cleanup. `prune-artifacts` is a dry run unless `--a
217241
only removes previews, sampled-frame sheets, and other deterministic derivatives. It retains raw and
218242
final media, ledgers, manifests, reports, hashes, and compact references fail-closed.
219243

220-
The repository workflows are manual-dispatch only; development pushes and pull requests do not
221-
start generation. If a human later chooses to run **live DeepInfra smoke test** and enters `LIVE`,
222-
the workflow makes exactly one FastWan request (maximum reserved
244+
The automatic **dry-run** workflow runs on pushes to `main` and pull requests across Python 3.11 and
245+
3.12. It receives no provider credential and never invokes `--live`; it scans tracked files for
246+
credential-shaped material, runs the test suite and configuration preflight, and validates the typed
247+
Stage 2 sequence. If a human later chooses to run **live DeepInfra smoke test** and enters `LIVE`,
248+
that separate manual workflow makes exactly one FastWan request (maximum reserved
223249
cost US$0.0125), never retries it, and retains the generated clip, append-only SQLite ledger,
224250
compiled prompt, command result, hashes, and JSON audit export as a workflow artifact for 30 days.
225251
Signed query parameters from provider output URLs are deliberately excluded from the ledger.

project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
},
140140
"pricing": {
141141
"verified_at": "2026-08-03",
142+
"max_age_days": 30,
142143
"currency": "USD",
143144
"fail_if_unverified_or_increased": true,
144145
"price_refresh_requires_human_approval": true

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ requires-python = ">=3.11"
1010

1111
[project.scripts]
1212
video-gen = "video_gen.cli:main"
13+
video-gen-secret-scan = "video_gen.secret_scan:main"
1314

1415
[tool.setuptools.packages.find]
1516
where = ["src"]
1617

1718
[tool.pytest.ini_options]
1819
testpaths = ["tests"]
1920
pythonpath = ["src"]
20-

series/surrey-care/series.json

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,24 @@
3131
"provider_voice": "Unified Chatterbox performance master, seed 3407",
3232
"language": "English (US provider setting), directed as Canadian English",
3333
"accent_direction": "Natural Lower Mainland Canadian English; composed clinical pacing; no generic American announcer tone and no stylized Punjabi accent.",
34-
"performance_baseline": "Low-key, clear and empathetic with a restrained dramatic arc: professional warmth, precise B.C. Services Card diction, concerned weight on inactive M.S.P., then compassionate but firm uncertainty. Generate the sequence in one continuous take and split only at deliberate pauses."
34+
"performance_baseline": "Low-key, clear and empathetic with a restrained dramatic arc: professional warmth, precise B.C. Services Card diction, concerned weight on inactive M.S.P., then compassionate but firm uncertainty. Generate the sequence in one continuous take and split only at deliberate pauses.",
35+
"voice_realization": {
36+
"voice_realization_id": "vr-amrit-chatterbox-3407-v1",
37+
"effective_persona_version": "pv01",
38+
"provider_model_id": "ResembleAI/chatterbox-turbo",
39+
"provider_voice": "Unified Chatterbox performance master, seed 3407",
40+
"immutable_settings": {
41+
"seed": 3407,
42+
"response_format": "wav"
43+
},
44+
"approval": {
45+
"status": "pending_human_audition",
46+
"audition_path": "runs/clinic-stage2-20260803T060048Z/audio/amrit-sequence-master-performance-v2.wav",
47+
"audition_sha256": "abf02c5a048fa05da4ec6edd0ebe3099b57288b59a5a9055a6c53dbeb71d77bf",
48+
"reviewed_by": null,
49+
"reviewed_at": null
50+
}
51+
}
3552
},
3653
"reference_pack": {
3754
"version": "rp01",
@@ -62,7 +79,24 @@
6279
"provider_voice": "Puck (Male)",
6380
"language": "English (US provider setting), directed as Canadian English",
6481
"accent_direction": "Natural Lower Mainland Canadian English with restrained Korean-influenced cadence only if stable; no caricature and no accent switching.",
65-
"performance_baseline": "Quiet, tired and direct, with short breaths and no theatrical distress."
82+
"performance_baseline": "Quiet, tired and direct, with short breaths and no theatrical distress.",
83+
"voice_realization": {
84+
"voice_realization_id": "vr-daniel-puck-v1",
85+
"effective_persona_version": "pv01",
86+
"provider_model_id": "PrunaAI/p-video-avatar",
87+
"provider_voice": "Puck (Male)",
88+
"immutable_settings": {
89+
"seed": 0,
90+
"voice_language": "English (US)"
91+
},
92+
"approval": {
93+
"status": "not_auditioned",
94+
"audition_path": null,
95+
"audition_sha256": null,
96+
"reviewed_by": null,
97+
"reviewed_at": null
98+
}
99+
}
66100
},
67101
"reference_pack": {
68102
"version": "rp01",

src/video_gen/cli.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .retention import audit_run_artifacts, prune_recomputable_artifacts
2626
from .stage2 import (audit_stage2_sequence, compile_stage2_prompt,
2727
compile_stage2_take_prompt,
28-
load_stage2_sequence)
28+
load_series, load_stage2_sequence)
2929

3030

3131
def parser() -> argparse.ArgumentParser:
@@ -114,14 +114,18 @@ def parser() -> argparse.ArgumentParser:
114114
speech.add_argument("--profile", default="cad_10")
115115
speech.add_argument("--text", required=True)
116116
speech.add_argument("--seed", type=int, default=0)
117+
speech.add_argument("--series-manifest")
118+
speech.add_argument("--character-id")
117119
speech.add_argument("--live", action="store_true")
118120
speech.add_argument("--confirm-live", action="store_true")
119121
speech.add_argument("--result")
120122
avatar = commands.add_parser("plan-avatar", help="generate one explicitly approved partner lip-sync clip")
121123
avatar.add_argument("--profile", default="cad_10")
122124
avatar.add_argument("--image", required=True)
123125
avatar.add_argument("--script", required=True)
124-
avatar.add_argument("--voice", required=True)
126+
avatar.add_argument("--voice")
127+
avatar.add_argument("--series-manifest")
128+
avatar.add_argument("--character-id")
125129
avatar.add_argument("--gaze-direction", choices=["screen_left", "screen_right"], required=True)
126130
avatar.add_argument(
127131
"--speaker-position", choices=["only_person", "frame_left", "frame_right"],
@@ -271,6 +275,23 @@ def image_video_audio_input(value: str | Path, *, live: bool = False) -> str:
271275
return f"data:{media_type};base64,{base64.b64encode(source.read_bytes()).decode()}"
272276

273277

278+
def canonical_voice_binding(
279+
series_manifest: str | None, character_id: str | None,
280+
) -> tuple[str, dict] | None:
281+
if series_manifest is None and character_id is None:
282+
return None
283+
if not series_manifest or not character_id:
284+
raise VideoGenError("voice binding requires --series-manifest and --character-id together")
285+
series = load_series(series_manifest)
286+
persona = next(
287+
(item for item in series["canonical_personas"] if item["character_id"] == character_id),
288+
None,
289+
)
290+
if persona is None:
291+
raise VideoGenError(f"unknown canonical character: {character_id}")
292+
return persona["voice"]["provider_voice"], persona["voice"]["voice_realization"]
293+
294+
274295
def main(argv: list[str] | None = None) -> int:
275296
args = parser().parse_args(argv)
276297
try:
@@ -478,19 +499,30 @@ def main(argv: list[str] | None = None) -> int:
478499
partner_avatar_attempt_cap=args.partner_avatar_attempt_cap,
479500
)
480501
if args.command == "plan-speech":
502+
voice_binding = canonical_voice_binding(args.series_manifest, args.character_id)
481503
request = orchestrator.run_speech(
482-
args.text, seed=args.seed, live=args.live, confirmed=args.confirm_live)
504+
args.text, seed=args.seed, live=args.live, confirmed=args.confirm_live,
505+
voice_realization=(voice_binding[1] if voice_binding else None),
506+
)
483507
emit_json({**request.__dict__, "reserved_usd": str(request.reserved_usd)}, args.result)
484508
return 0
485509
if args.command == "plan-avatar":
510+
voice_binding = canonical_voice_binding(args.series_manifest, args.character_id)
511+
voice = args.voice or (voice_binding[0] if voice_binding else None)
512+
if not voice:
513+
raise VideoGenError(
514+
"avatar generation requires --voice or a canonical series voice binding"
515+
)
486516
request = orchestrator.run_avatar(
487-
avatar_image_input(args.image, live=args.live), args.script, args.voice,
517+
avatar_image_input(args.image, live=args.live), args.script, voice,
488518
seed=args.seed,
489519
max_seconds=args.max_seconds, gaze_direction=args.gaze_direction,
490520
speaker_position=args.speaker_position,
491521
response_anticipation=args.response_anticipation,
492522
performance_direction=args.performance, live=args.live, confirmed=args.confirm_live,
493-
allow_partner=args.allow_partner_avatar, output_dir=args.output_dir)
523+
allow_partner=args.allow_partner_avatar,
524+
voice_realization=(voice_binding[1] if voice_binding else None),
525+
output_dir=args.output_dir)
494526
emit_json({**request.__dict__, "reserved_usd": str(request.reserved_usd)}, args.result)
495527
return 0
496528
if args.command == "plan-image-video":

0 commit comments

Comments
 (0)