Skip to main content

nw

Narrative Workflow — an application-orchestration framework for audiovisual projects (music videos, explainers, podcast clips, slideshows).

A project is a folder. An "app" is a small specialization on top. nw gives you the typed core — schema, folder facade, render workflow, strategy registry, storyboard bridge, QA reports, and a provenance graph — so apps don't have to.

import nw

proj = nw.Project.init("my_video", song="track.mp3")
proj.add_character("alex", description="warm, deadpan")
proj.set_character_anchor("alex", "characters/alex/refs/headshot.png")
proj.upsert_shot(
    nw.ShotSpec(
        id="shot_01",
        start_s=0.0,
        end_s=8.0,
        characters=("alex",),
        render_strategy="lipsync",
    )
)

prep = nw.prepare_shot(proj, "shot_01")  # local-only work
plan = nw.plan_render_shot(prep)  # pure data; inspect cost
print(f"estimated: ${plan.total_cost_usd:.2f}")
output = nw.execute_render(prep, plan, project=proj)  # the only billable phase

Install

pip install nw

What you get

A typed project on disk

nw.Project is a small facade over a project folder. The folder is the single source of truth: project.json holds project-level metadata; a per-project lacing graph (project.annot.sqlite) holds sections, shots, character/environment refs, and decisions.

proj = nw.Project("path/to/project")  # opens existing
summary = proj.read_summary()  # typed ProjectSummary
spec = proj.read_spec()  # typed ProjectSpec
proj.upsert_shot(shot)  # graph-backed
proj.log_decision("retry_shot", shot_id="shot_03", reason="lipsync drift")

Project.read_summary() returns the small set of facts you usually want at a glance: title, song path, counts of characters / sections / shots, how many shots are rendered, which lifecycle stages have been reached.

Plan → execute, with a budget gate

Rendering splits cleanly into three phases:

  1. prepare (nw.prepare_shot) — local work: audio slice, anchor resolution, storyboard prompt assembly. No billable fal calls.
  2. plan (nw.plan_render_shot) — pure data: returns a falaw.Plan. Inspect plan.total_cost_usd before executing.
  3. execute (nw.execute_render) — the only phase that talks to fal. Materializes shots/<id>/output.mp4 and records a render-decision in the project graph.
prep = nw.prepare_shot(proj, "shot_01", upload=False)  # for dry-run / cost preview
plan = nw.plan_render_shot(prep, quality="balanced")
print(plan.total_cost_usd, [c.tool for c in plan.calls])

prep = nw.prepare_shot(proj, "shot_01")  # upload=True for real run
output = nw.execute_render(prep, plan, project=proj)

A pluggable strategy registry

Each shot has an open-string render_strategy. nw.renderers ships five built-in strategies and lets apps register their own without touching nw:

name what it does
lipsync character anchor + audio → talking video (omnihuman)
image_to_video env / fresh storyboard still → animated clip
text_to_video prompt-only short clip
still image looped over audio (no video gen)
composite_lipsync character + environment + audio → composite, then talking video
nw.list_strategies()  # ['composite_lipsync', 'image_to_video', ...]
nw.register_strategy("my_app_strategy", MyStrategy())

A storyboard layer

nw.storyboard bridges artful storyboards into an nw.Project. Build one panel per shot, plan the seed-image generation as a falaw.Plan, then execute:

sb, intervals = nw.storyboard_from_shots(proj)
plan, panel_ids = nw.plan_render_panel_images(sb, quality="balanced")
sb = nw.execute_render_panel_images(proj, sb, plan, panel_ids)
nw.save_storyboard(proj, sb, panel_intervals=intervals)

QA reports for renders

nw.inspect answers questions a successful render can't: "did it come out the right length?", "is there a frozen-frame segment?", "are there gaps between shots?":

report = nw.shot_report(proj, "shot_01")
report.duration_within_tolerance  # False if Hailuo returned a short clip
report.has_long_freeze  # True if a ≥1s frozen segment is detected

compose = nw.compose_report(proj)
compose.freeze_alerts  # tuple of suspicious shots
compose.gaps  # gaps between consecutive shots

A provenance graph (and freshness queries)

All sections, shots, refs, and decisions live in a lacing annotation graph with was_derived_from edges. That makes "what's downstream of this change?" a one-line query:

stale = nw.stale_after(proj.root, character_annotation_id)
upstream = nw.derived_from(proj.root, render_annotation_id)
shots = nw.annotations_at_tier(proj.root, "shot")

Pre-graph projects (and muvid fixtures) auto-migrate on first open; the migration is idempotent and writes a sentinel under .nw/.

Sibling experiments

Comparing four interpretations of the same song is a first-class operation, not a shell loop:

nw.clone_project(
    "the_bells",
    "the_bells_v1_lipsync",
    preserve=("song", "lyrics", "characters"),
    reset=("script", "shots", "output", ".nw"),
)

# Apply the same operation across a cohort:
summaries = nw.summarize_all(["the_bells_v1", "the_bells_v2", "the_bells_v3"])
nw.apply_to_projects(roots, lambda p: nw.compose_report(p), parallel=True)

Project layout

