falaw

falaw: agent-friendly Python facade over fal.ai for AI media generation.

Quick start (single-shot generation):

>>> from falaw import generate_image
>>> r = generate_image("a tiger eye, macro, 35mm", quality="fast")
>>> r.first.download(to="./tiger.png")

Directorial workflow (Scene IR + caching):

>>> from falaw import Scene, cast_character, render_scene
>>> sarah = cast_character("Sarah", "mid-30s, dark curly hair")
>>> # ... build a Scene with characters/beats/shots ...
>>> manifest = render_scene(scene)            # caches per-beat
>>> # edit one beat, re-render: only that beat re-fires.

Leave notes for future sessions:

>>> from falaw import journal
>>> journal.note("Sarah's voice clone needs ~10s reference for stability")
class falaw.AccountStatus(ok: bool, locked: bool, unauthorized: bool, status_code: int | None, detail: str, url: str | None, error: str | None)[source]

Outcome of health_check().

detail: str

Server-supplied detail string (best-effort), or ‘’ if unavailable.

error: str | None

repr of the underlying exception when ok is False and the failure isn’t a recognized lock/unauth (e.g. network error).

locked: bool

True iff the response indicated a locked / unverified account.

message_for_user() str[source]

Single human-readable line explaining the status. Stable for logging.

ok: bool

True iff the account responded without an auth/lock error.

status_code: int | None

HTTP status from the probe (None if no HTTP exchange happened).

unauthorized: bool

True iff credentials are missing or invalid.

url: str | None

If actionable (e.g. billing dashboard), URL the user should visit.

class falaw.Asset(url: str, kind: str, content_type: str = '', width: int = 0, height: int = 0, duration_s: float = 0.0, metadata: dict = <factory>)[source]

A single piece of generated media.

Holds the URL plus minimal typed metadata. download materializes it.

download(*, to: str | None = None) str[source]

Download the asset to a file. Returns the local path.

class falaw.Beat(*, id: str, speaker: str = '', line: str = '', action: str = '', emotion: str = '', shot_id: str = '', duration_s: float | None = None, notes: str = '')[source]

The atomic unit of a scene: who, says what, with what intent.

A Beat that has only action (no line) is a non-verbal beat.

class falaw.CallPlan(*, tool: str, application: str, arguments: dict, output_kind: ~typing.Literal['image', 'video', 'audio', 'json', 'text', 'binary'], estimated_cost_usd: float | None = None, cache_status: ~typing.Literal['hit', 'miss', 'stale', 'unknown'] = 'unknown', expected_duration_s: tuple[float, float] | None = None, metadata: dict = <factory>)[source]

A single planned fal call. Pure data — no API contact yet.

application and arguments are the exact tuple cached_call_fal(application, arguments) would take, so a Plan can be cache-checked, executed, or replayed without ambiguity.

application: str

The fal model id that will be invoked (e.g. "fal-ai/flux/dev").

arguments: dict

Keyword arguments to pass to fal. Will be JSON-canonicalized for cache key computation; should be JSON-serializable.

property billable_cost_usd: float

Cost that will actually be billed (0 on cache hit, estimate otherwise).

Returns 0.0 (not None) on cache hit or unknown estimate so sums are well-defined; use estimated_cost_usd is None to check unknown status explicitly.

cache_status: Literal['hit', 'miss', 'stale', 'unknown']

Whether the cache will short-circuit this call. "hit" means execute won’t bill, so Plan.total_cost_usd and Plan.cache_hit_savings_usd reflect that.

estimated_cost_usd: float | None

Predicted cost in USD. None when the model has no cost_estimate populated (callers can distinguish “free” from “unknown”).

expected_duration_s: tuple[float, float] | None

(min, max) duration the model can produce, or None if no duration contract is known. Plan-level validators can check that the requested duration fits this range and raise FalDurationOutOfRange before the call instead of letting it silently truncate.

metadata: dict

Free-form labels for downstream consumers. Conventional keys: shot_id, beat_id, character_name, strategy.

output_kind: Literal['image', 'video', 'audio', 'json', 'text', 'binary']

What kind of Artifact this call will produce.

tool: str

High-level tool name — "generate_image", "image_to_video", etc. Distinct from application because one tool may dispatch to several fal models depending on quality tier.

class falaw.Character(*, name: str, description: str = '', reference_image_url: str = '', voice: Voice | None = None, style_notes: str = '')[source]

A reusable character: stable face, stable voice, stable style.

