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")

Functions

get_llm_price(provider, model, *[, prices])

The row for model (exact id), or None when the table does not list it.

list_llm_prices([provider, prices])

Every row, or the rows of one provider.

llm_call_cost_usd(provider, model, usage, *)

(USD, known) for one first-party call from the vendor's reported usage.

llm_prices_table_version()

The table's version, a digest of its bytes (see falaw.base.data_table_version()).

call_plan_from_dict(d)

Rebuild a CallPlan from a call_plan_to_dict() dict.

call_plan_to_dict(call)

Convert a CallPlan to a plain JSON-serializable dict.

plan_dependencies(plan)

Per-call set of the call indices it references via "<from N>".

plan_from_dict(d)

Rebuild a Plan from a plan_to_dict() dict.

plan_hash(plan)

Stable, plan-scoped structural idempotency key for a whole Plan.

plan_to_dict(plan)

Convert a Plan to a plain JSON-serializable dict.

catalogue_cost_basis(model_id, **quantities)

A falaw.CostBasis for a call priced from models.json.

cost_basis_from_dict(d)

Rebuild a CostBasis from a cost_basis_to_dict() dict.

cost_basis_to_dict(basis)

Convert a CostBasis to a plain JSON-serializable dict.

llm_cost_basis(routed_model, *[, ...])

A falaw.CostBasis for a routed LLM call priced from the rate table.

reprice_plan(plan, *[, pricers])

Re-quote every call in plan against today's rate tables.

clear_subscribers()

Drop all registered subscribers.

estimate_call_cost(record, *[, count, ...])

Cost of one fal call against record.

estimate_scene_cost(scene, *[, tts_quality, ...])

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

get_llm_rate(model, *[, rates])

The row for model, or None when the table does not price it.

list_llm_rates(*[, rates])

Every row in the table, in file order — the catalogue view.

llm_ceiling_usd(model, *[, input_tokens, ...])

Ceiling cost in USD of count calls routing model, or None.

subscribe(callback)

Register callback to receive every emitted ProgressEvent.

unsubscribe(callback)

Remove a previously subscribe()'d callback.

animate_face(image_url, audio_url, *[, ...])

Animate a still face from audio.

apply_note_to_beat(beat, note, *[, model])

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

apply_note_to_scene(scene, note, *[, model])

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

beat_content_hash(beat, *[, character])

Hash everything that affects how the beat renders.

cache_get(application, arguments, *[, ...])

Return the raw fal response if cached, else None.

cache_put(application, arguments, raw, *[, ...])

Persist a fal response.

cache_stats()

Quick summary of the cache: entry count, disk usage, and where it went.

cache_usage()

Disk usage of the falaw cache, broken down by area.

prune_assets(*[, older_than, max_bytes, ...])