my_video/
  project.json                  # project-level metadata (title, song, style)
  project.annot.sqlite          # lacing graph: sections, shots, refs, decisions
  storyboard.annot.sqlite       # storyboard panels (created on save_storyboard)
  song/                         # master audio
  lyrics/                       # lyrics + alignment (alignment.annot)
  characters/<name>/
    card.json                   # card with reference_image_path (the "anchor")
    refs/                       # candidate images
    selected/                   # curator-picked images
  environments/<name>/
    establishing.png            # the environment anchor
  shots/<shot_id>/
    audio.wav                   # the song over [start_s, end_s]
    shot.json                   # mirror of the shot spec
    output.mp4                  # the rendered shot
  output/
    final.mp4                   # composed timeline
  .nw/
    decisions.jsonl             # tail-grep-able decision audit
    migrated_to_graph           # migration sentinel

API at a glance

# Folder facade
nw.Project, nw.Project.init, nw.CharacterImage

# Schema
(
    nw.ProjectSpec,
    nw.ProjectSummary,
    nw.SectionSpec,
    nw.ShotSpec,
)
nw.CharacterRef, nw.EnvironmentRef, nw.SongInfo, nw.SCHEMA_VERSION

# Workflow
nw.prepare_shot, nw.plan_render_shot, nw.execute_render, nw.ShotPreparation

# Strategies
(
    nw.Strategy,
    nw.get_strategy,
    nw.list_strategies,
)
nw.register_strategy, nw.strategies

# Storyboard
(
    nw.open_storyboard,
    nw.save_storyboard,
    nw.storyboard_from_shots,
)
(
    nw.plan_render_panel_images,
    nw.execute_render_panel_images,
)
nw.storyboard_db_path, nw.project_asset_id

# Inspect / QA
(
    nw.shot_report,
    nw.compose_report,
    nw.ShotReport,
    nw.ComposeReport,
)
nw.FrozenSegment, nw.Gap

# Graph / provenance
(
    nw.ProjectGraph,
    nw.derived_from,
    nw.descendants_of,
    nw.stale_after,
)
nw.annotations_at_tier, nw.iter_all_annotations

# Experiments
nw.clone_project, nw.apply_to_projects, nw.summarize_all

# Migration
nw.migrate_to_graph, nw.is_migrated

Design notes

  • SSOT on the folder. Every typed value comes from project.json plus the project graph. Tools never have to invent their own storage.
  • Open render strategies. render_strategy is an open string, not a closed enum, so apps register strategies without modifying nw.
  • Plan-then-execute. Cost is computed and inspectable before the network goes near a credit card. Plans built with upload=False are refused at execute time — they exist for inspection only.
  • Provenance by default. Every render and every curator decision is written to the graph with was_derived_from, so freshness analysis (reelee-style) is a graph walk, not a heuristic.

Dependencies

pydantic, falaw (fal-AI planner), lacing (annotation graph), xdol (registry), artful (storyboard).

Optional system tools: ffmpeg / ffprobe for audio slicing and QA reports.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nw-0.0.16.tar.gz (156.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nw-0.0.16-py3-none-any.whl (99.4 kB view details)

Uploaded Python 3

File details

Details for the file nw-0.0.16.tar.gz.

File metadata

  • Download URL: nw-0.0.16.tar.gz
  • Upload date:
  • Size: 156.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for nw-0.0.16.tar.gz
Algorithm Hash digest
SHA256 f10ec81538a49b909c7f8961af60f0f7a9d75a647e69397dc5940af9890fcf8e
MD5 1bb359f99b47a48955ca61893bccd448
BLAKE2b-256 eb399d90282c1beb95b5f417bf843aedda2aed0743eb57b26dffb34386fa06eb

See more details on using hashes here.

File details

Details for the file nw-0.0.16-py3-none-any.whl.

File metadata

  • Download URL: nw-0.0.16-py3-none-any.whl
  • Upload date:
  • Size: 99.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for nw-0.0.16-py3-none-any.whl
Algorithm Hash digest
SHA256 b4aead9c0675fee6eb7d010e4b7eb83849ad9794007d5f243e13c45c8d91a803
MD5 b8a8e8fded94f2c0a6d38df5f0b2277b
BLAKE2b-256 a9f8967d701537043913214bb042c40e0e090ff7596f7f2a473725fa979571af

See more details on using hashes here.

Release history Release notifications | RSS feed

0.0.39

2 files

0.0.38

2 files

0.0.37

2 files

0.0.36

2 files

0.0.35

2 files

0.0.34

2 files

0.0.33

2 files

0.0.32

2 files

0.0.31

2 files

0.0.30

2 files

0.0.29

2 files

0.0.28

2 files

0.0.27

2 files

0.0.26

2 files

0.0.25

2 files

0.0.24

2 files

0.0.23

2 files

0.0.22

2 files

0.0.21

2 files

0.0.20

2 files

0.0.19

2 files

0.0.18

2 files

0.0.17

2 files

This release

0.0.16 This release

2 files

0.0.15

2 files

0.0.14

2 files

0.0.13

2 files

0.0.12

2 files

0.0.11

2 files

0.0.10

2 files

0.0.9

2 files

0.0.8

2 files

0.0.7

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page