class falaw.CostEstimate(*, kind: Literal['per_call', 'per_image', 'per_second', 'per_token', 'per_megapixel'], amount: float, currency: str = 'USD', notes: str = '', source: str = 'approximate')[source]

Quantitative cost of one fal call against this model.

kind

How the price scales. per_call is the simplest: constant per invocation regardless of size. per_second applies to video / TTS where output length matters. per_image covers batch image gen. per_megapixel covers high-res image generation. per_token covers LLM-style endpoints.

Type:

Literal[‘per_call’, ‘per_image’, ‘per_second’, ‘per_token’, ‘per_megapixel’]

amount

USD (or currency) per unit defined by kind.

Type:

float

currency

ISO 4217 code. "USD" is the only supported value today; the field exists so we can extend without a schema migration.

Type:

str

notes

Free-form caveats — “rounded up to next second”, etc.

Type:

str

source

How the estimate was obtained — "docs", "empirical", "approximate". Lets us flag stale or unverified entries in audits.

Type:

str

class falaw.CostLine(*, kind: str, item_id: str, model_id: str, amount: float, currency: str, note: str = '')[source]

One line item in a scene rollup.

class falaw.CostRollup(*, total_amount: float, currency: str = 'USD', lines: tuple[CostLine, ...] = (), skipped: tuple[str, ...] = ())[source]

Result of estimate_scene_cost().

by_kind() dict[str, float][source]

Sum per kind for quick inspection.

class falaw.Environment(*, name: str, description: str = '', reference_image_url: str = '', time_of_day: str = '', lighting: str = '')[source]

A reusable location/setting.

exception falaw.FalAccountLocked(message: str, *, status_code: int, detail: str = '', body: Any = None, headers: dict[str, str] | None = None, application: str | None = None, url: str | None = None, cause: BaseException | None = None)[source]

fal account is locked / suspended / awaiting verification.

Typical 403 with a body indicating the account is not in good standing. No amount of retrying or model-switching will fix this — the user has to act (verify email, top up billing, contact support).

exception falaw.FalBadRequest(message: str, *, status_code: int, detail: str = '', body: Any = None, headers: dict[str, str] | None = None, application: str | None = None, url: str | None = None, cause: BaseException | None = None)[source]

Server rejected the request payload — typical 400 / 422.

exception falaw.FalDurationOutOfRange(message: str, *, model_id: str, requested: float, valid_range: tuple[float, float])[source]

The requested duration is outside what the model can produce.

Raised by Plan/Execute when the caller asks for duration_s that the model’s declared expected_duration_range cannot satisfy. Callers can catch this to split the shot, repeat it, or pick a different model.

exception falaw.FalError[source]

Base for all falaw-raised exceptions.

exception falaw.FalHTTPError(message: str, *, status_code: int, detail: str = '', body: Any = None, headers: dict[str, str] | None = None, application: str | None = None, url: str | None = None, cause: BaseException | None = None)[source]

Wraps an HTTP error from fal.ai with the original status, body, and headers.

Subclasses pick out specific status codes / patterns. Use this base when you want to catch any HTTP failure (e.g. for retry).

exception falaw.FalInsufficientFunds(message: str, *, status_code: int, detail: str = '', body: Any = None, headers: dict[str, str] | None = None, application: str | None = None, url: str | None = None, cause: BaseException | None = None)[source]

Account balance is insufficient — typical 402.

exception falaw.FalModelHung(message: str, *, model_id: str, elapsed_s: float)[source]

A model was queued but never returned — distinct from a network timeout.

Raised by higher-level orchestration that sets a per-call wall-clock budget (e.g. “give up on this lipsync after 5 minutes and pick another model”).

exception falaw.FalRateLimited(message: str, *, retry_after_s: float | None = None, **kwargs)[source]

fal is throttling requests — typical 429.

retry_after_s is parsed from the Retry-After header if present, else None (caller decides backoff).

exception falaw.FalServerError(message: str, *, status_code: int, detail: str = '', body: Any = None, headers: dict[str, str] | None = None, application: str | None = None, url: str | None = None, cause: BaseException | None = None)[source]

fal-side server error — typical 5xx. Generally retryable.

exception falaw.FalTimeout(message: str, *, elapsed_s: float, application: str | None = None)[source]

The fal call timed out before producing a result.

exception falaw.FalUnauthorized(message: str, *, status_code: int, detail: str = '', body: Any = None, headers: dict[str, str] | None = None, application: str | None = None, url: str | None = None, cause: BaseException | None = None)[source]

