Phase 2: Production Bible — Character + Location bibles from scene data

Layer 3 implementation:
- Character Bible: canonical names, aliases, arcs, relationships, wardrobe
  states, emotional arcs, reference prompts — all grounded in scene evidence
- Location Bible: canonical names, variants, descriptions, types, features,
  mood associations, reference prompts — all grounded in scene evidence
- Combined Production Bible output for downstream layers
- Bible validator: duplicate detection, scene reference checks, hallucination
  detection, UNKNOWN field flagging
- Prompt contracts: L3_character_bible_v1, L3_location_bible_v1
- Named versioned output: character_bible_v1.json, location_bible_v1.json,
  production_bible_v1.json
- CLI: --phase 2 runs bible only, --phase omitted runs both phases
- OutputWriter: added write_named/write_named_raw for non-scene outputs

Tested on the_last_backup: 3 characters, 5 locations, 0 hallucinations,
3 warnings (UNKNOWN physical_description — correct behavior)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 16:51:55 -07:00
co-authored by Claude Opus 4.6
parent 74870f7c0d
commit 17e410751c
9 changed files with 756 additions and 77 deletions
+52 -24
View File
@@ -11,13 +11,14 @@ from dotenv import load_dotenv
def main():
load_dotenv()
parser = argparse.ArgumentParser(description="AI Movie Production Pipeline — Phase 1")
parser.add_argument("--script", type=str, help="Path to .fountain script file")
parser = argparse.ArgumentParser(description="AI Movie Production Pipeline")
parser.add_argument("--script", type=str, help="Path to .fountain script file (Phase 1)")
parser.add_argument("--project", type=str, help="Project name (determines output directory)")
parser.add_argument("--phase", type=int, default=None, choices=[1, 2], help="Run specific phase only (1=ingestion+extraction, 2=bible)")
parser.add_argument("--model", type=str, default="qwen3:14b", help="Model ID (default: qwen3:14b)")
parser.add_argument("--backend", type=str, default="ollama", choices=["ollama", "anthropic"], help="AI backend (default: ollama)")
parser.add_argument("--ollama-url", type=str, default="http://localhost:11434", help="Ollama server URL")
parser.add_argument("--scene", type=int, default=None, help="Process only this scene number")
parser.add_argument("--scene", type=int, default=None, help="Process only this scene number (Phase 1)")
parser.add_argument("--dry-run", action="store_true", help="Validate inputs only, no AI calls")
parser.add_argument("--force", action="store_true", help="Ignore cache, re-run even if unchanged")
parser.add_argument("--test", action="store_true", help="Run test suite against test_scripts/")
@@ -29,36 +30,63 @@ def main():
run_tests(args.model, args.backend, args.ollama_url, args.output_dir)
return
if not args.script or not args.project:
parser.error("--script and --project are required (unless using --test)")
if not args.project:
parser.error("--project is required (unless using --test)")
api_key = os.environ.get("ANTHROPIC_API_KEY", "")
if args.backend == "anthropic" and not api_key and not args.dry_run:
print("ERROR: ANTHROPIC_API_KEY not set. Set it in .env or environment.")
sys.exit(1)
if not os.path.exists(args.script):
print(f"ERROR: Script file not found: {args.script}")
sys.exit(1)
run_phase1_flag = args.phase is None or args.phase == 1
run_phase2_flag = args.phase is None or args.phase == 2
from src.execution.runner import run_phase1
# Phase 1: Script Ingestion + Understanding
if run_phase1_flag:
if not args.script:
if args.phase == 1 or args.phase is None:
parser.error("--script is required for Phase 1")
else:
if not os.path.exists(args.script):
print(f"ERROR: Script file not found: {args.script}")
sys.exit(1)
result = run_phase1(
script_path=args.script,
project_name=args.project,
api_key=api_key,
model=args.model,
backend=args.backend,
ollama_url=args.ollama_url,
output_dir=args.output_dir,
scene_filter=args.scene,
dry_run=args.dry_run,
force=args.force,
)
from src.execution.runner import run_phase1
if not result.success:
print(f"\nPIPELINE FAILED: {result.stop_reason}")
sys.exit(1)
result = run_phase1(
script_path=args.script,
project_name=args.project,
api_key=api_key,
model=args.model,
backend=args.backend,
ollama_url=args.ollama_url,
output_dir=args.output_dir,
scene_filter=args.scene,
dry_run=args.dry_run,
force=args.force,
)
if not result.success:
print(f"\nPHASE 1 FAILED: {result.stop_reason}")
sys.exit(1)
# Phase 2: Production Bible
if run_phase2_flag:
from src.bible.runner import run_phase2
bible_result = run_phase2(
project_name=args.project,
model=args.model,
backend=args.backend,
ollama_url=args.ollama_url,
api_key=api_key,
output_dir=args.output_dir,
dry_run=args.dry_run,
)
if not bible_result.success:
print(f"\nPHASE 2 FAILED: {bible_result.stop_reason}")
sys.exit(1)
print("\nPIPELINE COMPLETE")
sys.exit(0)