Reclaim materialized asset copies — the cheapest disk in the cache (falaw#22).

prune_content(*[, older_than, max_bytes, ...])

Reclaim content-addressed blobs — the gigabytes (falaw#22).

prune_manifests(*[, older_than, max_bytes, ...])

Reclaim cache entries — the fal responses themselves (falaw#22).

drop_cache_entry(application, arguments, *)

Delete the cache entry for (application, arguments).

cached_call_fal(application, arguments, *[, ...])

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

content_ref_for_url(url, *[, store, ...])

Materialize url's bytes into store and return their content hash.

default_content_store()

The falaw-cache-rooted lacing.ArtifactStore.

default_url_fetcher()

The transport used when no fetcher= argument is given.

using_url_fetcher(fetcher)

Make fetcher falaw's default asset transport for the duration.

call_fal(application, arguments, *[, ...])

Call a fal model via fal_client.subscribe.

cast_character(name, description, *[, ...])

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

current_fal_key()

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

cast_voice(character, *[, voice_id, ...])

Attach or update the Voice on a Character.

composite_character_in_environment(...[, ...])

Place a character into an environment as one composited still.

edit_image(image_url, prompt, *[, quality, ...])

Edit an image with a natural-language instruction.

establish_environment(name, description, *)

Create an Environment with a canonical establishing image.

execute_plan(plan, *[, on_event, dry_run, ...])

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

execute_plan_isolated(plan, *[, on_event, ...])

Execute a Plan with per-call failure isolation, returning a report.

extract_models_from_corpus(path)

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

generate_image(prompt, *[, quality, ...])

Generate an image from a text prompt.

generate_image_with_refs(prompt, ...[, ...])

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

get_model(id)

get_tool(name)

image_to_video(image_url[, prompt, quality, ...])

Animate a still image into a video.

iter_render_scene(scene, *[, tts_quality, ...])

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

lipsync(video_url, audio_url, *[, quality, ...])

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

list_models(*[, category, quality_tier])

list_tools(*[, tag])

llm_complete(prompt, *[, system, model, ...])

Single-shot LLM completion.

llm_complete_with_receipt(prompt, *[, ...])

Single-shot LLM completion that also reports what it cost.

load_scene(path)

make_beat(speaker[, line, action, emotion, ...])

make_call_plan(*, tool, application, ...[, ...])

Build a CallPlan and (optionally) check the cache.

make_shot(description, *[, framing, ...])

materialize_asset(url, *[, key_hint, store, ...])

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

parse_response(raw, *, application, arguments)

Best-effort parser over the common fal response shapes.

parse_screenplay(text, *[, title, style, model])

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

pick_model(*, category[, quality_tier])

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

model_constraints(id)

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

model_params(id)

Per-model parameters worth knowing before a call, {name: spec}.

video_model_constraints()

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

plan_animate_face(image_url, audio_url, *[, ...])

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

plan_composite_character_in_environment(...)

Plan a falaw.composite_character_in_environment() call.

plan_edit_image(image_url, prompt, *[, ...])

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

plan_generate_image(prompt, *[, quality, ...])

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

plan_generate_image_with_refs(prompt, ...[, ...])

Plan a falaw.generate_image_with_refs() call.

plan_image_to_video(image_url[, prompt, ...])

Plan a falaw.image_to_video() call.

plan_lipsync(video_url, audio_url, *[, ...])

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

plan_llm_complete(prompt, *[, system, ...])

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

plan_generate_audio(prompt, *[, kind, ...])

Plan a falaw.generate_audio() call (prompt → ambient/SFX/music).

plan_text_to_speech(text, *[, quality, ...])

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

refresh_full_docs(*[, docs_dir, ...])

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

refresh_llms(*[, docs_dir, journal])

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

refresh_models_from_corpus(*[, path, write])

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

refresh_model_prices(*[, write, api_key, ...])

Refresh models.json cost estimates from fal's pricing API.

check_model_liveness([ids, status_of])

{endpoint_id: is_live} for ids (default: every catalogued model).

dead_models([ids, status_of])

The ids from check_model_liveness() that are no longer deployed.

fetch_model_prices(endpoint_ids, *[, ...])

{endpoint_id: {"unit_price", "unit", "currency"}} from fal's API.

refresh_llm_rates(*[, write, ...])

Fetch both sources, propose a fresh table, diff it — never overwrite.

refresh_state()

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

register_tool(**spec_kwargs)

Decorator: register the wrapped function as a falaw tool.

remove_background(image_url, *[, quality, ...])

Remove the background from an image.

render_beat(beat, character, *[, ...])

Render one Beat to a lipsynced video.

render_scene(scene, *[, tts_quality, ...])

Render every shot and beat.

render_shot(shot, *[, environment, ...])

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

save_scene(scene, path)

scene_from_dict(d)

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

scene_to_dict(scene)

shot_content_hash(shot, *[, environment])

storyboard_shot(shot, *[, environment, ...])

Render a storyboard still for a Shot.

talking_avatar_from_text(text, image_url, *)

text + face image → talking video.

generate_audio(prompt, *[, kind, ...])

Generate ambient/SFX/music from a prompt.

text_to_speech(text, *[, quality, voice, ...])

Synthesize speech.

text_to_video(prompt, *[, quality, ...])

Generate a video from a text prompt.

upscale_image(image_url, *[, scale, ...])

Upscale an image.

using_fal_credentials(key)

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

voice_clone(reference_audio_url, text, *[, ...])

Generate speech in a cloned voice.

Classes

LlmPrice(*, provider, model, rates[, ...])

One row of the first-party table: a model's rates and where they came from.

AccountStatus(ok, locked, unauthorized, ...)

Outcome of health_check().

Asset(url, kind[, content_type, width, ...])

A single piece of generated media.

Beat(*, id[, speaker, line, action, ...])

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

CallOutcome(*, index, call, status[, ...])

What happened to one CallPlan, at its position in the Plan.

CallPlan(*, tool, application[, backend, ...])

A single planned fal call.

Character(*, name[, description, ...])

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

ContentRef(content_hash, bytes_size)

A content-addressed handle on some bytes falaw has materialized.

Environment(*, name[, description, ...])

A reusable location/setting.

ExecutionReport([outcomes])

The result of running a Plan: one CallOutcome per call, in order.

ModelRecord(*, id, category[, description, ...])

One entry in the fal model catalog.

Plan([calls])

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

CallRepricing(*, index, call, old_cost_usd, ...)

The before/after for one call in a re-priced plan.

CostBasis(*, pricer, priced[, quantities, ...])

How a CallPlan.estimated_cost_usd was arrived at (falaw#60).

Pricer(*, quote, table[, version])

One pricing rule, and the table that backs it.

RepricedPlan(*, plan[, calls])

Result of reprice_plan() — the new Plan plus the per-call diff.

Result([assets, raw, application, arguments])

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

CostEstimate(*, kind, amount[, currency, ...])

Quantitative cost of one fal call against this model.

CostLine(*, kind, item_id, model_id, amount, ...)

One line item in a scene rollup.

CostRollup(*, total_amount[, currency, ...])

Result of estimate_scene_cost().

ProgressEvent(*, kind, application, call_id)

One step in the lifecycle of a fal call.

Scene(*, title[, style, characters, ...])

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

Session([output_dir, journal, history])

A working session for a sequence of falaw operations.

Shot(*, id[, description, framing, ...])

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

ToolSpec(*, name, description, func[, ...])

Single source of truth for a tool exposed by falaw.

Voice(*, name[, voice_id, ...])

A character's voice spec.

LlmRate(*, model, tier, per_call_usd[, ...])

What one routed model costs, on every basis the table knows.

AreaUsage(name, path, entries, bytes)

Disk usage of one cache area.

CacheUsage(root, areas)

Where the falaw cache's disk is going, by area.

PruneCandidate(key, path, bytes[, last_modified])

One thing a prune would delete (or did).

PruneReport(area, dry_run[, candidates, ...])

What a prune removed, or — with dry_run=True — would remove.

LlmReceipt(application, model, cache_hit, ...)

What one eager LLM call cost — the record the eager path never kept.

Exceptions

FalAccountLocked(message, *, status_code[, ...])

fal account is locked / suspended / awaiting verification.

FalAssetFetchError(message, *, url[, cause])

The bytes behind a fal-served asset URL could not be retrieved.

FalClientNotInstalled([what, missing])

A real network call was attempted but the fal.ai client is not installed.

FalBadRequest(message, *, status_code[, ...])

Server rejected the request payload — typical 400 / 422.

FalDurationOutOfRange(message, *, model_id, ...)

The requested duration is outside what the model can produce.

FalError

Base for all falaw-raised exceptions.

FalHTTPError(message, *, status_code[, ...])

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

FalInsufficientFunds(message, *, status_code)

Account balance is insufficient — typical 402.

FalModelHung(message, *, model_id, elapsed_s)

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

FalNonCanonicalArgument(message, *, path)

An argument cannot be canonicalised into falaw's hashed JSON form.

FalPromptTooLong(message, *, model, limit, ...)

The prompt exceeds the model's hard length cap (falaw#76).

FalRateLimited(message, *[, retry_after_s])

fal is throttling requests — typical 429.

FalServerError(message, *, status_code[, ...])

fal-side server error — typical 5xx.

FalTimeout(message, *, elapsed_s[, application])

The fal call timed out before producing a result.

FalUnauthorized(message, *, status_code[, ...])

Missing or invalid API credentials — typical 401.

class falaw.AccountStatus(ok, locked, unauthorized, status_code, detail, url, error)[source]#

Bases: object

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()[source]#

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

Return type:

str

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.AreaUsage(name, path, entries, bytes)[source]#

Bases: object

Disk usage of one cache area.

entries counts the area’s own unit — cache entries for manifests, blobs for content, files for assets and url_index — not files on disk, which is why it is reported next to bytes rather than derived from it.

path is None when the area has no directory of its own. That is the case for manifests (scattered across two-character shard directories at the cache root) and for other. It is deliberately not the cache root: a caller reaching for rmtree(usage.area(...).path) would then destroy the content store along with it.

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

Bases: object

A single piece of generated media.

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

download(*, to=None, fix_suffix=False)[source]#

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

The bytes are sniffed: some models return a different format than the usual one (Recraft’s vector_illustration/* styles return SVG, not PNG). If the suffix of to contradicts the content, a UserWarning is issued; with fix_suffix=True the suffix is corrected instead (so the returned path may differ from to).

Return type:

str

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

Bases: object

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.CacheUsage(root, areas)[source]#

Bases: object

Where the falaw cache’s disk is going, by area.

The whole point of the breakdown: size_bytes alone cannot tell you whether you are looking at gigabytes of irreplaceable blobs or gigabytes of assets/ copies that cost nothing to regenerate.

area(name)[source]#

The AreaUsage named name.

Raises:

KeyError – no such area. The valid names are AREA_NAMES.

Return type:

AreaUsage

summary()[source]#

One human-readable line per area, largest first.

Return type:

str

property total_bytes: int#

Every byte under the cache root.

Equal to a full walk of the root, because other absorbs whatever the named areas do not claim. That identity is the point: a capacity report whose total is “the sum of the areas I thought to enumerate” silently under-reports the moment falaw grows a directory nobody added here, and under-reporting is the one direction a capacity tool cannot afford.

class falaw.CallOutcome(*, index, call, status, artifact=None, error=None, cache_hit=False, blocked_by=(), reason='')[source]#

Bases: object

What happened to one CallPlan, at its position in the Plan.

index is the call’s position in plan.calls and is the identity a caller retries or re-plans by — a report always carries exactly one outcome per call, in plan order, so index is also the safe key for zipping a Plan against anything the caller built alongside it.

artifact: Artifact | None#

The materialized artifact. Set if and only if status == "succeeded".

blocked_by: tuple[int, ...]#

Indices of the calls whose non-success blocked this one. () when the call was blocked for a run-level reason rather than a dependency.

cache_hit: bool#

Whether the result came from the cache as observed at run time.

Not the same thing as falaw.CallPlan.cache_status, which is a prediction made at plan time and can be wrong in both directions (a concurrent run filled the entry; a hit turned out to be unusable and was re-executed). Run-level cost accounting reads this one.

call: CallPlan#

The call this outcome is about — enough to retry it verbatim.

error: BaseException | None#

The exception the call raised. Set if and only if status == "failed".

Kept as the exception object rather than a string so the caller can use falaw’s typed hierarchy (falaw.errors) to decide between backing off, switching models, and giving up.

index: int#

Position in plan.calls. Stable, and unique within a report.

property ok: bool#

Shorthand for status == "succeeded".

reason: str#

Human-readable explanation. Required for blocked; free otherwise.

status: Literal['succeeded', 'failed', 'blocked']#

"succeeded" / "failed" / "blocked". See the module docstring.

class falaw.CallPlan(*, tool, application, backend='fal', arguments, output_kind, estimated_cost_usd=None, cache_status='unknown', expected_duration_s=None, metadata=<factory>, key_extra=<factory>, cost_basis=None)[source]#

Bases: object

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"). Backend-scoped: what it names depends on backend.

arguments: dict#

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

backend: str#

Which execution backend execute_plan() dispatches this call to (falaw#15) — see falaw.backends. Defaults to falaw.canonical.DFLT_BACKEND ("fal"), the only backend until a second one lands. Enters the per-call cache key and plan_hash only when it is not the default, so every call falaw has ever planned or cached keeps its exact pre-#15 digest — see falaw.canonical.

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.

cost_basis: CostBasis | None#

How estimated_cost_usd was computed, or None when nothing recorded it (falaw#60). Present, the quote can be re-run against today’s rate table by falaw.reprice_plan(); absent, that function reports the call as "no_basis" and clears its cost to unknown rather than passing a stale figure off as current. Purely descriptive — it stays out of plan_hash() and the per-call cache key, and out of the serialized dict entirely when unset, so every plan falaw has ever hashed or persisted is unmoved.

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.

key_extra: dict#

Identity beyond the wire arguments — entries that change what the call produces without appearing in arguments. Participates in the per-call cache key AND plan_hash, under omit-if-empty (an empty dict leaves every existing key byte-identical). Unlike metadata, which is deliberately identity-free labelling, putting something here says “a cached result minted without this value must not be reused”. First customer: nw’s Transform impl_version (nw#27) — “same interface, changed behaviour” must miss the cache without renaming anything.

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.CallRepricing(*, index, call, old_cost_usd, new_cost_usd, status, reason='', basis_changed=False)[source]#

Bases: object

The before/after for one call in a re-priced plan.

basis_changed: bool#

True when the rate table’s identity or version moved between the persisted basis and today’s — the audit answer to “did the price change because the table changed?”. Always False for "no_basis", which has nothing to compare against.

call: CallPlan#

The re-priced call — the same call with today’s estimated_cost_usd and a basis stamped with today’s table version.

property delta_usd: float | None#

new - old, or None when either side is unknown.

None rather than 0.0: a call that lost or gained its price did not move by zero, and a caller summing deltas must not be handed a number that says it did.

index: int#

Position in the original falaw.Plan.

new_cost_usd: float | None#

What today’s rates say, None for unknown or no-basis.

old_cost_usd: float | None#

What the persisted plan said, None if it said unknown.

reason: str#

Why, when the status is "unknown" or "no_basis". Empty otherwise.

status: Literal['unchanged', 'changed', 'unknown', 'no_basis']#

Which of the four cases this call fell into — see RepriceStatus.

class falaw.Character(*, name, description='', reference_image_url='', voice=None, style_notes='')[source]#

Bases: object

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

class falaw.ContentRef(content_hash, bytes_size)[source]#

Bases: object

A content-addressed handle on some bytes falaw has materialized.

content_hash is the SHA-256 hex digest of the bytes — the value lacing.Artifact.asset_id is contractually required to hold, and the value that goes into a downstream cache key in place of a URL.

class falaw.CostBasis(*, pricer, priced, quantities=<factory>, table='', table_version='')[source]#

Bases: object

How a CallPlan.estimated_cost_usd was arrived at (falaw#60).

A frozen quote goes stale the moment the rate table moves — 0.0.46 moved falaw’s LLM prices tenfold — and a persisted plan cannot be re-quoted from application and arguments alone: the quantity hints that priced it (the clip’s duration, the prompt’s token bound) are estimator-only and never enter the wire arguments. This records them, plus which table priced them, so a saved plan can be re-quoted faithfully (falaw.reprice_plan()) and audited after the fact.

Pure data. Nothing here reaches the network, and nothing here enters plan_hash() or the per-call cache key: a basis says what a call cost, never what it produces, so stamping one leaves every existing digest byte-identical.

priced: str#

a falaw.ModelRecord.id for the catalogue pricer, the routed model argument for the LLM pricer. Distinct from CallPlan.application, which for fal-ai/any-llm names the router rather than the model that sets the price.

Type:

The entity that was priced

pricer: str#

Which pricing rule produced the figure — a key into falaw.reprice.DFLT_PRICERS. "model_catalogue" for a record priced out of models.json, "llm_rates" for a routed LLM priced out of llm_rates.json. An unrecognized name re-prices as unknown, never as the old number.

quantities: dict#

The estimator-only shape hints fed to the pricer, by keyword — {"seconds": 6.0}, {"input_tokens": 20000, "max_output_tokens": 4000}. Absent keys mean the hint was not supplied, which is what made a quantity-priced call unpriceable in the first place. Omit-when-unset: a hint that was None at plan time is left out rather than written as null.

table: str#

Identity of the rate table consulted, e.g. "falaw/data/llm_rates.json". Free-form so a caller’s own reconciled table can name itself.

table_version: str#

The version of that table at plan time — falaw stamps a short content digest, so a table whose numbers moved gets a different value even when its declared version did not. Empty means unversioned/unknown.

class falaw.CostEstimate(*, kind, amount, currency='USD', notes='', source='approximate')[source]#

Bases: object

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.

amount#

USD (or currency) per unit defined by kind.

currency#

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

notes#

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

source#

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

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

Bases: object

One line item in a scene rollup.

class falaw.CostRollup(*, total_amount, currency='USD', lines=(), skipped=())[source]#

Bases: object

Result of estimate_scene_cost().

by_kind()[source]#

Sum per kind for quick inspection.

Return type:

dict[str, float]

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

Bases: object

A reusable location/setting.

class falaw.ExecutionReport(outcomes=())[source]#

Bases: object

The result of running a Plan: one CallOutcome per call, in order.

len(report.outcomes) == len(plan.calls) always, including on a run where most calls failed. That invariant is the whole point: a consumer that built something per call (nw builds one skeleton annotation per call) can zip against outcomes and stay aligned. Zipping against a shorter list of successes is the silent mis-pairing this type exists to prevent.

artifacts_or_raise()[source]#

Every artifact in plan order, or re-raise the first failure’s exception.

The bridge back to the plain list[Artifact] contract of falaw.execute_plan(). The exception raised is the original one the call raised, unwrapped — falaw’s typed error hierarchy (falaw.errors) is only useful to a caller if it survives the trip through the executor.

A run with no failures but some blocked calls raises too: the list would otherwise be short, and a short list is exactly the silent mis-pairing this type exists to prevent.

Return type:

list[Artifact]

property blocked: tuple[CallOutcome, ...]#

Outcomes whose call never ran, in plan order.

property cache_hit_savings_usd: float#

Estimated USD not spent because a succeeded call was served from cache.

The run-time counterpart of falaw.Plan.cache_hit_savings_usd (which is a plan-time prediction).

property estimated_spend_usd: float#

succeeded calls that were not cache hits.

Estimated, because it sums falaw.CallPlan.estimated_cost_usd — falaw does not read fal’s invoice. Two deliberate exclusions:

  • Cache hits cost nothing, and this reads the observed CallOutcome.cache_hit, not the plan-time prediction.

  • Failed calls are not counted. The vendor may or may not have billed a call that raised, and falaw cannot know which; adding an estimate for it would be inventing a number. Read failed to see how many calls are unaccounted for.

Type:

Estimated USD billed by this run

property failed: tuple[CallOutcome, ...]#

Outcomes whose call raised, in plan order.

property has_unknown_costs: bool#

True when a call that actually billed has no price estimate.

The run-level twin of falaw.Plan.has_unknown_costs, and the reason estimated_spend_usd must never be read on its own: an unpriced call contributes 0.0 to the sum, so a report reading $0.00 means either “nothing was spent” or “we do not know what was spent”. Those are not the same answer, and a budget gate that cannot tell them apart approves the second one.

property is_complete: bool#

True when every call succeeded.

property produced: tuple[Artifact, ...]#

The artifacts that were made, in plan order.

Shorter than the Plan when anything failed — deliberately named so it does not read like something to zip a per-call sequence against. Use outcomes for anything positional.

property succeeded: tuple[CallOutcome, ...]#

Outcomes that produced an artifact, in plan order.

summary()[source]#

A small JSON-able digest — counts, spend, and which indices failed.

For logs, telemetry and run records, where the artifacts and exception objects themselves are not serializable.

Return type:

dict

exception falaw.FalAccountLocked(message, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#

Bases: FalHTTPError

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.FalAssetFetchError(message, *, url, cause=None)[source]#

Bases: FalError

The bytes behind a fal-served asset URL could not be retrieved.

Raised by falaw.content when content-addressing an artifact fails — the URL 404s (fal deletes expired files permanently), the transfer errors, or the response is empty. It is deliberately loud: returning an Artifact whose asset_id is not the SHA-256 of any bytes would break lacing.Artifact’s content-hash contract and poison every downstream cache key derived from it.

exception falaw.FalBadRequest(message, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#

Bases: FalHTTPError

Server rejected the request payload — typical 400 / 422.

exception falaw.FalClientNotInstalled(what='calling fal.ai', *, missing='fal_client')[source]#

Bases: FalError, ImportError

A real network call was attempted but the fal.ai client is not installed.

pip install falaw carries no fal.ai client, so that callers that only build, price, hash and cache plans install none. Anything that actually reaches fal.ai needs the fal extra: pip install 'falaw[fal]'. It is an ImportError, so except ImportError still matches.

exception falaw.FalDurationOutOfRange(message, *, model_id, requested, valid_range)[source]#

Bases: FalError

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]#

Bases: Exception

Base for all falaw-raised exceptions.

exception falaw.FalHTTPError(message, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#

Bases: FalError

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, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#

Bases: FalHTTPError

Account balance is insufficient — typical 402.

exception falaw.FalModelHung(message, *, model_id, elapsed_s)[source]#

Bases: FalError

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.FalNonCanonicalArgument(message, *, path)[source]#

Bases: FalError

An argument cannot be canonicalised into falaw’s hashed JSON form.

Raised by falaw.canonical when a value reaches a key-composition site (the per-call cache key, plan_hash, the dry-run artifact id) that JSON cannot represent faithfully: a non-JSON object, a non-finite float, or a non-string mapping key. Deliberately loud: the old behaviour — json.dumps(..., default=str) — silently collided structurally different calls into one cache key, handing back the wrong artifact as a supposed saving (falaw#17).

path names the offending node, e.g. arguments.extra.ref.

exception falaw.FalPromptTooLong(message, *, model, limit, length)[source]#

Bases: FalError, ValueError

The prompt exceeds the model’s hard length cap (falaw#76).

Raised before any call is queued: fal answers an over-long prompt with an HTTP 422 only after the request round-trips. limit/length are in characters.

exception falaw.FalRateLimited(message, *, retry_after_s=None, **kwargs)[source]#

Bases: FalHTTPError

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, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#

Bases: FalHTTPError

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

exception falaw.FalTimeout(message, *, elapsed_s, application=None)[source]#

Bases: FalError

The fal call timed out before producing a result.

exception falaw.FalUnauthorized(message, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#

Bases: FalHTTPError

Missing or invalid API credentials — typical 401.

class falaw.LlmPrice(*, provider, model, rates, long_prompt_over_tokens=None, long_prompt_rates=None, source='', date='', notes='')[source]#

Bases: object

One row of the first-party table: a model’s rates and where they came from.

class falaw.LlmRate(*, model, tier, per_call_usd, input_usd_per_mtok=None, output_usd_per_mtok=None, source='', date='', notes='')[source]#

Bases: object

What one routed model costs, on every basis the table knows.

model#

The model argument value this row prices, e.g. "anthropic/claude-sonnet-4.5".

tier#

fal’s billing tier for the router — "standard" or "premium". Informational; per_call_usd is already materialized, so no multiplier arithmetic happens in code.

per_call_usd#

What fal bills per request when this model is routed.

input_usd_per_mtok#

Upstream list price per TOKENS_PER_RATE_UNIT prompt tokens, or None when no upstream rate is published for this id — unknown, never zero.

output_usd_per_mtok#

The same for completion tokens.

source#

Where these numbers came from.

date#

ISO date they were read on. Rates move; a quote is only as fresh as its row.

notes#

Free-form caveats.

property has_token_rates: bool#

True when both token columns are populated, so a token quote is possible.

class falaw.LlmReceipt(application, model, cache_hit, chars_in, chars_out, tokens_in=None, tokens_out=None, estimated_cost_usd=None, cost_source='unknown')[source]#

Bases: object

What one eager LLM call cost — the record the eager path never kept.

The falaw half of falaw#50’s option B: the eager llm_complete() returned a bare string, so LLM spend was invisible to every accounting surface — not because estimates were None, but because *nothing was recorded at all* (the field signature: real dollars spent beside plans: 0). A caller that books receipts uses llm_complete_with_receipt() and gets this alongside the text.

Field semantics, honest by construction:

  • chars_in / chars_out are measured facts (prompt+system in, response text out).

  • tokens_in / tokens_out are best-effort from the raw response’s usage block (OpenAI prompt_tokens/completion_tokens and Anthropic input_tokens/output_tokens shapes). None means unrecorded by the provider response, never zero.

  • estimated_cost_usd is 0.0 on a cache hit (nothing was billed), else fal’s published per-request price for the routed model, read off the rate table (falaw.llm_ceiling_usd()) — an estimate, not an observed bill. It is the routed model and not the fal-ai/any-llm record because the record carries fal’s standard tier, and a premium routed model bills ten times that: pricing every turn from the record under-reported premium spend tenfold in the ledger a consumer sums (falaw#55). None only when nothing can price it at all.

  • cost_source says which of those cases produced the number, so a receipt consumer never has to guess: "cache_hit", "rate_table:per_call", "registry:<kind>" (the fallback for a model the table does not price), or "unknown".

class falaw.ModelRecord(*, id, category, description='', aliases=(), quality_tier='', cost_hint='', cost_estimate=None, docs_url='', max_clip_seconds=None, single_character_recommended=False, supported_resolutions=(), default_negative_prompt='', max_prompt_chars=None, size_param='image_size', param_specs=<factory>)[source]#

Bases: object

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.

max_prompt_chars: int | None#

Hard cap on prompt length, from the model’s OpenAPI schema. Calls over it fail at fal with an HTTP 422 after queueing; falaw raises first.

param_specs: Mapping[str, Mapping[str, Any]]#

Discoverable per-model parameters that matter for choosing well, as {name: {"enum": [...], "default": ...}} — e.g. Recraft’s style, whose photoreal default silently overrides a woodcut prompt. See falaw.model_params().

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.

size_param: str#

"image_size" (fal’s named presets, the default) or "aspect_ratio" (+ resolution).

Type:

Which argument carries the output size

supported_resolutions: tuple[str, ...]#

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

class falaw.Plan(calls=())[source]#

Bases: object

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 known_cost_usd: float#

The priced part of total_cost_usd — same number today, but honest by construction.

total_cost_usd coerces unpriced calls to $0.00 so sums stay well-defined; a cumulative budget gate that reads it alone will under-quote (400 unpriceable video calls total $0.00). Read this together with unknown_call_count: the true cost is known_cost_usd plus an unknown amount spread over that many calls, and a correct gate refuses when the count is nonzero rather than pretending the unknown part is free (falaw#18).

property total_cost_usd: float#

Sum of CallPlan.billable_cost_usd across all calls.

property unknown_call_count: int#

How many billable calls carry no price at all.

The countable form of has_unknown_costs — see known_cost_usd for the budget-gate arithmetic it enables.

with_call_replaced(index, new_call)[source]#

Return a new Plan with calls[index] replaced.

Return type:

Plan

class falaw.Pricer(*, quote, table, version=<function Pricer.<lambda>>)[source]#

Bases: object

One pricing rule, and the table that backs it.

The seam for a caller with their own numbers: build a Pricer closing over a reconciled rate table and pass pricers={\*\*DFLT_PRICERS, LLM_RATES_PRICER: mine} to reprice_plan(). The table / version pair is stamped onto the re-priced call’s basis, so the next audit can see which table produced which number.

table is also a gate, not just a label: a call whose basis names a different table is reported as unknown rather than re-quoted, so a row priced from your invoice is never silently re-quoted at falaw’s published rate (or the reverse). Give your Pricer the same table string the basis carries — falaw.llm_rates.CUSTOM_LLM_RATES_TABLE for anything a plan_* priced with llm_rates=.

quote: Callable[[CostBasis], float | None]#

Price basis in USD, or return None for unknown. May raise; a raise is reported as "unknown" with the exception text as the reason, so one unpriceable row never aborts a whole plan’s re-quote.

table: str#

Identity of the table this pricer reads, for the refreshed basis.

version: Callable[[], str]#

Today’s version of that table, for the refreshed basis.

class falaw.ProgressEvent(*, kind, application, call_id, message='', pct=None, elapsed_s=0.0)[source]#

Bases: object

One step in the lifecycle of a fal call.

kind#

Lifecycle stage. See EventKind.

application#

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

call_id#

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

message#

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

pct#

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

elapsed_s#

Seconds since the call started.

class falaw.PruneCandidate(key, path, bytes, last_modified=None)[source]#

Bases: object

One thing a prune would delete (or did).

last_modified is None when the age could not be determined — a non-filesystem blob backend, or a manifest too corrupt to read. Such a candidate is never selected by older_than (an unprovable age is not evidence of staleness) and is evicted last under max_bytes.

class falaw.PruneReport(area, dry_run, candidates=(), deleted=(), kept_entries=0, kept_bytes=0, rebillable_entries=0, unreferenced_candidates=0, errors=())[source]#

Bases: object

What a prune removed, or — with dry_run=True — would remove.

rebillable_entries is the number this exists for: cache entries that will cost money again because of this prune. Its meaning is exact per area, and each is a different claim:

  • manifests — every dropped entry re-bills, so it equals the candidate count.

  • content — entries whose recorded response names an asset that resolves (through the url -> hash index) to a blob being dropped. Those become unmaterializable from cache; they re-download if fal still serves the URL and re-render if it does not, and falaw cannot tell which from here. It mirrors the predicate falaw.plan.execute() applies to a hit from the built-in converter. It is therefore an upper bound, not an exact population: with a custom artifact_converter= the entry is never dropped (it returns a broken artifact instead), and with fetch_bytes=False dropping the blob changes nothing. Both errors are overcounts — the safe direction for a number you read before spending.

  • assets — only the copies whose blob is also gone. While the blob survives, re-materializing is a local copy and costs nothing.

unreferenced_candidates is the other half of the story, and only content populates it: blobs that no manifest points at. falaw cannot tell whether such a blob is garbage or the last copy of something irreplaceable — falaw.materialize_asset() puts reference images and locally-rendered file:// media in the same store, and those never had a fal response behind them. Counting them as rebillable would be wrong (no cache entry re-bills), but reporting nothing would tell an operator the prune is free when it may be destroying the only copy of a reference image.

property freed_bytes: int#

Bytes actually freed — or, under dry_run, that would be freed.

Sums deleted, not candidates. A deletion that failed (a read-only volume, a permission error) leaves its bytes on disk, and a capacity tool that reports them as reclaimed tells an operator staring at a full disk that the problem is solved when it is not.

summary()[source]#

A human-readable line, phrased in the tense the run actually was.

Return type:

str

class falaw.RepricedPlan(*, plan, calls=())[source]#

Bases: object

Result of reprice_plan() — the new Plan plus the per-call diff.

Read plan for the re-quoted plan (it is an ordinary falaw.Plan, so known_cost_usd / unknown_call_count / has_unknown_costs mean what they always mean), and calls for what changed and why.

calls: tuple[CallRepricing, ...]#

One entry per call of the input plan, in order.

property changed: tuple[CallRepricing, ...]#

Calls whose price moved — the ones a re-quote exists to surface.

property known_delta_usd: float#

Sum of CallRepricing.delta_usd over calls priced both times.

The movement you can actually see. It says nothing about the calls in unpriced — read that alongside it, never this alone.

plan: Plan#

The re-priced plan. Structurally identical to the input — same calls, same order, same arguments, so plan_hash is unmoved — with today’s costs.

property unpriced: tuple[CallRepricing, ...]#

"unknown" plus "no_basis".

A budget gate refuses while this is non-empty, the same way it refuses on falaw.Plan.has_unknown_costs — the two are the same judgement, one made at re-quote time.

Type:

Calls today’s rates cannot price

class falaw.Result(assets=<factory>, raw=<factory>, application='', arguments=<factory>)[source]#

Bases: object

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, style='', characters=(), environments=(), shots=(), beats=(), notes='')[source]#

Bases: object

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

with_beat(beat)[source]#

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

Return type:

Scene

class falaw.Session(output_dir=<factory>, journal=<factory>, history=<factory>)[source]#

Bases: object

A working session for a sequence of falaw operations.

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

Bases: object

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, description, func, input_schema=<factory>, output_schema=<factory>, tags=(), examples=(), version='0.0.1')[source]#

Bases: object

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, voice_id='', reference_audio_url='', model_id='', style_notes='')[source]#

Bases: object

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, audio_url, *, prompt='', quality='balanced', model_id=None, extra=None)[source]#

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

Return type:

Result

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

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

Return type:

Beat

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

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

Return type:

Scene

falaw.beat_content_hash(beat, *, character=None)[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.

Return type:

str

falaw.cache_get(application, arguments, *, backend='fal', key_extra=None)[source]#

Return the raw fal response if cached, else None.

Return type:

Optional[dict]

falaw.cache_put(application, arguments, raw, *, note='', wire_arguments=None, backend='fal', key_extra=None)[source]#

Persist a fal response. Returns the entry directory path.

Parameters:
  • application (str) – fal model id.

  • arguments (Mapping[str, Any]) – the arguments the entry is keyed on.

  • raw (dict) – the fal response to store.

  • note (str) – free-form label recorded in the manifest.

  • wire_arguments (Optional[Mapping[str, Any]]) – the arguments actually sent to fal, when they differ from the key arguments (chained calls send URLs but are keyed on content hashes). Recorded for debugging only — it never affects the key.

  • backend (str) – which execution backend produced raw (falaw#15). Joins the cache key only when non-default, same rule as falaw.canonical.cache_key_payload(); recorded in the manifest under the same condition, for debugging.

Return type:

str

The manifest is written to a temporary file and moved into place with os.replace(), so a reader never sees a half-written entry. That is not hypothetical since execute_plan(concurrency=N): two calls of one Plan that are structurally identical land on the same key, and an interleaved json.dump would leave a permanently unparseable entry — a cache that poisons itself under exactly the fan-out it exists to make cheap.

falaw.cache_stats()[source]#

Quick summary of the cache: entry count, disk usage, and where it went.

areas is the part worth reading. Since falaw#14 the cache holds the bytes of every generated asset, so a total on its own cannot distinguish gigabytes of irreplaceable blobs from gigabytes of assets/ copies that cost nothing to regenerate — and only the second is safe to reclaim without thinking. Each area’s economics, and the primitives that reclaim it, are in falaw.prune.

size_bytes is every byte under the cache root, unchanged in meaning from before the breakdown existed: the other area absorbs whatever the named areas do not claim, so the areas always sum to the whole.

(+SKIPed — it reads the caller’s real cache, a multi-gigabyte walk on the production box. tests.test_prune pins this against a throwaway cache instead.)

Return type:

dict

>>> stats = cache_stats()
>>> sorted(stats["areas"])
['assets', 'content', 'manifests', 'other', 'scenes', 'url_index']
falaw.cache_usage()[source]#

Disk usage of the falaw cache, broken down by area.

The structured form behind falaw.cache_stats(). Read the module docstring for what each area costs to reclaim.

Return type:

CacheUsage

falaw.cached_call_fal(application, arguments, *, key_arguments=None, refresh=False, on_event=None, backend='fal', key_extra=None)[source]#

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

Parameters:
  • application (str) – fal model id.

  • arguments (Mapping[str, Any]) – model input dict — what is sent on the wire.

  • key_arguments (Optional[Mapping[str, Any]]) – what the cache entry is keyed on, when that differs from what goes on the wire. Defaults to arguments. The split exists because a chained call must send fal an expiring URL for its upstream input while being keyed on that input’s content hash, so a byte-identical upstream regeneration hits instead of re-billing.

  • refresh (bool) – 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”.

  • backend (str) – which execution backend serves this call (falaw#15) — resolved via falaw.backends. Also joins the cache key (non-default only), so two backends never share an entry. Despite the name, this function is no longer fal-specific; the name is kept because “fal” is still the only backend and every existing call site already spells it this way.

Return type:

dict

Returns:

Raw response (whether from cache or network).

falaw.call_fal(application, arguments, *, on_log=None, on_event=None, with_logs=True, journal_errors=True, api_key=None)[source]#

Call a fal model via fal_client.subscribe.

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

  • arguments (Mapping[str, Any]) – Input arguments. Keys depend on the model.

  • on_log (Optional[Callable[[str], None]]) – Legacy log callback — receives raw log lines as strings. Defaults to no-op (use on_event for structured access).

  • on_event (Optional[Callable[[ProgressEvent], None]]) – Per-call subscriber for ProgressEvent`s. Fires in addition to the global subscribers registered via :func:`falaw.events.subscribe.

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

  • journal_errors (bool) – 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 (Optional[str]) – 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.

Return type:

dict

Returns:

The raw response dict from the model.

falaw.call_plan_from_dict(d)[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. backend defaults to DFLT_BACKEND when absent — a dict written before falaw#15 names no backend, and it always meant "fal".

cost_basis is absent on any plan written before falaw#60 and on any call nothing stamped; it comes back as None, which falaw.reprice_plan() reports as "no_basis" — never as a quote that is still current.

Return type:

CallPlan

falaw.call_plan_to_dict(call)[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.

backend (falaw#15) is a tolerated-default addition, not a PLAN_DICT_SCHEMA bump: it is always written, but call_plan_from_dict() defaults it to DFLT_BACKEND when absent, so a dict from before this field existed still parses, and a dict written by this version still parses under older falaw (the extra key is simply never read there). No migration needed either direction.

cost_basis (falaw#60) is the same kind of addition under a stricter rule — omit-when-unset: the key is written only when a basis exists, so a plan built without one serializes to the exact bytes it did before the field existed, and every stored plan, fixture and cassette is unmoved.

Return type:

dict

falaw.cast_character(name, description, *, image_url='', style='', quality='high', voice_id='', reference_audio_url='', voice_style='')[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.

Return type:

Character

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

Attach or update the Voice on a Character.

Return type:

Character

falaw.catalogue_cost_basis(model_id, **quantities)[source]#

A falaw.CostBasis for a call priced from models.json.

quantities are falaw.estimate_call_cost() keywords (count, seconds, megapixels, tokens); None values are dropped, so a hint that was never supplied is recorded as absent rather than as null — the same thing the estimator saw.

Return type:

CostBasis

>>> catalogue_cost_basis("fal-ai/flux/dev", seconds=None).quantities
{}
falaw.check_model_liveness(ids=None, *, status_of=None)[source]#

{endpoint_id: is_live} for ids (default: every catalogued model).

Only a 404 means dead; any other non-200 answer (rate limit, fal outage) raises rather than report a live model as dead, which would get it deleted.

Return type:

dict[str, bool]

falaw.clear_subscribers()[source]#

Drop all registered subscribers. Mostly for tests.

Return type:

None

falaw.composite_character_in_environment(character_image_url, environment_image_url, *, prompt='', quality='balanced', model_id=None, extra=None)[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).

Return type:

Result

falaw.content_ref_for_url(url, *, store=None, fetcher=None, refresh=False, assume_immutable=None)[source]#

Materialize url’s bytes into store and return their content hash.

Idempotent and cheap on repeat, but how cheap depends on whether the URL can change behind falaw’s back — and falaw decides that rather than asking the caller to know (thorwhalen/falaw#23):

  1. Immutable URL (fal’s own — see is_immutable_url()) with a remembered hash whose blob is present: returned immediately, **no network**. This is what makes re-executing an already-cached plan free rather than re-downloading every clip.

  2. Mutable URL with a remembered hash: falaw revalidates — a conditional GET replaying the recorded ETag / Last-Modified (for file://, a (mtime, size) comparison). A 304 costs one round-trip and no payload; a 200 means the bytes really changed and the new ones are stored, so the content hash changes with them.

  3. No usable answer — nothing remembered, no validators recorded, or a transport that cannot make conditional requests: a plain fetch.

Step 3 is the important default. A transport that cannot revalidate makes falaw re-fetch, never trust: an unverifiable hint is not evidence.

Parameters:
  • url (str) – The asset URL. file:// is supported and treated as mutable.

  • store – Injected lacing.ArtifactStore; defaults to default_content_store().

  • fetcher (Optional[Callable[[str], Iterable[bytes]]]) – Injected byte source; defaults to default_url_fetcher() (the built-in urllib transport unless using_url_fetcher() has installed another). To support revalidation, a custom transport exposes a conditional_fetch(url, validators) -> ConditionalOutcome attribute; without one it is simply never asked.

  • refresh (bool) – Skip every shortcut and re-fetch unconditionally. Rarely needed now that mutable URLs revalidate on their own — keep it for a URL whose origin lies about its validators.

  • assume_immutable (Optional[bool]) – Override the host-based decision. True restores the old unconditional trust (use for a host you mint yourself, and prefer adding it to IMMUTABLE_URL_HOSTS); False forces revalidation even for fal.

Raises:

FalAssetFetchError – the bytes could not be retrieved, or the response was empty. Never returns a reference it could not back with bytes — a silent zero-byte “artifact” is the failure mode this guards.

Return type:

ContentRef

falaw.cost_basis_from_dict(d)[source]#

Rebuild a CostBasis from a cost_basis_to_dict() dict.

Return type:

CostBasis

falaw.cost_basis_to_dict(basis)[source]#

Convert a CostBasis to a plain JSON-serializable dict.

table / table_version are omitted when empty, matching the omit-when-unset discipline the rest of the wire shape follows — a basis that names no table writes no key rather than a pair of "".

Return type:

dict

falaw.current_fal_key()[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.

Return type:

Optional[str]

falaw.dead_models(ids=None, *, status_of=None)[source]#

The ids from check_model_liveness() that are no longer deployed.

Return type:

list[str]

falaw.default_content_store()[source]#

The falaw-cache-rooted lacing.ArtifactStore.

Rooted at <falaw cache dir>/content, so it moves with $FALAW_CACHE_DIR / $FALAW_DATA_DIR like every other piece of falaw state. Constructed per call (the constructor only ensures directories exist) so a test that re-points the cache dir gets a fresh store.

Deleting a blob removes it from disk rather than moving it to the OS trash. dol.Files — the blob backend from_directory lays down — trashes by default, including on Linux, which is a kind default for a user’s own files and the wrong one here: every blob is derived data, and the only thing in falaw that deletes one is falaw.prune.prune_content(), whose whole job is to give the volume its space back. A trashed blob frees nothing (the trash usually lives on the same volume) while the prune report says it did (thorwhalen/falaw#66). The deletion is still an explicit, dry_run-by-default operator act, so the trash was never the safety net.

falaw.default_url_fetcher()[source]#

The transport used when no fetcher= argument is given.

using_url_fetcher()’s installed fetcher if there is one, else the built-in urllib-based one. Resolved at call time, so every falaw entry point that reads asset bytes — content_ref_for_url(), falaw.materialize_asset(), falaw.execute_plan() and falaw.execute_plan_isolated() — honours an override installed after they were imported.

Return type:

Callable[[str], Iterable[bytes]]

falaw.drop_cache_entry(application, arguments, *, backend='fal', key_extra=None)[source]#

Delete the cache entry for (application, arguments). Returns whether one existed.

The counterpart to cache_put(), and the mechanism that keeps a cache from becoming a trap. An entry whose response can no longer be turned into a usable artifact — fal deleted the URL and the bytes are not in the content store — must be a miss, not a permanent failure: without this, escaping one dead call means re-running the whole plan. falaw.plan.execute() calls it.

The plan-level escapes are execute_plan(plan, refresh=True), which re-runs every call and keeps what it pays for, and execute_plan(plan, use_cache=False), which re-runs and keeps nothing — so the second one bills again on the next run (falaw#49). Neither is a substitute for this function: both re-bill every call in the plan, and this drops the one entry that actually went bad.

Only the manifest is removed. Blobs in the content store are shared by content hash across entries and are never dropped from here.

Return type:

bool

falaw.edit_image(image_url, prompt, *, quality='balanced', model_id=None, extra=None)[source]#

Edit an image with a natural-language instruction.

Return type:

Result

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

Create an Environment with a canonical establishing image.

Return type:

Environment

falaw.estimate_call_cost(record, *, count=1, seconds=None, megapixels=None, tokens=None)[source]#

Cost of one fal call against record.

Returns None when the cost is unknown, so callers can distinguish “free” from “we can’t say”. That happens when the record carries no cost_estimate at all, or when its pricing is quantity-based (per_second / per_token) and the caller did not supply the quantity — an unpriceable call, not a free one.

Returning 0.0 for a missing quantity would be actively dangerous: a per_second clip is the single most expensive thing fal bills for, and a caller gating a budget on the answer would read “$0.00” and spend real money without a prompt. None instead propagates to CallPlan.estimated_cost_usd and lights up Plan.has_unknown_costs, which exists for exactly this case. Callers that know the quantity should pass it; per_second callers can fall back to record.max_clip_seconds for an upper bound.

per_megapixel is deliberately different: an image’s pixel budget has a sane house default (below), so it stays priceable.

Return type:

Optional[float]

falaw.estimate_scene_cost(scene, *, tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, shot_seconds=None)[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).

shot_seconds is the assumed clip length used to price shots_as_video. A Shot carries no duration of its own — screen time comes from the renderer’s per-shot run — and image-to-video models bill per second, so without this the video lines are genuinely unpriceable and are reported in CostRollup.skipped rather than silently priced at $0.00. Pass the length you expect (the beat path has the same knob as estimated_seconds).

Return type:

CostRollup

falaw.execute_plan(plan, *, on_event=None, dry_run=False, use_cache=True, refresh=False, artifact_converter=None, content_store=None, fetch_bytes=None, asset_fetcher=None, concurrency=1)#

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

This is the halt policy: the first call that raises ends the run, and its exception propagates unchanged (falaw’s typed hierarchy — see falaw.errors — is what a caller classifies on, so it is never wrapped). Use execute_isolated() when one bad call must not discard the rest of a fan-out; it returns an ExecutionReport with one outcome per call instead of raising.

Parameters:
  • plan (Plan) – The Plan to execute.

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

  • dry_run (bool) – 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 (bool) – When True (default), executes via cached_call_fal so cache hits skip the network. When False, the cache is not touched at all — neither read nor written. That is the historical meaning and it is unchanged: a caller who genuinely wants no cache interaction still has it. To re-run a plan and keep what the re-run paid for, leave use_cache=True and pass refresh.

  • refresh (bool) – When True, skip the cache read and keep the cache write — the third mode (falaw#49). Every call executes fresh, and every fresh result is stored under the key it would have been read from, so the next run (or a downstream consumer) hits it. Requires use_cache=True, since there is no key to write under when the cache is off; the contradictory combination raises rather than silently picking one. This is the mode to reach for behind a force / “re-verify this” switch: without it, forcing a re-run discards the result it just paid for and guarantees the next consumer re-bills.

  • artifact_converter (Optional[ResultToArtifact]) – 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}}). Mutually exclusive with content_store / fetch_bytes / asset_fetcher, which configure the built-in converter only — passing both raises, rather than silently ignoring the ones a custom converter cannot honour. Converters do not own cost_usd: the executor stamps it from the observed run outcome after conversion, overwriting whatever the converter set (falaw#26).

  • content_store – Injected lacing.ArtifactStore that media bytes are materialized into. Defaults to falaw.content.default_content_store() (a directory store rooted in the falaw cache). Point this at an S3-backed store to share content — and therefore cache hits — across machines.

  • fetch_bytes (Optional[bool]) – Whether to download each media result so its asset_id is the SHA-256 of its bytes. Defaults to DFLT_FETCH_BYTES (true), overridable process-wide via FETCH_BYTES_ENVVAR. Opting out forfeits caching for chained calls: without bytes there is no content hash, so downstream calls fall back to keying on the upstream URL — which fal mints fresh per upload, so the downstream entry can never be reused across runs or machines. It also means asset_id is not a content hash, in violation of lacing.Artifact’s contract. Use it only when you genuinely want URL-only artifacts and no reuse.

  • asset_fetcher – Injected byte source (url -> Iterable[bytes]) used to read media results; defaults to falaw.content.default_url_fetcher(). This is the per-call transport seam. A hermetic test suite usually wants the process-wide one instead — falaw.testing.fake_assets(), built on falaw.content.using_url_fetcher() — because a suite reaching falaw through its own public API has no execute call site to pass this to. Passing it here still wins over any installed default. ($FALAW_FETCH_ARTIFACT_BYTES=0 also silences the network, but by turning content addressing off — see FETCH_BYTES_ENVVAR.)

  • concurrency (int) – How many calls may be in flight at once. 1 (DFLT_CONCURRENCY) runs the Plan sequentially, on the calling thread, exactly as it always has. Above 1, independent calls run on a thread pool bounded by this number — a Plan is I/O-bound (an HTTP request that fal takes tens of seconds to answer), so threads are the right tool and the bound is what keeps a 200-call fan-out from becoming 200 simultaneous paid requests. **Chained calls are never parallelised with their producers**: a call holding a "<from N>" placeholder waits for call N. Two things to weigh before raising it: the vendor’s rate limit, and memory — materializing a media result peaks at roughly twice the asset’s size (thorwhalen/lacing#25), so concurrency multiplies the peak.

Return type:

list

Failure handling — a paid result is never discarded#

Two different things can go wrong when reading a result’s bytes, and they get two different answers:

  • A fresh call whose bytes cannot be fetched. fal has already run — and billed — the generation. Raising would throw away a result we paid for, typically over a transient network failure. So the artifact degrades: url is kept, bytes_size stays 0, asset_id is a digest of the response and is not claimed to be a content hash, and a UserWarning is emitted. Downstream key resolution reads

    bytes_size == 0 and falls back to the URL — a guaranteed cache miss, never a wrong hit.

  • A cache hit that cannot be materialized — fal deleted the URL and the bytes are not in the content store. The entry is unusable, so it is treated as a miss: it is invalidated and the call re-executed once. A cache must never become a trap whose only escape is re-billing the whole plan with use_cache=False.

Placeholder resolution — the wire/key split#

Any string argument equal to "<from N>" (for an integer N) is rewritten 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.

It happens twice, into two different argument sets, because the same value cannot serve both jobs:

  • the wire arguments get artifacts[N].url — what fal needs in order to fetch the input;

  • the key arguments get sha256:<artifacts[N].asset_id> — the upstream’s content hash, so a byte-identical upstream regeneration produces a downstream cache hit instead of re-billing the expensive call. Keying on the URL instead is the defect this split exists to fix (falaw#14): fal mints a unique URL per upload, so a URL-keyed downstream entry is unreachable the moment the upstream genuinely re-runs.

An upstream artifact with no materialized bytes has no content hash, so its key ref falls back to the URL — a guaranteed miss, never a wrong hit.

rtype:

list

returns:

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

falaw.execute_plan_isolated(plan, *, on_event=None, dry_run=False, use_cache=True, refresh=False, artifact_converter=None, content_store=None, fetch_bytes=None, asset_fetcher=None, concurrency=1, halt_on_failure=False)#

Execute a Plan with per-call failure isolation, returning a report.

The fan-out counterpart of execute(). Where execute raises on the first failure — discarding every artifact produced before it, each of which fal has already billed — this returns an ExecutionReport carrying one CallOutcome per call: the successes with their artifacts, the failures with their exceptions, and the calls that never ran with the reason why.

len(report.outcomes) == len(plan.calls) always, so a caller that built something per call can zip against report.outcomes and stay aligned.

Parameters:

halt_on_failure (bool) – When True, stop submitting work as soon as any call fails; everything not yet started is reported blocked with a run-level reason. This is what execute() uses, and at concurrency=1 it reproduces the historical sequential behaviour exactly. Note that at concurrency > 1 calls already

Return type:

ExecutionReport in flight are not cancelled — a fal request cannot be recalled once made, and pretending otherwise would discard results that were billed anyway.

:param All other arguments are as execute().:

Three outcome states, not two#

failed and blocked are different questions for the caller. A failed call can be retried verbatim. A blocked one cannot: its input does not exist, so it has to be re-planned after its producer succeeds. Any call holding a "<from N>" placeholder whose call N did not succeed is blocked, transitively.

Examples

>>> from falaw import CallPlan, Plan, execute_plan_isolated
>>> plan = Plan(calls=(CallPlan(tool="t", application="m",
...                             arguments={}, output_kind="image"),))
>>> report = execute_plan_isolated(plan, dry_run=True)
>>> report.is_complete, len(report.outcomes)
(True, 1)
falaw.extract_models_from_corpus(path)[source]#

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

Return type:

Iterator[dict]

falaw.fetch_model_prices(endpoint_ids, *, api_key=None, http_get=None, batch_size=50)[source]#

{endpoint_id: {"unit_price", "unit", "currency"}} from fal’s API.

Batches requests at batch_size ids (the endpoint caps at MAX_IDS_PER_REQUEST). Ids the API does not know are simply absent from the result — the caller decides what absence means. That takes work: the live endpoint answers a batch containing even one unknown id with a blanket 404, so a failed batch is bisected down to the ids that actually price (O(unknown x log batch) extra requests).

Return type:

dict[str, dict]

falaw.generate_audio(prompt, *, kind='ambient', duration_s=None, model_id=None, extra=None)[source]#

Generate ambient/SFX/music from a prompt. Argument shape mirrors falaw.plan_generate_audio() exactly, so planned and eager calls with identical inputs collapse to the same cache entry.

Return type:

Result

falaw.generate_image(prompt, *, quality='balanced', image_size='landscape_4_3', model_id=None, extra=None)[source]#

Generate an image from a text prompt.

image_size is a fal preset (landscape_16_9 …); models that size by aspect_ratio (nano-banana-pro) get the equivalent ratio, with extra={"resolution": "2K"} for the pixel tier. A prompt over the model’s cap raises falaw.FalPromptTooLong before any call. Check falaw.model_params() for enums whose default may not be what you want (Recraft style defaults to photoreal).

Return type:

Result

falaw.generate_image_with_refs(prompt, reference_image_urls, *, quality='balanced', model_id=None, extra=None)[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.

Return type:

Result

falaw.get_llm_price(provider, model, *, prices=None)[source]#

The row for model (exact id), or None when the table does not list it.

Return type:

Optional[LlmPrice]

falaw.get_llm_rate(model, *, rates=None)[source]#

The row for model, or None when the table does not price it.

None is the load-bearing answer: it means unknown, and every caller must propagate it as unknown rather than substituting the router’s flat price for a model nobody has priced.

Return type:

Optional[LlmRate]

falaw.image_to_video(image_url, prompt='', *, quality='high', model_id=None, extra=None)[source]#

Animate a still image into a video.

Return type:

Result

falaw.iter_render_scene(scene, *, tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, force=False, concurrency=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, audio_url, *, quality='high', model_id=None, extra=None)[source]#

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

Return type:

Result

falaw.list_llm_prices(provider=None, *, prices=None)[source]#

Every row, or the rows of one provider.

Return type:

list[LlmPrice]

falaw.list_llm_rates(*, rates=None)[source]#

Every row in the table, in file order — the catalogue view.

Return type:

list[LlmRate]

falaw.llm_call_cost_usd(provider, model, usage, *, prices=None)[source]#

(USD, known) for one first-party call from the vendor’s reported usage.

  • provider="anthropic": input_tokens (uncached), output_tokens, cache_read_input_tokens, cache_creation_input_tokens.

  • provider="openai": Responses (input_tokens / output_tokens / input_tokens_details.cached_tokens) or Chat Completions (prompt_tokens / completion_tokens / prompt_tokens_details.cached_tokens). Input includes the cached part, which is billed at the cached rate; reasoning tokens are already in output.

known is False when the model is not in the table; the cost is then priced at the provider’s dearest listed rates, which is not a ceiling for a model pricier than any listed (see the module docstring). prices= swaps in the caller’s own table, e.g. one reconciled against an invoice.

Raises:

ValueError – provider is not one of PROVIDERS.

Return type:

tuple[float, bool]

falaw.llm_ceiling_usd(model, *, input_tokens=None, max_output_tokens=None, count=1, rates=None)[source]#

Ceiling cost in USD of count calls routing model, or None.

Two modes, decided by whether the caller passes a token hint at all.

No hints — quote fal’s published per-request price for the routed model. That is a known vendor fact, and for the premium tier it is ten times what the router’s flat record said:

>>> flat = llm_ceiling_usd('anthropic/claude-sonnet-4.5')
>>> flat
0.01

Both hints — quote the maximum of that request price and input_tokens in plus max_output_tokens out at the routed model’s upstream rates. Taking the max is what makes it a ceiling: the flat price binds a short prompt, the token sum binds a long one, and quoting either alone under-quotes the other case.

>>> long_prompt = llm_ceiling_usd(
...     'anthropic/claude-sonnet-4.5', input_tokens=20_000, max_output_tokens=4_000
... )
>>> round(long_prompt, 4)
0.12
>>> long_prompt > flat        # length raises the quote; the flat rate is a floor
True

Monotone in both hints, so a bigger hint never quotes less:

>>> a = llm_ceiling_usd('openai/gpt-4o', input_tokens=1_000, max_output_tokens=100)
>>> b = llm_ceiling_usd('openai/gpt-4o', input_tokens=9_000, max_output_tokens=100)
>>> b >= a
True

Both hints are upper bounds the caller holds at plan time: the prompt is already written, and max_tokens caps the response. Real output length is unknowable before the call, which is exactly why this quotes the cap and never a midpoint guess.

A token quote is therefore all or nothing. Asking for one that cannot be bounded yields None — unknown, which forces approval — rather than a number that looks like a ceiling and is not. That covers one hint without the other (an uncapped response has no bound to quote):

>>> llm_ceiling_usd('anthropic/claude-sonnet-4.5', input_tokens=50_000) is None
True

and a row with no published upstream token rates (the request price alone is not a ceiling on a token bill nobody has priced):

>>> llm_ceiling_usd(
...     'google/gemini-flash-1.5', input_tokens=50_000, max_output_tokens=1_000
... ) is None
True
Parameters:
  • model (str) – The model argument the call will send to the router.

  • input_tokens (Optional[int]) – Upper bound on prompt tokens. None together with max_output_tokens selects the per-request basis alone.

  • max_output_tokens (Optional[int]) – The response cap (any-llm’s max_tokens).

  • count (int) – Number of identical calls.

  • rates (Optional[Mapping[str, LlmRate]]) – Override table, for a caller who has reconciled real rates against their own billing.

Raises:
  • TypeError – a token hint or count that is not a plain int. A bool and a float both do the arithmetic silently — True prices one token, 1.5e4 prices a fractional one — so a caller who passed the wrong thing would get a number rather than a complaint.

  • ValueError – a negative token hint or count, which would make the token basis lower the ceiling — silently, and in the one direction a spend gate must never err.

Return type:

Optional[float]

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

Single-shot LLM completion. Returns the assistant text.

Delegates to llm_complete_with_receipt() (one argument-construction site, so the two spellings share one cache identity) and discards the receipt. A caller that accounts for spend wants the receipt variant.

Return type:

str

falaw.llm_complete_with_receipt(prompt, *, system='', model='anthropic/claude-sonnet-4.5', temperature=0.7, extra=None)[source]#

Single-shot LLM completion that also reports what it cost.

Same arguments, same wire call, same cache identity as llm_complete() — the receipt is a side channel, observed from the call this function was making anyway (the cache layer’s cache_hit event, the response’s usage block, the registry’s estimate). Library surface only — deliberately NOT a registered tool: the tool surface keeps the one string-returning llm_complete.

Return type:

tuple[str, LlmReceipt]

falaw.llm_cost_basis(routed_model, *, input_tokens=None, max_output_tokens=None, custom_rates=False)[source]#

A falaw.CostBasis for a routed LLM call priced from the rate table.

custom_rates=True records that the quote came from a caller-supplied rates= table rather than the committed one: there is no file to digest, so the version stays empty and the table names itself falaw.llm_rates.CUSTOM_LLM_RATES_TABLE. reprice_plan() then refuses to re-quote it with the committed table, reporting unknown — a caller’s reconciled $0.50 row re-priced at falaw’s published $0.01 is a 50x under-quote, not a price drop. Re-quote it by passing a Pricer over your table that names that same identity.

Return type:

CostBasis

falaw.llm_prices_table_version()[source]#

The table’s version, a digest of its bytes (see falaw.base.data_table_version()).

Return type:

str

falaw.make_call_plan(*, tool, application, arguments, output_kind, backend='fal', estimated_cost_usd=None, expected_duration_s=None, metadata=None, consult_cache=True, cost_basis=None)[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".

A chained call — arguments still holding a "<from N>" placeholder, because the upstream call has not executed yet — is never peeked (falaw#15, D2): its resolved key is not knowable at plan time, and the unresolved key is never written by anything (execute() always keys on the resolved form), so peeking it can only ever report a false "miss" — never a real "hit". That silently over-quotes cost and under-reports the plan’s cache_hit_savings_usd on every chained call, which under prepaid billing (a quote that may be deducted) is a billing bug. cache_status is "unknown" here instead, exactly the case CacheStatus documents it for.

cost_basis records how estimated_cost_usd was arrived at so the quote can be re-run later (falaw#60). It is descriptive only: it never reaches the cache key or plan_hash(), and a plan built without one is byte-identical to one built before the field existed.

Raises:

FalNonCanonicalArgument – an argument cannot be hashed faithfully (non-JSON object, non-finite float, non-string mapping key). Raised here — while planning is still free — rather than at key-composition time on the way to the network (falaw#17).

Return type:

CallPlan

falaw.materialize_asset(url, *, key_hint='', store=None, fetcher=None, refresh=False)[source]#

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

The local filename is content-addressed by the asset’s bytes, so two URLs serving identical bytes resolve to one file. The extension is a presentational hint for ffmpeg/PIL; the SHA-256 is the address.

Repeat calls are cheap, in three widening circles — this is what makes it safe to call from a loop over 200 shots:

  1. the file is already on disk here (no store lookup, no network) — immutable URLs only, see below;

  2. the bytes are in the content store (no network, or one validating round-trip) — so it still works after fal has expired the URL;

  3. otherwise, one download.

Circle 1 matters on its own: the content store is prunable, so an asset can survive as a materialized file after its blob is gone.

Circle 1 is taken only when the URL cannot change — that is, falaw.content.is_immutable_url() — because reaching it requires trusting the url -> hash index to name the current bytes, and for an arbitrary caller-supplied URL it does not (thorwhalen/falaw#23). A mutable URL goes to circle 2, where falaw.content.content_ref_for_url() revalidates before reusing anything; when the bytes really are unchanged that costs one conditional request and still no download, and when they have changed you get the new file instead of silently getting the old one.

Parameters:
  • url (str) – the remote asset URL. file:// is supported and is used deliberately by downstream packages for locally-rendered media.

  • key_hint (str) – optional human-readable filename prefix.

  • store – injected lacing.ArtifactStore; defaults to falaw.content.default_content_store().

  • fetcher – injected byte source (url -> Iterable[bytes]); defaults to the urllib-based one. The seam for custom transport (auth headers, retries) and for a hermetic test suite.

  • refresh (bool) – re-fetch unconditionally, skipping every circle. Rarely needed now: a mutable URL revalidates on its own (falaw#23), so this is for an origin that lies about its validators.

Raises:

FalAssetFetchError – the bytes could not be retrieved. Unlike a generated-media artifact (which degrades to URL-only — see falaw.plan.execute()), there is nothing to degrade to here: the caller asked for a local file.

Return type:

str

falaw.model_constraints(id)[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.

Return type:

dict

falaw.model_params(id)[source]#

Per-model parameters worth knowing before a call, {name: spec}.

Each spec is {"enum": [...], "default": ...}; resolves aliases. For fal-ai/recraft/v3/text-to-image this shows style defaults to realistic_image — a photograph, whatever the prompt describes — and lists the styles that actually give a woodcut or a linocut. Empty when the catalogue records none (fal’s OpenAPI schema is the full source).

Return type:

dict

falaw.parse_response(raw, *, application, arguments)[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.

Return type:

Result

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

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

Return type:

Scene

falaw.pick_model(*, category, quality_tier='balanced')[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.

Return type:

ModelRecord

falaw.plan_animate_face(image_url, audio_url, *, prompt='', quality='balanced', model_id=None, duration_s=None, extra=None, metadata=None, consult_cache=True)[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).

Return type:

CallPlan

falaw.plan_composite_character_in_environment(character_image_url, environment_image_url, *, prompt='', quality='balanced', model_id=None, extra=None, metadata=None, consult_cache=True)[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.

Return type:

CallPlan

falaw.plan_dependencies(plan)[source]#

Per-call set of the call indices it references via "<from N>".

The Plan’s dependency DAG, read straight off the placeholders — one frozenset per call, in plan order, so deps[3] == {1} means call 3 consumes call 1’s output. An empty set means the call is independent and may run concurrently with any other independent call, which is what execute_isolated() schedules on.

Also the plan’s structural validator, and it runs **before a cent is spent**: a malformed reference used to surface only when execution reached the offending call, i.e. after every call before it had been billed.

Raises:

ValueError – a placeholder that is not "<from N>" for an integer N; an N outside the plan; or an N that does not run before the referencing call (including a self-reference) — the output would not exist yet, so it can only ever be a bug.

Return type:

tuple[frozenset[int], …]

>>> a = CallPlan(tool="t", application="m", arguments={}, output_kind="image")
>>> b = CallPlan(tool="t", application="m",
...              arguments={"image_url": "<from 0>"}, output_kind="video")
>>> plan_dependencies(Plan(calls=(a, b)))
(frozenset(), frozenset({0}))
>>> plan_dependencies(Plan(calls=(b,)))
Traceback (most recent call last):
    ...
ValueError: Placeholder '<from 0>' in call 0 references call 0, which does not run before it. ...
Return type:

tuple[frozenset[int], ...]

falaw.plan_edit_image(image_url, prompt, *, quality='balanced', model_id=None, extra=None, metadata=None, consult_cache=True)[source]#

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

Return type:

CallPlan

falaw.plan_from_dict(d)[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.

Return type:

Plan

falaw.plan_generate_audio(prompt, *, kind='ambient', duration_s=None, model_id=None, extra=None, metadata=None, consult_cache=True)[source]#

Plan a falaw.generate_audio() call (prompt → ambient/SFX/music).

The planning primitive behind an ambient bed or music cue (falaw#10): the user who leaves their editor to hunt a city-night ambience is the user this generates one for — costed, cached and planned like every other call. Mirrors the eager falaw.generate_audio() signature so a planned call and an eager call with identical inputs collapse to the same cache entry. Pure data; no network at plan time.

kind selects the default model (see GENERATE_AUDIO_DEFAULTS for why these are explicit ids). duration_s reaches the model as its integer duration argument where the model takes one (mmaudio does) AND feeds the cost estimate; for models with no duration argument it is estimator-only.

Return type:

CallPlan

falaw.plan_generate_image(prompt, *, quality='balanced', image_size='landscape_4_3', model_id=None, extra=None, metadata=None, consult_cache=True)[source]#

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

Return type:

CallPlan

falaw.plan_generate_image_with_refs(prompt, reference_image_urls, *, quality='balanced', model_id=None, extra=None, metadata=None, consult_cache=True)[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.

Return type:

CallPlan

falaw.plan_hash(plan)[source]#

Stable, plan-scoped structural idempotency key for a whole Plan.

Answers “does this whole plan match one I already ran?” — the handle a job manager (its first customer, nw.jobs) uses to dedup double-submits and to replay a resumed render for free. It is computed before execution and with <from N> placeholders intact, so it is stable across re-plans of the same structural request.

The digest canonicalizes each call over {app, args, tool} (falaw.canonical.plan_identity_payload()) — matching _synthetic_artifact()’s canonicalization, and deliberately not the per-call content-addressed cache key (falaw.cache._key(), which keys on {app, args} with no tool). plan_hash and the per-call cache key therefore key on different bytes and must not be assumed to agree call-for-call. Both projections live side by side in falaw.canonical with one shared byte-form (falaw.canonical.canonical_blob() — sorted keys, no default=str fallback, no NaN), so an argument the form cannot represent faithfully raises falaw.errors.FalNonCanonicalArgument instead of colliding, and a new identity-bearing field is an explicit decision about both hashes.

Two structurally-identical plans hash equal; changing any call’s app, args, or tool — or the order of calls — changes the hash.

Return type:

str

>>> a = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...              arguments={"prompt": "a tiger"}, output_kind="image")
>>> b = CallPlan(tool="image_to_video", application="fal-ai/svd",
...              arguments={"image_url": "<from 0>"}, output_kind="video")
>>> plan_hash(Plan(calls=(a, b))) == plan_hash(Plan(calls=(a, b)))
True
>>> plan_hash(Plan(calls=(a, b))) == plan_hash(Plan(calls=(b, a)))
False

backend (falaw#15) joins the hashed payload too, so a plan built for one backend never dedups against the structurally-identical plan for another — and, since it is included only when non-default, every plan made of today’s (all-"fal") calls hashes exactly as it did before:

>>> comfy = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...                   arguments={"prompt": "a tiger"}, output_kind="image",
...                   backend="comfyui")
>>> plan_hash(Plan(calls=(a,))) == plan_hash(Plan(calls=(comfy,)))
False
falaw.plan_image_to_video(image_url, prompt='', *, quality='high', model_id=None, duration_s=None, extra=None, metadata=None, consult_cache=True)[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).

Return type:

CallPlan

falaw.plan_lipsync(video_url, audio_url, *, quality='high', model_id=None, duration_s=None, extra=None, metadata=None, consult_cache=True)[source]#

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

Return type:

CallPlan

falaw.plan_llm_complete(prompt, *, system='', model=None, temperature=0.7, output_kind='text', input_tokens=None, max_output_tokens=None, llm_rates=None, extra=None, metadata=None, consult_cache=True)[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 a ceiling for the routed model, not the router’s flat price (falaw#50, option A). fal-ai/any-llm is one registry record routing thirty models at two published request tiers, so the record’s single per_call figure under-quotes every premium model tenfold — including this function’s own default. falaw.llm_ceiling_usd() prices the model argument instead:

  • no token hints → fal’s published per-request price for that model;

  • input_tokens and max_output_tokens → the greater of that price and the upstream per-token cost of a prompt that long capped at that many output tokens. Monotone in both hints;

  • a model the rate table does not price, or a token quote that cannot be bounded → estimated_cost_usd is None, which lights up falaw.plan.Plan.has_unknown_costs and forces approval. It is

    never quietly the router’s flat price.

input_tokens is what opts a call into a token quote. Given one, max_output_tokens defaults to whatever the caller already put under MAX_OUTPUT_TOKENS_ARGUMENT in extra — the cap is a fact of the call, not a second thing to remember. Without one, extra is left alone: capping your response is not asking to be quoted by tokens, and reading the cap as half a token quote would drop a call that has a perfectly good request price down to forced approval.

Both hints are estimator-only: they never enter arguments, so adding one to an existing call site changes the quote without moving the cache key or the plan hash.

llm_rates swaps in a caller’s own rate table — the seam for someone who has reconciled real numbers against their fal invoice.

Return type:

CallPlan

falaw.plan_text_to_speech(text, *, quality='balanced', voice=None, model_id=None, duration_s=None, tokens=None, extra=None, metadata=None, consult_cache=True)[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 and tokens are optional hints used only by the cost estimator — they never enter arguments, so they cannot move the cache identity. duration_s prices per_second records (the produced audio’s actual duration comes back on the materialized Artifact); tokens prices per_token records. For character-billed TTS (ElevenLabs — its registry record says so explicitly) pass tokens=len(text). Without the matching hint a quantity-priced record stays estimated_cost_usd=None — unknown, never free, which forces approval downstream.

Return type:

CallPlan

falaw.plan_to_dict(plan)[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.

Return type:

dict

falaw.prune_assets(*, older_than=None, max_bytes=None, dry_run=True, store=None)[source]#

Reclaim materialized asset copies — the cheapest disk in the cache (falaw#22).

assets/ holds a copy of each blob rather than a hard link, which is deliberate (falaw.content.write_blob_to_file() explains why a link would let a consumer corrupt the content store) but doubles the on-disk cost of every materialized asset. That makes this the prune to reach for first: while a blob survives, re-materializing its asset is a local copy and costs nothing, so rebillable_entries counts only the copies whose blob is also gone.

Parameters:
  • older_than (Union[float, int, timedelta, None]) – drop copies last written more than this ago — seconds, or a timedelta.

  • max_bytes (Optional[int]) – drop oldest-first until the area fits in this budget.

  • dry_run (bool) – report without deleting. Default, deliberately.

  • store – injected lacing.ArtifactStore, used only to ask whether each dropped copy still has a blob behind it; defaults to falaw.content.default_content_store().

Returns:

with area="assets".

Return type:

PruneReport

Raises:

ValueError – neither bound was given.

falaw.prune_content(*, older_than=None, max_bytes=None, dry_run=True, store=None)[source]#

Reclaim content-addressed blobs — the gigabytes (falaw#22).

This is the expensive prune. A blob is the last copy of an asset once fal has expired its URL (“expired files are permanently deleted and cannot be recovered”), so dropping one can turn a free cache hit into a re-rendered clip. The report says how many entries that applies to before you commit; read rebillable_entries.

Parameters:
  • older_than (Union[float, int, timedelta, None]) – drop blobs last written more than this ago — seconds, or a timedelta.

  • max_bytes (Optional[int]) – drop oldest-first until the area fits in this budget.

  • dry_run (bool) – report without deleting. Default, deliberately.

  • store – injected lacing.ArtifactStore; defaults to falaw.content.default_content_store(), whose deletes remove the file. An injected store keeps its own backend’s delete policy: a stock ArtifactStore.from_directory store moves each blob to the OS trash, which frees no space on that volume even though the report counts the bytes as freed.

Return type:

PruneReport

Blobs are listed and deleted through lacing’s contained iter_blobs/delete_blob (falaw#66): nothing outside the blob root is listed or removed, and an in-root symlink is unlinked, never its target.

Returns:

with area="content".

Return type:

PruneReport

Raises:

ValueError – neither bound was given — see _require_a_bound().

Both bounds may be combined; a blob selected by either is dropped.

>>> report = prune_content(older_than=timedelta(days=90))
>>> report.dry_run
True
falaw.prune_manifests(*, older_than=None, max_bytes=None, dry_run=True)[source]#

Reclaim cache entries — the fal responses themselves (falaw#22).

Manifests are kilobytes, so this is rarely where the disk is; it is here because a stale entry is its own problem — it pins a model version and a price you may no longer want served from cache.

Unlike prune_content(), the cost is unconditional: every dropped entry re-bills its call on the next run, so rebillable_entries always equals the candidate count.

Parameters:
  • older_than (Union[float, int, timedelta, None]) – drop entries stored more than this ago — seconds, or a timedelta. Read from the manifest’s own stored_at, falling back to file mtime.

  • max_bytes (Optional[int]) – drop oldest-first until the area fits in this budget.

  • dry_run (bool) – report without deleting. Default, deliberately.

Returns:

with area="manifests".

Return type:

PruneReport

Raises:

ValueError – neither bound was given.

Only the manifest is removed; blobs are shared by content hash across entries and are never dropped from here — the same rule falaw.drop_cache_entry() follows.

falaw.refresh_full_docs(*, docs_dir=None, max_workers=16, force=False, journal=True)[source]#

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

Return type:

dict

falaw.refresh_llm_rates(*, write=False, http_get_any_llm_doc=None, http_get_openrouter_models=None, rates_path=None, proposed_path=None, diff_path=None, today=None)[source]#

Fetch both sources, propose a fresh table, diff it — never overwrite.

rates_path (default: the committed llm_rates.json) is read only, never written: this function has no code path that touches the committed table, by design — a repriced or retiered model is a decision for a human to promote, not something a refresh job gets to make silently. With write=True, the freshly-derived table is written to proposed_path (default: llm_rates.proposed.json next to the committed file) and the diff report to diff_path (default: llm_rates.diff.txt).

Returns a summary dict: {"fetched_at", "committed_models", "proposed_models", "diff", "write", "proposed_path", "diff_path"}. The two path keys are None when write=False.

Return type:

dict

falaw.refresh_llms(*, docs_dir=None, journal=True)[source]#

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

Return type:

dict

falaw.refresh_model_prices(*, write=False, api_key=None, http_get=None, models_path=None, today=None)[source]#

Refresh models.json cost estimates from fal’s pricing API.

Returns a summary dict; write=False (default) reports what would change without touching the file. models_path and today exist for tests (the fetch date lands in each estimate’s notes).

Return type:

dict

falaw.refresh_models_from_corpus(*, path=None, write=False)[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.

Return type:

dict

falaw.refresh_state()[source]#

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

Return type:

dict

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

Decorator: register the wrapped function as a falaw tool.

Return type:

Callable

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

Remove the background from an image.

Return type:

Result

falaw.render_beat(beat, character, *, tts_quality='balanced', lipsync_quality='high', tts_model_id=None, avatar_model_id=None, force=False)[source]#

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

Parameters:
  • tts_model_id (Optional[str]) – 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 (Optional[str]) – Override the avatar/lipsync model (e.g. "fal-ai/bytedance/omnihuman/v1.5" to bypass the default ai-avatar which is known to hang).

Return type:

dict

falaw.render_scene(scene, *, tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, force=False, concurrency=1)[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).

Return type:

dict

falaw.render_shot(shot, *, environment=None, characters=(), style='', as_video=False, quality='balanced', image_model_id=None, image_to_video_model_id=None, force=False)[source]#

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

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

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

Return type:

dict

falaw.reprice_plan(plan, *, pricers={'llm_rates': Pricer(quote=<function _quote_from_llm_rates>, table='falaw/data/llm_rates.json', version=<functools._lru_cache_wrapper object>), 'model_catalogue': Pricer(quote = <function _quote_from_catalogue>, table='falaw/data/models.json', version=<functools._lru_cache_wrapper object>)})[source]#

Re-quote every call in plan against today’s rate tables.

Pure data: no network, no billing API, nothing executed. Returns a new RepricedPlan — the input plan is untouched.

A call carrying a falaw.CostBasis is re-quoted through the pricer its basis names, with the exact quantity hints that produced the original figure, and comes back with a basis re-stamped to today’s table version. A call carrying no basis is reported as "no_basis" and its cost is cleared to None: the frozen number is a fact about a past moment, and silently re-presenting it as a current quote is the failure this function exists to prevent. That clearing lights up falaw.Plan.has_unknown_costs, which is exactly right — nobody can say what that call costs today.

A basis is only re-quoted by a pricer reading the same table it names. A mismatch is "unknown", not a re-quote: two tables are two sets of books, and pricing a caller’s reconciled $0.50 row at falaw’s published $0.01 would be a 50x under-quote wearing the clothes of a price drop. Pass a Pricer naming that table to re-quote against the same books.

cache_status is carried through unchanged. Re-pricing answers “what would this cost?”, not “is it still cached?”; peeking the cache here would quietly turn a re-quote into an I/O operation and make a hit look like a price drop. Re-plan the calls if you want a fresh cache reading.

Parameters:
Return type:

RepricedPlan

>>> from falaw import Plan, CallPlan, reprice_plan
>>> stale = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...                  arguments={"prompt": "a tiger"}, output_kind="image",
...                  estimated_cost_usd=0.025)
>>> out = reprice_plan(Plan(calls=(stale,)))
>>> out.calls[0].status
'no_basis'
>>> out.plan.calls[0].estimated_cost_usd is None   # unknown, not $0.025
True
>>> out.plan.has_unknown_costs
True
falaw.scene_from_dict(d)[source]#

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

Return type:

Scene

falaw.storyboard_shot(shot, *, environment=None, characters=(), style='', quality='balanced')[source]#

Render a storyboard still for a Shot.

Return type:

Result

falaw.subscribe(callback)[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:
    ...
Return type:

Callable[[ProgressEvent], None]

falaw.talking_avatar_from_text(text, image_url, *, voice=None, prompt='', tts_quality='balanced', avatar_quality='balanced')[source]#

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

Return type:

Result

falaw.text_to_speech(text, *, quality='balanced', voice=None, model_id=None, extra=None)[source]#

Synthesize speech. voice semantics are model-specific.

Return type:

Result

falaw.text_to_video(prompt, *, quality='high', model_id=None, extra=None)[source]#

Generate a video from a text prompt.

Return type:

Result

falaw.unsubscribe(callback)[source]#

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

Return type:

None

falaw.upscale_image(image_url, *, scale=2.0, model_id=None, extra=None)[source]#

Upscale an image.

Return type:

Result

falaw.using_fal_credentials(key)[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: this is ocracy.kit.using_credentials(fal=key), backed by a contextvars.ContextVar, so the binding is visible only within the entering context (and asyncio tasks or copied contexts it spawns), never to concurrent requests.

Return type:

Iterator[None]

falaw.using_url_fetcher(fetcher)[source]#

Make fetcher falaw’s default asset transport for the duration.

The public seam for replacing falaw’s network transport wholesale — with an authenticated client, a retrying one, a local mirror, or (the common case) an in-memory fake in a test suite.

Prefer this to reaching for the module’s private default: it covers every entry point in one place, it needs no monkeypatch, and it cannot be invalidated by an internal rename. It is also the only mechanism that reaches a call falaw makes from a thread you did not create — see _DEFAULT_FETCHER for why that matters.

An explicitly passed fetcher= / asset_fetcher= still wins: this changes the default, never an explicit choice.

Nests and restores, so an inner block cannot leak over an outer one:

Return type:

Iterator[Callable[[str], Iterable[bytes]]]

>>> from lacing import ArtifactStore
>>> store = ArtifactStore.in_memory()
>>> before = default_url_fetcher()
>>> with using_url_fetcher(lambda url: [b"faked"]):
...     content_ref_for_url("https://fal.media/x.png", store=store).bytes_size
5
>>> default_url_fetcher() is before
True

For a ready-made fake with pinned bytes and 404s, see falaw.testing.

falaw.video_model_constraints()[source]#

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

Return type:

list[dict]

falaw.voice_clone(reference_audio_url, text, *, model_id=None, extra=None)[source]#

Generate speech in a cloned voice.

Return type:

Result

Modules

journal

Agent journal: append-only log of notes, issues and improvements.

account

Account health probe.

backends

Execution backend registry — where CallPlan.backend dispatches (falaw#15).

base

Core types: ToolSpec and ModelRecord.

bridges

Bridges: derive auxiliary surfaces (skill, MCP, HTTP service) from the tool registry.

cache

Content-addressed cache for fal calls.

canonical

One canonical byte-form for everything falaw hashes (falaw#17).

content

Content addressing for fal-produced media — identify a file by what it is.

core

Wrapper over fal_client.subscribe with progress events and auto-journaling.

corpus

Extract model records from misc/docs/llms-full.txt.

cost

Cost estimation: ModelRecord.cost_estimate + Scene rollups.

degrade

Where falaw sends "I carried on, but you should know" — one collection point.

errors

Typed exceptions for falaw operations.

events

Structured progress events for fal calls.

liveness

Is a catalogued endpoint still deployed? (falaw#76)

llm_prices

One first-party LLM price table: what a direct Anthropic / OpenAI call costs (falaw#81).

llm_rates

Per-routed-model LLM rates: the data behind a ceiling quote (falaw#50, option A).

llm_rates_refresh

Refresh proposals for the LLM rate table, never a silent overwrite (falaw#56).

local

Local utilities: ffmpeg + PIL glue for stitching fal outputs.

operations

Task-level verbs: stable, agent-friendly entry points.

outcomes

Per-call outcomes of running a falaw.Plan — the partial-result type.

plan

Plan / Execute primitives — separate planning (data) from execution (effects).

pricing

Read fal's pricing API into models.json cost estimates (falaw#18).

prune

Capacity management for the falaw cache: see what is on disk, and reclaim it.

refresh

Refresh fal.ai docs locally with conditional GETs.

registry

Tool and model registries.

reprice

Re-quote a persisted falaw.Plan at today's rates (falaw#60).

results

Result wrapper: parse fal responses into typed assets, lazy download.

scene

Scene IR: the editable structure that survives all the way to the pixels.

schema_export

Export the Python SSOT as committed JSON for the TypeScript twin (ts/).

session

Session: optional stateful controller over a sequence of falaw operations.

testing

Make a suite that uses falaw genuinely offline — the fake asset transport.