Missing or invalid API credentials — typical 401.

class falaw.ModelRecord(*, id: str, category: str, description: str = '', aliases: tuple[str, ...] = (), quality_tier: str = '', cost_hint: str = '', cost_estimate: CostEstimate | None = None, docs_url: str = '', max_clip_seconds: float | None = None, single_character_recommended: bool = False, supported_resolutions: tuple[str, ...] = (), default_negative_prompt: str = '')[source]

One entry in the fal model catalog.

default_negative_prompt: str

Quality/realism negatives worth appending by default (e.g. to avoid the plastic-skin look). Empty when none.

max_clip_seconds: float | None

Practical max length of a single generated clip, in seconds (e.g. ~10 for Seedance). Drives the “this shot is too long, split it” warning.

True when the model handles a single character per shot far better than multiple interacting ones — drives the “two characters, consider shot/reverse-shot” warning.

supported_resolutions: tuple[str, ...]

Resolutions the model offers, cheap→expensive (e.g. (“720p”, “1080p”)).

class falaw.Plan(calls: tuple[CallPlan, ...] = ())[source]

An ordered sequence of CallPlan — a render plan, in essence.

Plans compose: a + b returns a new Plan with a.calls followed by b.calls. Plan(calls=()) is the identity. Plans are frozen, so edits return new Plans (use with_call_replaced() for in-place-feel).

property cache_hit_savings_usd: float

USD that would have been spent without the cache.

Equal to sum(c.estimated_cost_usd for c in calls if c.cache_status == "hit" and c.estimated_cost_usd is not None).

property has_unknown_costs: bool

True if any non-cache-hit call has no cost estimate.

Use this to refuse to gate on a budget when the estimate is incomplete.

property total_cost_usd: float

Sum of CallPlan.billable_cost_usd across all calls.

with_call_replaced(index: int, new_call: CallPlan) Plan[source]

Return a new Plan with calls[index] replaced.

class falaw.ProgressEvent(*, kind: Literal['queued', 'progress', 'log', 'done', 'error', 'cache_hit'], application: str, call_id: str, message: str = '', pct: float | None = None, elapsed_s: float = 0.0)[source]

One step in the lifecycle of a fal call.

kind

Lifecycle stage. See EventKind.

Type:

Literal[‘queued’, ‘progress’, ‘log’, ‘done’, ‘error’, ‘cache_hit’]

application

fal model id (e.g. "fal-ai/flux/dev").

Type:

str

call_id

A short hex string that uniquely identifies the call. All events for one call_fal invocation share the same call_id.

Type:

str

message

Free-form text. For "log" events this is the log line; for "error" it’s repr(exc); otherwise empty.

Type:

str

pct

Optional progress percentage in [0.0, 100.0]. fal’s current API doesn’t surface this; included for forward compatibility.

Type:

float | None

elapsed_s

Seconds since the call started.

Type:

float

class falaw.Result(assets: list[Asset] = <factory>, raw: dict = <factory>, application: str = '', arguments: dict = <factory>)[source]

A fal call result with parsed assets and the original raw response.

The raw response is kept so callers can inspect provider-specific fields (timings, seed, has_nsfw_concepts, …) that we do not normalize.

class falaw.Scene(*, title: str, style: str = '', characters: tuple[Character, ...] = (), environments: tuple[Environment, ...] = (), shots: tuple[Shot, ...] = (), beats: tuple[Beat, ...] = (), notes: str = '')[source]

The whole editable structure: cast, locations, shots, beats.

with_beat(beat: Beat) Scene[source]

Return a new Scene with beat replacing any existing beat with the same id.

class falaw.Session(output_dir: str = <factory>, journal: Journal = <factory>, history: list[Result] = <factory>)[source]

A working session for a sequence of falaw operations.

>>> import tempfile
>>> s = Session(output_dir=tempfile.mkdtemp())
>>> s.history
[]
class falaw.Shot(*, id: str, description: str = '', framing: str = 'medium', environment: str = '', characters: tuple[str, ...] = (), camera: str = '', notes: str = '')[source]

A visual frame: framing + environment + characters in view.

Beats anchor to a Shot via shot_id. The Shot itself has its own rendered output (still or short clip used as the anchor for beat lipsync renders).

class falaw.ToolSpec(*, name: str, description: str, func: ~typing.Callable[[...], ~typing.Any], input_schema: ~typing.Mapping[str, ~typing.Any] = <factory>, output_schema: ~typing.Mapping[str, ~typing.Any] = <factory>, tags: tuple[str, ...] = (), examples: tuple[~typing.Mapping[str, ~typing.Any], ...] = (), version: str = '0.0.1')[source]

