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
reprof the underlying exception whenokis 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.
- 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(noline) 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.
applicationandargumentsare the exact tuplecached_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(notNone) on cache hit or unknown estimate so sums are well-defined; useestimated_cost_usdis Noneto check unknown status explicitly.
- cache_status: Literal['hit', 'miss', 'stale', 'unknown']
Whether the cache will short-circuit this call.
"hit"meansexecutewon’t bill, soPlan.total_cost_usdandPlan.cache_hit_savings_usdreflect that.
- estimated_cost_usd: float | None
Predicted cost in USD.
Nonewhen the model has nocost_estimatepopulated (callers can distinguish “free” from “unknown”).
- expected_duration_s: tuple[float, float] | None
(min, max)duration the model can produce, orNoneif no duration contract is known. Plan-level validators can check that the requested duration fits this range and raiseFalDurationOutOfRangebefore 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 fromapplicationbecause 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_callis the simplest: constant per invocation regardless of size.per_secondapplies to video / TTS where output length matters.per_imagecovers batch image gen.per_megapixelcovers high-res image generation.per_tokencovers LLM-style endpoints.- Type:
Literal[‘per_call’, ‘per_image’, ‘per_second’, ‘per_token’, ‘per_megapixel’]
- amount
USD (or
currency) per unit defined bykind.- 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().
- 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_sthat the model’s declaredexpected_duration_rangecannot satisfy. Callers can catch this to split the shot, repeat it, or pick a different model.
- 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_sis parsed from theRetry-Afterheader if present, elseNone(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.
- single_character_recommended: bool
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 + breturns a new Plan witha.callsfollowed byb.calls.Plan(calls=())is the identity. Plans are frozen, so edits return new Plans (usewith_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_usdacross all calls.
- 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_falinvocation share the samecall_id.- Type:
str
- message
Free-form text. For
"log"events this is the log line; for"error"it’srepr(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.
- 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
namefor 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.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 syntheticcache_hitevent 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_eventfor 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 viausing_fal_credentials()is used; when that is also unset, the fal SDK’s ownFAL_KEYenv-var lookup applies (the historical behaviour). A resolved key is used per-call via a dedicatedfal_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
CallPlanfrom acall_plan_to_dict()dict.arguments/metadataare copied (a deserialized plan owns its own data);expected_duration_sis re-tupled.
- falaw.call_plan_to_dict(call: CallPlan) dict[source]
Convert a
CallPlanto a plain JSON-serializable dict.The inverse of
call_plan_from_dict().expected_duration_s(atuple) 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_urlis given, we skip face generation and use that image directly. Otherwise we run text-to-image with the description (+ optionalstylesuffix), 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.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). Passmodel_idto 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_keyargument tocall_fal()wins over this context value, which in turn wins over the fal SDK’s ownFAL_KEYenv-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
Noneif the record has nocost_estimate(so callers can distinguish “free” from “unknown”). The unit picked is whatever matches the record’scost_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_modelsemantics the renderer uses, then sums per-call costs. Returns a structuredCostRollupwith per-line breakdowns, plus a list of “skipped” entries the caller should surface (typically: a model with nocost_estimatepopulated).
- 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_idandurl=None. Useful for exercising downstream composition without an API key.use_cache – When True (default), executes via
cached_call_falso cache hits skip the network. When False, every call is fresh.artifact_converter – Per-CallPlan converter from raw fal response to
lacing.Artifact. WhenNone(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 integerN) is rewritten toartifacts[N].urljust 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.ArtifactperCallPlaninplan.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 theimage_editcategory (Flux Kontext et al.) and threads the references asimage_url(first) +image_urls(all) — the same wire shape image-edit models understand.Pass
model_idto 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"}. Withconcurrency=1results arrive in submission order (shots before beats). Withconcurrency > 1they 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
CallPlanand (optionally) check the cache.When
consult_cache=True(the default), the cache is peeked using the same key the eventual call would produce;cache_statusis set to"hit"if a cached entry exists,"miss"otherwise. This makesPlan.total_cost_usdhonest: a fully-cached Plan reports $0.When
consult_cache=False(e.g. for unit tests or “what would a fresh run cost?” reporting),cache_statusis"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_secondsetc. areNone/ 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 setquality="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
Planfrom aplan_to_dict()dict.Raises
ValueErrorifdcarries an unrecognizedschematag — a plan written by an incompatible future version should fail loudly, not silently lose calls. A missingschemais 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_editcategory (Flux Kontext et al.) and threads the references asimage_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_sis used only for cost estimation when the model is pricedper_second; it does not get passed to fal unless the caller puts it inextra(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-llmwith the exact same application id and argument shape as the eagerfalaw.llm_complete(), so a planned call and an eager call with identical inputs collapse to the same cache entry.output_kindis"text"for a free-form completion or"json"when the prompt asks for a strict-JSON response — it tellsfalaw.execute()what kind oflacing.Artifactto 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-llmper-call estimate (source="approximate"— real pricing is per-token); passmodelto 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.voicesemantics are model-specific.duration_sis 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
Planto 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 aschematag (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.txtandllms-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. Settingwrite=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-basedpick_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 defaultai-avatarwhich 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.
concurrencycontrols how many shots/beats run in parallel against fal. The work is HTTP-bound, so a thread pool is enough. Default1preserves serial behavior. Useiter_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
callbackto 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.
voicesemantics 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
keyas the fal credential for everycall_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
keyinstead of the server’sFAL_KEYenv var — without any intermediate function needing a credential parameter.A falsy
keyis 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.