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:
- prepare (
nw.prepare_shot) — local work: audio slice, anchor resolution, storyboard prompt assembly. No billable fal calls. - plan (
nw.plan_render_shot) — pure data: returns afalaw.Plan. Inspectplan.total_cost_usdbefore executing. - execute (
nw.execute_render) — the only phase that talks to fal. Materializesshots/<id>/output.mp4and 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.jsonplus the project graph. Tools never have to invent their own storage. - Open render strategies.
render_strategyis an open string, not a closed enum, so apps register strategies without modifyingnw. - Plan-then-execute. Cost is computed and inspectable before the
network goes near a credit card. Plans built with
upload=Falseare 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nw-0.0.13.tar.gz.
File metadata
- Download URL: nw-0.0.13.tar.gz
- Upload date:
- Size: 144.6 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e0da1e1456d0291ad00dd3a7d6faa98c3f1c36324c57f3a535bd72250f07251
|
|
| MD5 |
b0a3ea9dfa272ab7c725c01d06b003c3
|
|
| BLAKE2b-256 |
fdbb6088064a8b5e232fe5b5e9f200f5f07cb4222289077322d8773c2f2e4685
|
File details
Details for the file nw-0.0.13-py3-none-any.whl.
File metadata
- Download URL: nw-0.0.13-py3-none-any.whl
- Upload date:
- Size: 96.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3852c7aaba090bf7e529a9fef3a034d3bb9e7113a61d813ff4d6e6bad663c59
|
|
| MD5 |
d58f58c0acc6c3af2da5d017fb81ffd2
|
|
| BLAKE2b-256 |
c258b8fed4ea80c820ea087a69b40d5f3ff3f2d097c92ce566a8a009bb393f90
|