Single source of truth for a tool exposed by falaw.

A ToolSpec is what the registry stores. Bridges read it to produce Claude-skill instructions, MCP tool descriptors, HTTP endpoints, etc.

class falaw.Voice(*, name: str, voice_id: str = '', reference_audio_url: str = '', model_id: str = '', style_notes: str = '')[source]

A character’s voice spec.

Three modes, choose any: * voice_id — model-side voice id (e.g. ElevenLabs voice). * reference_audio_url — a few seconds of audio to clone. * model_id — override the default TTS model for this voice.

Always provide name for stable, human-readable referencing.

falaw.animate_face(image_url: str, audio_url: str, *, prompt: str = '', quality: str = 'balanced', model_id: str | None = None, extra: dict | None = None) Result[source]

Animate a still face from audio. Image + audio → talking video.

falaw.apply_note_to_beat(beat: Beat, note: str, *, model: str = 'anthropic/claude-sonnet-4.5') Beat[source]

Use the LLM to apply a directorial note to a Beat.

falaw.apply_note_to_scene(scene: Scene, note: str, *, model: str = 'anthropic/claude-sonnet-4.5') Scene[source]

Apply a cross-cutting note: LLM proposes per-beat edits, we apply them.

falaw.beat_content_hash(beat: Beat, *, character: Character | None = None) str[source]

Hash everything that affects how the beat renders.

Includes the beat’s content + the character’s identity anchors (face image, voice spec). Style/emotion changes invalidate the cache; pure id renames do not.

falaw.cache_get(application: str, arguments: Mapping[str, Any]) dict | None[source]

Return the raw fal response if cached, else None.

falaw.cache_put(application: str, arguments: Mapping[str, Any], raw: dict, *, note: str = '') str[source]

Persist a fal response. Returns the entry directory path.

falaw.cache_stats() dict[source]

Quick summary of the cache: entry count and disk usage.

falaw.cached_call_fal(application: str, arguments: Mapping[str, Any], *, refresh: bool = False, on_event=None) dict[source]

Call a fal model, but reuse the cached response when present.

Parameters:
  • application – fal model id.

  • arguments – model input dict.

  • refresh – if True, bypass the cache and overwrite it with a fresh result.

  • on_event – Per-call subscriber for falaw.events.ProgressEvent. On a cache hit, a synthetic cache_hit event is emitted so UIs can show “skipped” instead of “running”.

Returns:

Raw fal response (whether from cache or network).

falaw.call_fal(application: str, arguments: Mapping[str, Any], *, on_log: Callable[[str], None] | None = None, on_event: Callable[[ProgressEvent], None] | None = None, with_logs: bool = True, journal_errors: bool = True, api_key: str | None = None) dict[source]

Call a fal model via fal_client.subscribe.

Parameters:
  • application – fal model id (e.g. "fal-ai/flux/dev").

  • arguments – Input arguments. Keys depend on the model.

  • on_log – Legacy log callback — receives raw log lines as strings. Defaults to no-op (use on_event for structured access).

  • on_event – Per-call subscriber for ProgressEvent`s. Fires in addition to the global subscribers registered via :func:`falaw.events.subscribe.

  • with_logs – Pass through to fal_client; when True the model streams logs.

  • journal_errors – When True, exceptions are recorded as journal issues before being re-raised. The journal entry includes the application id and arguments so future agents can recognize the same trap.

  • api_key – Explicit fal key for this call. When None (default) the key bound via using_fal_credentials() is used; when that is also unset, the fal SDK’s own FAL_KEY env-var lookup applies (the historical behaviour). A resolved key is used per-call via a dedicated fal_client.SyncClient — it is never written to a global or an env var.

Returns:

The raw response dict from the model.

falaw.call_plan_from_dict(d: dict) CallPlan[source]

Rebuild a CallPlan from a call_plan_to_dict() dict.

arguments / metadata are copied (a deserialized plan owns its own data); expected_duration_s is re-tupled.

falaw.call_plan_to_dict(call: CallPlan) dict[source]

Convert a CallPlan to a plain JSON-serializable dict.

The inverse of call_plan_from_dict(). expected_duration_s (a tuple) becomes a 2-element list since JSON has no tuple type; everything else is already JSON-native.

falaw.cast_character(name: str, description: str, *, image_url: str = '', style: str = '', quality: str = 'high', voice_id: str = '', reference_audio_url: str = '', voice_style: str = '') Character[source]

