"""Render strategies — pluggable, plan-producing, **shot-typed**.
**Scope.** A Strategy is the plug-in point of the *shot* render unit
(:mod:`nw.workflow`) — and, through the adapter in
``nw/transforms/_adapters/render_strategy.py``, of the **general engine too**.
That adapter wraps *every* registered strategy at import time, so registering
one here publishes a ``shot_to_render_result.fal.<name>`` Transform for free,
and :class:`nw.genres.Genre` validates a genre's ``strategy_names`` against
this registry. The two registries are one pipeline with two front doors, not
two pipelines — the README says the same thing under "Render strategies".
What a Strategy **cannot** express is a render that is not a video shot: it is
typed to a :class:`nw.workflow.ShotPreparation` in and an ``output.mp4`` out.
So the split is by render *kind*, not by registry:
- a new way to render a **shot** → register a Strategy here, and get the
Transform adaptation for free;
- a new render **kind** — audio weave, slideshow, anything whose input is not a
shot — → register a :class:`nw.transforms.Transform` directly; that registry
is the render-kind-agnostic one.
See nw#9.
A *strategy* knows how to turn a :class:`nw.workflow.ShotPreparation` into:
1. A :class:`falaw.Plan` (pure data — :meth:`Strategy.plan`).
2. A final ``output.mp4`` path, given the Plan's executed Artifacts
(:meth:`Strategy.materialize`).
Strategies are registered with an :class:`xdol.Registry` keyed by name. Apps
can register their own strategies (e.g. ``composite_lipsync``, ``slideshow``,
``panel``) without modifying nw.
Built-in strategies (registered at import):
- ``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-talk
("Thor in a bell tower playing piano, lipsynced")
"""
from __future__ import annotations
from typing import Protocol
from xdol import Registry
from falaw import Plan
from lacing import Artifact
[docs]
class Strategy(Protocol):
"""Render-strategy contract."""
name: str
[docs]
def plan(
self,
prep, # nw.workflow.ShotPreparation — avoid import cycle
*,
quality: str = "balanced",
model_overrides: dict[str, str] | None = None,
) -> Plan:
"""Build a :class:`falaw.Plan` for the prepared shot. No fal calls."""
...
[docs]
def materialize(
self,
prep, # nw.workflow.ShotPreparation
plan: Plan,
artifacts: list[Artifact],
) -> "Path": # noqa: F821
"""Turn executed Artifacts into ``shot_dir/output.mp4``. May download +
run ffmpeg, but no fal calls."""
...
# The strategy registry. ``on_conflict="error"`` so a misconfigured plugin
# can't silently shadow a built-in.
strategies: Registry = Registry(name="nw.renderers", on_conflict="error")
"""Public registry — apps add strategies via ``strategies.register("name", impl)``."""
[docs]
def get_strategy(name: str) -> Strategy:
"""Look up a strategy by name; raises if unknown."""
if name not in strategies:
known = sorted(strategies.keys())
raise KeyError(
f"No render strategy {name!r}; registered: {known}. "
"Apps can register custom strategies via "
"`nw.renderers.strategies.register(name, impl)`."
)
return strategies[name]
[docs]
def list_strategies() -> list[str]:
"""Return all registered strategy names (sorted)."""
return sorted(strategies.keys())
[docs]
def register_strategy(name: str, impl: Strategy) -> Strategy:
"""Register a strategy. Returns ``impl`` so it can be used inline."""
return strategies.register(name, impl)
# --- import the built-ins so they self-register on package import -----------
from . import lipsync as _lipsync # noqa: E402,F401
from . import image_to_video as _image_to_video # noqa: E402,F401
from . import text_to_video as _text_to_video # noqa: E402,F401
from . import still as _still # noqa: E402,F401
from . import composite_lipsync as _composite_lipsync # noqa: E402,F401