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.14.tar.gz (146.9 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.14-py3-none-any.whl (97.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: nw-0.0.14.tar.gz
  • Upload date:
  • Size: 146.9 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.14.tar.gz
Algorithm Hash digest
SHA256 6605920c3bb342d8a50691ce1964506598672457cd6da300d003efc11df572e2
MD5 2d6d0892f75aea131115c91ac1175f8f
BLAKE2b-256 ba839dba7228502f65d3d0c45a4df9f5d54f1d71a0d56e477efaa9c8061ba61c

See more details on using hashes here.

File details

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

File metadata

  • Download URL: nw-0.0.14-py3-none-any.whl
  • Upload date:
  • Size: 97.8 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.14-py3-none-any.whl
Algorithm Hash digest
SHA256 09263e41c8dbefbca6883f3578e79ece9fee48ffe76db3b55298d3fa077b8037
MD5 3735ac7816422e43c430968c36875e4f
BLAKE2b-256 2dd2cf4abb5ad4cd1b4c537dc164959afa4b172b43c96599ac0a54cf95ac497b

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

This release

0.0.14 This release

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