Create a Character with a canonical face (and optional voice).

If image_url is given, we skip face generation and use that image directly. Otherwise we run text-to-image with the description (+ optional style suffix), cache the result, and use the URL.

falaw.cast_voice(character: Character, *, voice_id: str = '', reference_audio_url: str = '', style_notes: str = '', model_id: str = '') Character[source]

Attach or update the Voice on a Character.

falaw.clear_subscribers() None[source]

Drop all registered subscribers. Mostly for tests.

falaw.composite_character_in_environment(character_image_url: str, environment_image_url: str, *, prompt: str = '', quality: str = 'balanced', model_id: str | None = None, extra: dict | None = None) Result[source]

Place a character into an environment as one composited still.

The single most user-visible primitive missing from muvid (per interface_design_plan item E). With this, an agent can produce “Thor in a bell tower” — the character anchor for a downstream omnihuman lipsync — without manually compositing in an image editor.

Defaults to fal-ai/flux-kontext/dev (image_edit category at balanced/high tier). Pass model_id to override (e.g. "fal-ai/flux-pro/kontext/max" for highest quality, or "fal-ai/bytedance/seededit/v3/edit-image" for SeedEdit).

falaw.current_fal_key() str | None[source]

The fal API key bound for the current context, or None.

Resolution order callers should mirror: an explicit api_key argument to call_fal() wins over this context value, which in turn wins over the fal SDK’s own FAL_KEY env-var lookup.

falaw.edit_image(image_url: str, prompt: str, *, quality: str = 'balanced', model_id: str | None = None, extra: dict | None = None) Result[source]

Edit an image with a natural-language instruction.

falaw.establish_environment(name: str, description: str, *, time_of_day: str = '', lighting: str = '', image_url: str = '', quality: str = 'high') Environment[source]

Create an Environment with a canonical establishing image.

falaw.estimate_call_cost(record: ModelRecord, *, count: int = 1, seconds: float | None = None, megapixels: float | None = None, tokens: int | None = None) float | None[source]

Cost of one fal call against record.

Returns None if the record has no cost_estimate (so callers can distinguish “free” from “unknown”). The unit picked is whatever matches the record’s cost_estimate.kind; mismatched units fall back to the closest-fit default.

falaw.estimate_scene_cost(scene: Scene, *, tts_quality: str = 'balanced', lipsync_quality: str = 'high', shot_quality: str = 'balanced', shots_as_video: bool = False) CostRollup[source]

Estimate the USD cost of a full render_scene() invocation.

Walks every shot + beat with the same pick_model semantics the renderer uses, then sums per-call costs. Returns a structured CostRollup with per-line breakdowns, plus a list of “skipped” entries the caller should surface (typically: a model with no cost_estimate populated).

falaw.execute_plan(plan: Plan, *, on_event: Callable | None = None, dry_run: bool = False, use_cache: bool = True, artifact_converter: ResultToArtifact | None = None) list

Execute a Plan, returning a list of materialized :class:`lacing.Artifact`s.

Parameters:
  • plan – The Plan to execute.

  • on_event – Optional per-call event subscriber (passed to call_fal).

  • dry_run – When True, no fal calls are made; synthetic Artifacts are returned with placeholder asset_id and url=None. Useful for exercising downstream composition without an API key.

  • use_cache – When True (default), executes via cached_call_fal so cache hits skip the network. When False, every call is fresh.

  • artifact_converter – Per-CallPlan converter from raw fal response to lacing.Artifact. When None (default), a built-in converter handles the common shapes ({images: [{url}]}, {video: {url}}, {audio: {url}}).

Placeholder resolution

Any string argument equal to "<from N>" (for an integer N) is rewritten to artifacts[N].url just before the call is made — so a multi-step plan (e.g. generate_image → image_to_video) can reference the upstream output without the planner needing to know its URL. The rewrite happens after the upstream call has executed; planning itself is unaffected.

returns:

One lacing.Artifact per CallPlan in plan.calls, in the same order.

falaw.extract_models_from_corpus(path: str) Iterator[dict][source]

Yield ModelRecord-shaped dicts parsed from llms-full.txt.

falaw.generate_image(prompt: str, *, quality: str = 'balanced', image_size: str = 'landscape_4_3', model_id: str | None = None, extra: dict | None = None) Result[source]

Generate an image from a text prompt.

