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.8.tar.gz (134.0 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.8-py3-none-any.whl (89.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: nw-0.0.8.tar.gz
  • Upload date:
  • Size: 134.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","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.8.tar.gz
Algorithm Hash digest
SHA256 68d6039824435b7d77f5f79b89d52ca9132f0455fcdfd7b656980189e903c8ef
MD5 bc71108c394530476b78164197ea0307
BLAKE2b-256 9f3f2b8e44de110aaefa9650fc74c31126fa8c4e79492a47fd7e315c9e6610b5

See more details on using hashes here.

File details

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

File metadata

  • Download URL: nw-0.0.8-py3-none-any.whl
  • Upload date:
  • Size: 89.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","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.8-py3-none-any.whl
Algorithm Hash digest
SHA256 06ce218203fe1e4202f1b6a51c7e2cf1b649d53d56209af5f5446bcad3806ba5
MD5 aaf3c2aecbd5e7f14480ea848658a498
BLAKE2b-256 83a2cbc015425689949cfa2a3ec015cf57999f51ca9c98c1039d84c88ae78b59

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

0.0.16

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

This release

0.0.8 This release

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