falaw.generate_image_with_refs(prompt: str, reference_image_urls: list[str], *, quality: str = 'balanced', model_id: str | None = None, extra: dict | None = None) Result[source]

Generate a new image conditioned on one or more reference images.

The missing twin of generate_image(): text-to-image models silently ignore reference images, so callers wanting a recurring subject to stay consistent (a character’s face across storyboard panels) need a model that actually ingests references. This routes to the image_edit category (Flux Kontext et al.) and threads the references as image_url (first) + image_urls (all) — the same wire shape image-edit models understand.

Pass model_id to override the picked model.

falaw.image_to_video(image_url: str, prompt: str = '', *, quality: str = 'high', model_id: str | None = None, extra: dict | None = None) Result[source]

Animate a still image into a video.

falaw.iter_render_scene(scene: Scene, *, tts_quality: str = 'balanced', lipsync_quality: str = 'high', shot_quality: str = 'balanced', shots_as_video: bool = False, force: bool = False, concurrency: int = 1)[source]

Yield (kind, result) pairs as each shot/beat finishes.

kind{"shot", "beat"}. With concurrency=1 results arrive in submission order (shots before beats). With concurrency > 1 they arrive in completion order (use the "shot_id" / "beat_id" keys to re-key by identity).

Cache hits are immediate: a fully-cached scene yields all results in close succession even at concurrency=1.

falaw.lipsync(video_url: str, audio_url: str, *, quality: str = 'high', model_id: str | None = None, extra: dict | None = None) Result[source]

Re-sync mouth motion in an existing video to a new audio track.

falaw.llm_complete(prompt: str, *, system: str = '', model: str = 'anthropic/claude-sonnet-4.5', temperature: float = 0.7, extra: dict | None = None) str[source]

Single-shot LLM completion. Returns the assistant text.

falaw.make_call_plan(*, tool: str, application: str, arguments: dict, output_kind: Literal['image', 'video', 'audio', 'json', 'text', 'binary'], estimated_cost_usd: float | None = None, expected_duration_s: tuple[float, float] | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Build a CallPlan and (optionally) check the cache.

When consult_cache=True (the default), the cache is peeked using the same key the eventual call would produce; cache_status is set to "hit" if a cached entry exists, "miss" otherwise. This makes Plan.total_cost_usd honest: a fully-cached Plan reports $0.

When consult_cache=False (e.g. for unit tests or “what would a fresh run cost?” reporting), cache_status is "unknown".

falaw.materialize_asset(url: str, *, key_hint: str = '') str[source]

Download a remote asset to the cache and return the local path.

The local filename is content-addressed by the URL, so calling this twice for the same URL is free.

falaw.model_constraints(id: str) dict[source]

The capability/limit fields for a model — the “static reminder of limitations” a shot-list builder surfaces. Resolves aliases.

Returns a JSON-able dict; max_clip_seconds etc. are None / empty when unknown for that model.

falaw.parse_response(raw: dict, *, application: str, arguments: dict) Result[source]

Best-effort parser over the common fal response shapes.

fal models return a variety of layouts (lists, single objects, bare URLs). We normalize each into Asset(url, kind, …). Unknown shapes pass through as raw only — callers can read result.raw for anything we miss.

falaw.parse_screenplay(text: str, *, title: str = '', style: str = '', model: str = 'anthropic/claude-sonnet-4.5') Scene[source]

Convert prose screenplay text into a Scene IR via an LLM call.

falaw.pick_model(*, category: str, quality_tier: str = 'balanced') ModelRecord[source]

Pick a sensible fal model for a (category, quality) request.

First-match semantics: when several models share a tier, the earlier entry wins. Curated entries are written first in data/models.json, so they take precedence over corpus-merged additions. If no model has the exact tier, neighboring tiers are tried. KeyError only when the category is empty.

falaw.plan_animate_face(image_url: str, audio_url: str, *, prompt: str = '', quality: str = 'balanced', model_id: str | None = None, duration_s: float | None = None, extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.animate_face() call (image + audio → talking video).

Note: the default avatar model is known to hang. For production-grade behavior, callers should pass model_id="fal-ai/bytedance/omnihuman/v1.5" or set quality="high" (which picks omnihuman).

falaw.plan_composite_character_in_environment(character_image_url: str, environment_image_url: str, *, prompt: str = '', quality: str = 'balanced', model_id: str | None = None, extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.composite_character_in_environment() call.

The character image anchors identity; the environment image anchors location, lighting, palette. The default model is Flux Kontext dev.

falaw.plan_edit_image(image_url: str, prompt: str, *, quality: str = 'balanced', model_id: str | None = None, extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.edit_image() call (Flux Kontext / SeedEdit / OmniGen).

falaw.plan_from_dict(d: dict) Plan[source]

Rebuild a Plan from a plan_to_dict() dict.

Raises ValueError if d carries an unrecognized schema tag — a plan written by an incompatible future version should fail loudly, not silently lose calls. A missing schema is tolerated (treated as v1) so hand-written plans stay easy.

falaw.plan_generate_image(prompt: str, *, quality: str = 'balanced', image_size: str = 'landscape_4_3', model_id: str | None = None, extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.generate_image() call without executing it.

falaw.plan_generate_image_with_refs(prompt: str, reference_image_urls: list[str], *, quality: str = 'balanced', model_id: str | None = None, extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.generate_image_with_refs() call.

The planning sibling of the eager op: text-to-image models ignore reference images, so a caller wanting a recurring subject to stay consistent must route to a reference-capable model. This picks the image_edit category (Flux Kontext et al.) and threads the references as image_url (first) + image_urls (all), the same wire shape the eager op uses — so a planned and an eager call with identical inputs collapse to one cache entry.

falaw.plan_image_to_video(image_url: str, prompt: str = '', *, quality: str = 'high', model_id: str | None = None, duration_s: float | None = None, extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.image_to_video() call.

duration_s is used only for cost estimation when the model is priced per_second; it does not get passed to fal unless the caller puts it in extra (different models have different argument names).

falaw.plan_lipsync(video_url: str, audio_url: str, *, quality: str = 'high', model_id: str | None = None, duration_s: float | None = None, extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.lipsync() call (existing video + new audio → re-synced video).

falaw.plan_llm_complete(prompt: str, *, system: str = '', model: str | None = None, temperature: float = 0.7, output_kind: str = 'text', extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.llm_complete() call without executing it.

Routes through fal-ai/any-llm with the exact same application id and argument shape as the eager falaw.llm_complete(), so a planned call and an eager call with identical inputs collapse to the same cache entry.

output_kind is "text" for a free-form completion or "json" when the prompt asks for a strict-JSON response — it tells falaw.execute() what kind of lacing.Artifact to materialize. Either way the LLM response is materialized to a content-addressed cache file (Artifact.path), because LLM output is text, not a URL.

Cost is the fal-ai/any-llm per-call estimate (source="approximate" — real pricing is per-token); pass model to pick the underlying model.

falaw.plan_text_to_speech(text: str, *, quality: str = 'balanced', voice: str | None = None, model_id: str | None = None, duration_s: float | None = None, extra: dict | None = None, metadata: dict | None = None, consult_cache: bool = True) CallPlan[source]

Plan a falaw.text_to_speech() call (text → audio Artifact).

Mirrors the eager falaw.text_to_speech() signature so a planned call and an eager call with identical inputs collapse to the same cache entry. voice semantics are model-specific.

duration_s is an optional hint used only by the cost estimator — the produced audio’s actual duration comes back on the materialized Artifact.

falaw.plan_to_dict(plan: Plan) dict[source]

Convert a Plan to a plain JSON-serializable dict.

The result round-trips through plan_from_dict(). This is the substrate primitive a consumer (a persistence layer, an MCP transport, a plan-diff tool) builds on — falaw owns the wire shape of its own Plan so every consumer agrees on it. Carries a schema tag (PLAN_DICT_SCHEMA) so a future breaking change is detectable.

falaw.refresh_full_docs(*, docs_dir: str | None = None, max_workers: int = 16, force: bool = False, journal: bool = True) dict[source]

Re-crawl per-page docs and rebuild fal_ai_docs_full.md.

falaw.refresh_llms(*, docs_dir: str | None = None, journal: bool = True) dict[source]

Refresh llms.txt and llms-full.txt; return a summary dict.

falaw.refresh_models_from_corpus(*, path: str | None = None, write: bool = False) dict[source]

Merge corpus-discovered models into models.json (additive).

Returns {added, total, from_corpus, write} summary. Setting write=False (the default) reports what would change without touching the file.

falaw.refresh_state() dict[source]

Return the saved per-source refresh state (etags, last fetch times).

falaw.register_tool(**spec_kwargs) Callable[source]

Decorator: register the wrapped function as a falaw tool.

>>> @register_tool(name='echo', description='echo back', tags=('demo',))
... def _echo(x): return x
>>> get_tool('echo').name
'echo'
falaw.remove_background(image_url: str, *, quality: str = 'high', model_id: str | None = None, extra: dict | None = None) Result[source]

Remove the background from an image.

falaw.render_beat(beat: Beat, character: Character, *, tts_quality: str = 'balanced', lipsync_quality: str = 'high', tts_model_id: str | None = None, avatar_model_id: str | None = None, force: bool = False) dict[source]

Render one Beat to a lipsynced video. Returns a small manifest dict.

Parameters:
  • tts_model_id – Override the TTS model. When provided, takes precedence over the character’s voice.model_id and over tts_quality-based pick_model. Use this to force a specific TTS engine for one beat (e.g. eleven-v3 for emotional delivery, multilingual-v2 for consistency).

  • avatar_model_id – Override the avatar/lipsync model (e.g. "fal-ai/bytedance/omnihuman/v1.5" to bypass the default ai-avatar which is known to hang).

falaw.render_scene(scene: Scene, *, tts_quality: str = 'balanced', lipsync_quality: str = 'high', shot_quality: str = 'balanced', shots_as_video: bool = False, force: bool = False, concurrency: int = 1) dict[source]

Render every shot and beat. Returns a manifest dict.

concurrency controls how many shots/beats run in parallel against fal. The work is HTTP-bound, so a thread pool is enough. Default 1 preserves serial behavior. Use iter_render_scene() instead if you want results yielded as each unit completes (for live UI updates).

falaw.render_shot(shot: Shot, *, environment: Environment | None = None, characters: tuple = (), style: str = '', as_video: bool = False, quality: str = 'balanced', image_model_id: str | None = None, image_to_video_model_id: str | None = None, force: bool = False) dict[source]

Render a Shot as a still (default) or a short clip.

Parameters:
  • image_model_id – Override the image-gen model used for the storyboard still (defaults to pick_model(category="image", …)).

  • image_to_video_model_id – Override the image-to-video model used when as_video=True (e.g. "fal-ai/minimax/hailuo-02/pro/image-to-video").

falaw.scene_from_dict(d: Mapping[str, Any]) Scene[source]

Inverse of asdict: reconstruct a Scene from a plain dict.

falaw.storyboard_shot(shot: Shot, *, environment: Environment | None = None, characters: tuple = (), style: str = '', quality: str = 'balanced') Result[source]

Render a storyboard still for a Shot.

falaw.subscribe(callback: Callable[[ProgressEvent], None]) Callable[[ProgressEvent], None][source]

Register callback to receive every emitted ProgressEvent.

Returns the callback unchanged so it can be used as a decorator:

@subscribe
def log_to_file(ev: ProgressEvent) -> None:
    ...
falaw.talking_avatar_from_text(text: str, image_url: str, *, voice: str | None = None, prompt: str = '', tts_quality: str = 'balanced', avatar_quality: str = 'balanced') Result[source]

text + face image → talking video. Two fal calls, one Result.

falaw.text_to_speech(text: str, *, quality: str = 'balanced', voice: str | None = None, model_id: str | None = None, extra: dict | None = None) Result[source]

Synthesize speech. voice semantics are model-specific.

falaw.text_to_video(prompt: str, *, quality: str = 'high', model_id: str | None = None, extra: dict | None = None) Result[source]

Generate a video from a text prompt.

falaw.unsubscribe(callback: Callable[[ProgressEvent], None]) None[source]

Remove a previously subscribe()’d callback. No-op if absent.

falaw.upscale_image(image_url: str, *, scale: float = 2.0, model_id: str | None = None, extra: dict | None = None) Result[source]

Upscale an image.

falaw.using_fal_credentials(key: str | None) Iterator[None][source]

Bind key as the fal credential for every call_fal() in this context.

Intended for server-side bring-your-own-key flows: wrap a unit of work that will make one or more fal calls, and they all authenticate with key instead of the server’s FAL_KEY env var — without any intermediate function needing a credential parameter.

A falsy key is a deliberate no-op (the context is left untouched), so a caller can pass an optional header value straight through without special-casing “no BYO key — fall back to the server/env key”.

Thread/async safe: backed by a contextvars.ContextVar, so the binding is visible only within the entering context (and threads/tasks it spawns), never to concurrent requests.

falaw.video_model_constraints() list[dict][source]

model_constraints for every video model in the catalog — the data a shot-list builder shows as its model-limits reference.

falaw.voice_clone(reference_audio_url: str, text: str, *, model_id: str | None = None, extra: dict | None = None) Result[source]

Generate speech in a cloned voice.