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
|
The row for |
|
Every row, or the rows of one |
|
|
The table's version, a digest of its bytes (see |
|
Rebuild a |
|
|
Convert a |
|
Per-call set of the call indices it references via |
Rebuild a |
|
|
Stable, plan-scoped structural idempotency key for a whole |
|
Convert a |
|
A |
Rebuild a |
|
|
Convert a |
|
A |
|
Re-quote every call in |
Drop all registered subscribers. |
|
|
Cost of one fal call against |
|
Estimate the USD cost of a full |
|
The row for |
|
Every row in the table, in file order — the catalogue view. |
|
Ceiling cost in USD of |
|
Register |
|
Remove a previously |
|
Animate a still face from audio. |
|
Use the LLM to apply a directorial note to a Beat. |
|
Apply a cross-cutting note: LLM proposes per-beat edits, we apply them. |
|
Hash everything that affects how the beat renders. |
|
Return the raw fal response if cached, else None. |
|
Persist a fal response. |
Quick summary of the cache: entry count, disk usage, and where it went. |
|
Disk usage of the falaw cache, broken down by area. |
|
|
Reclaim materialized asset copies — the cheapest disk in the cache (falaw#22). |
|
Reclaim content-addressed blobs — the gigabytes (falaw#22). |
|
Reclaim cache entries — the fal responses themselves (falaw#22). |
|
Delete the cache entry for |
|
Call a fal model, but reuse the cached response when present. |
|
Materialize |
The falaw-cache-rooted |
|
The transport used when no |
|
|
Make |
|
Call a fal model via fal_client.subscribe. |
|
Create a Character with a canonical face (and optional voice). |
The fal API key bound for the current context, or |
|
|
Attach or update the Voice on a Character. |
|
Place a character into an environment as one composited still. |
|
Edit an image with a natural-language instruction. |
|
Create an Environment with a canonical establishing image. |
|
Execute a Plan, returning a list of materialized :class:`lacing.Artifact`s. |
|
Execute a Plan with per-call failure isolation, returning a report. |
Yield ModelRecord-shaped dicts parsed from llms-full.txt. |
|
|
Generate an image from a text prompt. |
|
Generate a new image conditioned on one or more reference images. |
|
|
|
|
|
Animate a still image into a video. |
|
Yield |
|
Re-sync mouth motion in an existing video to a new audio track. |
|
|
|
|
|
Single-shot LLM completion. |
|
Single-shot LLM completion that also reports what it cost. |
|
|
|
|
|
Build a |
|
|
|
Download a remote asset to the cache and return the local path. |
|
Best-effort parser over the common fal response shapes. |
|
Convert prose screenplay text into a Scene IR via an LLM call. |
|
Pick a sensible fal model for a (category, quality) request. |
The capability/limit fields for a model — the "static reminder of limitations" a shot-list builder surfaces. |
|
|
Per-model parameters worth knowing before a call, |
|
|
|
Plan a |
Plan a |
|
|
Plan a |
|
Plan a |
|
Plan a |
|
Plan a |
|
Plan a |
|
Plan a |
|
Plan a |
|
Plan a |
|
Re-crawl per-page docs and rebuild |
|
Refresh |
|
Merge corpus-discovered models into models.json (additive). |
|
Refresh |
|
|
|
The ids from |
|
|
|
Fetch both sources, propose a fresh table, diff it — never overwrite. |
Return the saved per-source refresh state (etags, last fetch times). |
|
|
Decorator: register the wrapped function as a falaw tool. |
|
Remove the background from an image. |
|
Render one Beat to a lipsynced video. |
|
Render every shot and beat. |
|
Render a Shot as a still (default) or a short clip. |
|
|
Inverse of asdict: reconstruct a Scene from a plain dict. |
|
|
|
|
|
|
Render a storyboard still for a Shot. |
|
text + face image → talking video. |
|
Generate ambient/SFX/music from a prompt. |
|
Synthesize speech. |
|
Generate a video from a text prompt. |
|
Upscale an image. |
Bind |
|
|
Generate speech in a cloned voice. |
Classes
|
One row of the first-party table: a model's rates and where they came from. |
|
Outcome of |
|
A single piece of generated media. |
|
The atomic unit of a scene: who, says what, with what intent. |
|
What happened to one |
|
A single planned fal call. |
|
A reusable character: stable face, stable voice, stable style. |
|
A content-addressed handle on some bytes falaw has materialized. |
|
A reusable location/setting. |
|
The result of running a Plan: one |
|
One entry in the fal model catalog. |
|
An ordered sequence of |
|
The before/after for one call in a re-priced plan. |
|
How a |
|
One pricing rule, and the table that backs it. |
|
Result of |
|
A fal call result with parsed assets and the original raw response. |
|
Quantitative cost of one fal call against this model. |
|
One line item in a scene rollup. |
|
Result of |
|
One step in the lifecycle of a fal call. |
|
The whole editable structure: cast, locations, shots, beats. |
|
A working session for a sequence of falaw operations. |
|
A visual frame: framing + environment + characters in view. |
|
Single source of truth for a tool exposed by falaw. |
|
A character's voice spec. |
|
What one routed model costs, on every basis the table knows. |
|
Disk usage of one cache area. |
|
Where the falaw cache's disk is going, by area. |
|
One thing a prune would delete (or did). |
|
What a prune removed, or — with |
|
What one eager LLM call cost — the record the eager path never kept. |
Exceptions
|
fal account is locked / suspended / awaiting verification. |
|
The bytes behind a fal-served asset URL could not be retrieved. |
|
A real network call was attempted but the fal.ai client is not installed. |
|
Server rejected the request payload — typical 400 / 422. |
|
The requested duration is outside what the model can produce. |
Base for all falaw-raised exceptions. |
|
|
Wraps an HTTP error from fal.ai with the original status, body, and headers. |
|
Account balance is insufficient — typical 402. |
|
A model was queued but never returned — distinct from a network timeout. |
|
An argument cannot be canonicalised into falaw's hashed JSON form. |
|
The prompt exceeds the model's hard length cap (falaw#76). |
|
fal is throttling requests — typical 429. |
|
fal-side server error — typical 5xx. |
|
The fal call timed out before producing a result. |
|
Missing or invalid API credentials — typical 401. |
- class falaw.AccountStatus(ok, locked, unauthorized, status_code, detail, url, error)[source]#
Bases:
objectOutcome of
health_check().- error: str | None#
reprof the underlying exception whenokis False and the failure isn’t a recognized lock/unauth (e.g. network error).
- class falaw.AreaUsage(name, path, entries, bytes)[source]#
Bases:
objectDisk usage of one cache area.
entriescounts the area’s own unit — cache entries formanifests, blobs forcontent, files forassetsandurl_index— not files on disk, which is why it is reported next tobytesrather than derived from it.pathisNonewhen the area has no directory of its own. That is the case formanifests(scattered across two-character shard directories at the cache root) and forother. It is deliberately not the cache root: a caller reaching forrmtree(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:
objectA single piece of generated media.
Holds the URL plus minimal typed metadata.
downloadmaterializes 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 oftocontradicts the content, aUserWarningis issued; withfix_suffix=Truethe suffix is corrected instead (so the returned path may differ fromto).- Return type:
- class falaw.Beat(*, id, speaker='', line='', action='', emotion='', shot_id='', duration_s=None, notes='')[source]#
Bases:
objectThe atomic unit of a scene: who, says what, with what intent.
A Beat that has only
action(noline) is a non-verbal beat.
- class falaw.CacheUsage(root, areas)[source]#
Bases:
objectWhere the falaw cache’s disk is going, by area.
The whole point of the breakdown:
size_bytesalone cannot tell you whether you are looking at gigabytes of irreplaceable blobs or gigabytes ofassets/copies that cost nothing to regenerate.- property total_bytes: int#
Every byte under the cache root.
Equal to a full walk of the root, because
otherabsorbs 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:
objectWhat happened to one
CallPlan, at its position in the Plan.indexis the call’s position inplan.callsand is the identity a caller retries or re-plans by — a report always carries exactly one outcome per call, in plan order, soindexis also the safe key for zipping a Plan against anything the caller built alongside it.- 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.
- 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.
- 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:
objectA single planned fal call. Pure data — no API contact yet.
applicationandargumentsare the exact tuplecached_call_fal(application, arguments)would take, so a Plan can be cache-checked, executed, or replayed without ambiguity.- application: str#
The fal model id that will be invoked (e.g.
"fal-ai/flux/dev"). Backend-scoped: what it names depends onbackend.
- 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) — seefalaw.backends. Defaults tofalaw.canonical.DFLT_BACKEND("fal"), the only backend until a second one lands. Enters the per-call cache key andplan_hashonly when it is not the default, so every call falaw has ever planned or cached keeps its exact pre-#15 digest — seefalaw.canonical.
- property billable_cost_usd: float#
Cost that will actually be billed (0 on cache hit, estimate otherwise).
Returns
0.0(notNone) on cache hit or unknown estimate so sums are well-defined; useestimated_cost_usdis Noneto check unknown status explicitly.
- cache_status: Literal['hit', 'miss', 'stale', 'unknown']#
Whether the cache will short-circuit this call.
"hit"meansexecutewon’t bill, soPlan.total_cost_usdandPlan.cache_hit_savings_usdreflect that.
- cost_basis: CostBasis | None#
How
estimated_cost_usdwas computed, orNonewhen nothing recorded it (falaw#60). Present, the quote can be re-run against today’s rate table byfalaw.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 ofplan_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.
Nonewhen the model has nocost_estimatepopulated (callers can distinguish “free” from “unknown”).
- expected_duration_s: tuple[float, float] | None#
(min, max)duration the model can produce, orNoneif no duration contract is known. Plan-level validators can check that the requested duration fits this range and raiseFalDurationOutOfRangebefore the call instead of letting it silently truncate.
- 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 ANDplan_hash, under omit-if-empty (an empty dict leaves every existing key byte-identical). Unlikemetadata, 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 Transformimpl_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.
- class falaw.CallRepricing(*, index, call, old_cost_usd, new_cost_usd, status, reason='', basis_changed=False)[source]#
Bases:
objectThe 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
Falsefor"no_basis", which has nothing to compare against.
- call: CallPlan#
The re-priced call — the same call with today’s
estimated_cost_usdand a basis stamped with today’s table version.
- property delta_usd: float | None#
new - old, orNonewhen either side is unknown.Nonerather than0.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.
- class falaw.Character(*, name, description='', reference_image_url='', voice=None, style_notes='')[source]#
Bases:
objectA reusable character: stable face, stable voice, stable style.
- class falaw.ContentRef(content_hash, bytes_size)[source]#
Bases:
objectA content-addressed handle on some bytes falaw has materialized.
content_hashis the SHA-256 hex digest of the bytes — the valuelacing.Artifact.asset_idis 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:
objectHow a
CallPlan.estimated_cost_usdwas 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
applicationandargumentsalone: 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.idfor the catalogue pricer, the routedmodelargument for the LLM pricer. Distinct fromCallPlan.application, which forfal-ai/any-llmnames 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 ofmodels.json,"llm_rates"for a routed LLM priced out ofllm_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 wasNoneat plan time is left out rather than written asnull.
- class falaw.CostEstimate(*, kind, amount, currency='USD', notes='', source='approximate')[source]#
Bases:
objectQuantitative cost of one fal call against this model.
- kind#
How the price scales.
per_callis the simplest: constant per invocation regardless of size.per_secondapplies to video / TTS where output length matters.per_imagecovers batch image gen.per_megapixelcovers high-res image generation.per_tokencovers LLM-style endpoints.
- amount#
USD (or
currency) per unit defined bykind.
- 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:
objectOne line item in a scene rollup.
- class falaw.CostRollup(*, total_amount, currency='USD', lines=(), skipped=())[source]#
Bases:
objectResult of
estimate_scene_cost().
- class falaw.Environment(*, name, description='', reference_image_url='', time_of_day='', lighting='')[source]#
Bases:
objectA reusable location/setting.
- class falaw.ExecutionReport(outcomes=())[source]#
Bases:
objectThe result of running a Plan: one
CallOutcomeper 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 againstoutcomesand 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 offalaw.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
failedto 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 reasonestimated_spend_usdmust never be read on its own: an unpriced call contributes0.0to the sum, so a report reading$0.00means 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 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
outcomesfor anything positional.
- property succeeded: tuple[CallOutcome, ...]#
Outcomes that produced an artifact, in plan order.
- exception falaw.FalAccountLocked(message, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#
Bases:
FalHTTPErrorfal 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:
FalErrorThe bytes behind a fal-served asset URL could not be retrieved.
Raised by
falaw.contentwhen 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 whoseasset_idis not the SHA-256 of any bytes would breaklacing.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:
FalHTTPErrorServer rejected the request payload — typical 400 / 422.
- exception falaw.FalClientNotInstalled(what='calling fal.ai', *, missing='fal_client')[source]#
Bases:
FalError,ImportErrorA real network call was attempted but the fal.ai client is not installed.
pip install falawcarries no fal.ai client, so that callers that only build, price, hash and cache plans install none. Anything that actually reaches fal.ai needs thefalextra:pip install 'falaw[fal]'. It is anImportError, soexcept ImportErrorstill matches.
- exception falaw.FalDurationOutOfRange(message, *, model_id, requested, valid_range)[source]#
Bases:
FalErrorThe requested duration is outside what the model can produce.
Raised by Plan/Execute when the caller asks for
duration_sthat the model’s declaredexpected_duration_rangecannot satisfy. Callers can catch this to split the shot, repeat it, or pick a different model.
- exception falaw.FalHTTPError(message, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#
Bases:
FalErrorWraps 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:
FalHTTPErrorAccount balance is insufficient — typical 402.
- exception falaw.FalModelHung(message, *, model_id, elapsed_s)[source]#
Bases:
FalErrorA 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:
FalErrorAn argument cannot be canonicalised into falaw’s hashed JSON form.
Raised by
falaw.canonicalwhen 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).pathnames the offending node, e.g.arguments.extra.ref.
- exception falaw.FalPromptTooLong(message, *, model, limit, length)[source]#
Bases:
FalError,ValueErrorThe 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/lengthare in characters.
- exception falaw.FalRateLimited(message, *, retry_after_s=None, **kwargs)[source]#
Bases:
FalHTTPErrorfal is throttling requests — typical 429.
retry_after_sis parsed from theRetry-Afterheader if present, elseNone(caller decides backoff).
- exception falaw.FalServerError(message, *, status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)[source]#
Bases:
FalHTTPErrorfal-side server error — typical 5xx. Generally retryable.
- exception falaw.FalTimeout(message, *, elapsed_s, application=None)[source]#
Bases:
FalErrorThe 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:
FalHTTPErrorMissing 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:
objectOne 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:
objectWhat one routed model costs, on every basis the table knows.
- model#
The
modelargument 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_usdis 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_UNITprompt tokens, orNonewhen 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.
- 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:
objectWhat 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 wereNone, but because *nothing was recorded at all* (the field signature: real dollars spent besideplans: 0). A caller that books receipts usesllm_complete_with_receipt()and gets this alongside the text.Field semantics, honest by construction:
chars_in/chars_outare measured facts (prompt+system in, response text out).tokens_in/tokens_outare best-effort from the raw response’s usage block (OpenAIprompt_tokens/completion_tokensand Anthropicinput_tokens/output_tokensshapes).Nonemeans unrecorded by the provider response, never zero.estimated_cost_usdis0.0on 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 thefal-ai/any-llmrecord 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).Noneonly when nothing can price it at all.cost_sourcesays 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:
objectOne 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
promptlength, 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’sstyle, whose photoreal default silently overrides a woodcut prompt. Seefalaw.model_params().
- single_character_recommended: bool#
True when the model handles a single character per shot far better than multiple interacting ones — drives the “two characters, consider shot/reverse-shot” warning.
- class falaw.Plan(calls=())[source]#
Bases:
objectAn ordered sequence of
CallPlan— a render plan, in essence.Plans compose:
a + breturns a new Plan witha.callsfollowed byb.calls.Plan(calls=())is the identity. Plans are frozen, so edits return new Plans (usewith_call_replaced()for in-place-feel).- property cache_hit_savings_usd: float#
USD that would have been spent without the cache.
Equal to
sum(c.estimated_cost_usd for c in calls if c.cache_status == "hit" and c.estimated_cost_usd is not None).
- property has_unknown_costs: bool#
True if any non-cache-hit call has no cost estimate.
Use this to refuse to gate on a budget when the estimate is incomplete.
- property known_cost_usd: float#
The priced part of
total_cost_usd— same number today, but honest by construction.total_cost_usdcoerces unpriced calls to$0.00so 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 withunknown_call_count: the true cost isknown_cost_usdplus 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_usdacross all calls.
- property unknown_call_count: int#
How many billable calls carry no price at all.
The countable form of
has_unknown_costs— seeknown_cost_usdfor the budget-gate arithmetic it enables.
- class falaw.Pricer(*, quote, table, version=<function Pricer.<lambda>>)[source]#
Bases:
objectOne pricing rule, and the table that backs it.
The seam for a caller with their own numbers: build a
Pricerclosing over a reconciled rate table and passpricers={\*\*DFLT_PRICERS, LLM_RATES_PRICER: mine}toreprice_plan(). Thetable/versionpair is stamped onto the re-priced call’s basis, so the next audit can see which table produced which number.tableis also a gate, not just a label: a call whose basis names a different table is reported asunknownrather than re-quoted, so a row priced from your invoice is never silently re-quoted at falaw’s published rate (or the reverse). Give yourPricerthe sametablestring the basis carries —falaw.llm_rates.CUSTOM_LLM_RATES_TABLEfor anything aplan_*priced withllm_rates=.
- class falaw.ProgressEvent(*, kind, application, call_id, message='', pct=None, elapsed_s=0.0)[source]#
Bases:
objectOne 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_falinvocation share the samecall_id.
- message#
Free-form text. For
"log"events this is the log line; for"error"it’srepr(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:
objectOne thing a prune would delete (or did).
last_modifiedisNonewhen the age could not be determined — a non-filesystem blob backend, or a manifest too corrupt to read. Such a candidate is never selected byolder_than(an unprovable age is not evidence of staleness) and is evicted last undermax_bytes.
- class falaw.PruneReport(area, dry_run, candidates=(), deleted=(), kept_entries=0, kept_bytes=0, rebillable_entries=0, unreferenced_candidates=0, errors=())[source]#
Bases:
objectWhat a prune removed, or — with
dry_run=True— would remove.rebillable_entriesis 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 theurl -> hashindex) 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 predicatefalaw.plan.execute()applies to a hit from the built-in converter. It is therefore an upper bound, not an exact population: with a customartifact_converter=the entry is never dropped (it returns a broken artifact instead), and withfetch_bytes=Falsedropping 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_candidatesis the other half of the story, and onlycontentpopulates 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-renderedfile://media in the same store, and those never had a fal response behind them. Counting them asrebillablewould 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, notcandidates. 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.
- class falaw.RepricedPlan(*, plan, calls=())[source]#
Bases:
objectResult of
reprice_plan()— the new Plan plus the per-call diff.Read
planfor the re-quoted plan (it is an ordinaryfalaw.Plan, soknown_cost_usd/unknown_call_count/has_unknown_costsmean what they always mean), andcallsfor 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_usdover 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_hashis 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:
objectA 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:
objectThe whole editable structure: cast, locations, shots, beats.
- class falaw.Session(output_dir=<factory>, journal=<factory>, history=<factory>)[source]#
Bases:
objectA 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:
objectA 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:
objectSingle 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:
objectA character’s voice spec.
Three modes, choose any:
voice_id— model-side voice id (e.g. ElevenLabs voice).reference_audio_url— a few seconds of audio to clone.model_id— override the default TTS model for this voice.
Always provide
namefor stable, human-readable referencing.
- falaw.animate_face(image_url, audio_url, *, prompt='', quality='balanced', model_id=None, extra=None)[source]#
Animate a still face from audio. Image + audio → talking video.
- Return type:
- 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:
- 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:
- 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:
- falaw.cache_get(application, arguments, *, backend='fal', key_extra=None)[source]#
Return the raw fal response if cached, else None.
- 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 producedraw(falaw#15). Joins the cache key only when non-default, same rule asfalaw.canonical.cache_key_payload(); recorded in the manifest under the same condition, for debugging.
- Return type:
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 sinceexecute_plan(concurrency=N): two calls of one Plan that are structurally identical land on the same key, and an interleavedjson.dumpwould 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.
areasis 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 ofassets/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 infalaw.prune.size_bytesis every byte under the cache root, unchanged in meaning from before the breakdown existed: theotherarea 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_prunepins this against a throwaway cache instead.)- Return type:
>>> 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:
- 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 toarguments. 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 syntheticcache_hitevent is emitted so UIs can show “skipped” instead of “running”.backend (
str) – which execution backend serves this call (falaw#15) — resolved viafalaw.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:
- 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 (useon_eventfor structured access).on_event (
Optional[Callable[[ProgressEvent],None]]) – Per-call subscriber forProgressEvent`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. WhenNone(default) the key bound viausing_fal_credentials()is used; when that is also unset, the fal SDK’s ownFAL_KEYenv-var lookup applies (the historical behaviour). A resolved key is used per-call via a dedicatedfal_client.SyncClient— it is never written to a global or an env var.
- Return type:
- Returns:
The raw response dict from the model.
- falaw.call_plan_from_dict(d)[source]#
Rebuild a
CallPlanfrom acall_plan_to_dict()dict.arguments/metadataare copied (a deserialized plan owns its own data);expected_duration_sis re-tupled.backenddefaults toDFLT_BACKENDwhen absent — a dict written before falaw#15 names no backend, and it always meant"fal".cost_basisis absent on any plan written before falaw#60 and on any call nothing stamped; it comes back asNone, whichfalaw.reprice_plan()reports as"no_basis"— never as a quote that is still current.- Return type:
- falaw.call_plan_to_dict(call)[source]#
Convert a
CallPlanto a plain JSON-serializable dict.The inverse of
call_plan_from_dict().expected_duration_s(atuple) becomes a 2-element list since JSON has no tuple type; everything else is already JSON-native.backend(falaw#15) is a tolerated-default addition, not aPLAN_DICT_SCHEMAbump: it is always written, butcall_plan_from_dict()defaults it toDFLT_BACKENDwhen 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:
- 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_urlis given, we skip face generation and use that image directly. Otherwise we run text-to-image with the description (+ optionalstylesuffix), cache the result, and use the URL.- Return type:
- falaw.cast_voice(character, *, voice_id='', reference_audio_url='', style_notes='', model_id='')[source]#
Attach or update the Voice on a Character.
- Return type:
- falaw.catalogue_cost_basis(model_id, **quantities)[source]#
A
falaw.CostBasisfor a call priced frommodels.json.quantitiesarefalaw.estimate_call_cost()keywords (count,seconds,megapixels,tokens);Nonevalues are dropped, so a hint that was never supplied is recorded as absent rather than asnull— the same thing the estimator saw.- Return type:
>>> catalogue_cost_basis("fal-ai/flux/dev", seconds=None).quantities {}
- falaw.check_model_liveness(ids=None, *, status_of=None)[source]#
{endpoint_id: is_live}forids(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.
- 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). Passmodel_idto override (e.g."fal-ai/flux-pro/kontext/max"for highest quality, or"fal-ai/bytedance/seededit/v3/edit-image"for SeedEdit).- Return type:
- falaw.content_ref_for_url(url, *, store=None, fetcher=None, refresh=False, assume_immutable=None)[source]#
Materialize
url’s bytes intostoreand 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):
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.Mutable URL with a remembered hash: falaw revalidates — a conditional
GETreplaying the recordedETag/Last-Modified(forfile://, a(mtime, size)comparison). A304costs one round-trip and no payload; a200means the bytes really changed and the new ones are stored, so the content hash changes with them.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 todefault_content_store().fetcher (
Optional[Callable[[str],Iterable[bytes]]]) – Injected byte source; defaults todefault_url_fetcher()(the built-inurllibtransport unlessusing_url_fetcher()has installed another). To support revalidation, a custom transport exposes aconditional_fetch(url, validators) -> ConditionalOutcomeattribute; 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.Truerestores the old unconditional trust (use for a host you mint yourself, and prefer adding it toIMMUTABLE_URL_HOSTS);Falseforces 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:
- falaw.cost_basis_from_dict(d)[source]#
Rebuild a
CostBasisfrom acost_basis_to_dict()dict.- Return type:
- falaw.cost_basis_to_dict(basis)[source]#
Convert a
CostBasisto a plain JSON-serializable dict.table/table_versionare 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:
- falaw.current_fal_key()[source]#
The fal API key bound for the current context, or
None.Resolution order callers should mirror: an explicit
api_keyargument tocall_fal()wins over this context value, which in turn wins over the fal SDK’s ownFAL_KEYenv-var lookup.
- falaw.dead_models(ids=None, *, status_of=None)[source]#
The ids from
check_model_liveness()that are no longer deployed.
- 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_DIRlike 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 backendfrom_directorylays 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 isfalaw.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-inurllib-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()andfalaw.execute_plan_isolated()— honours an override installed after they were imported.
- 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, andexecute_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:
- falaw.edit_image(image_url, prompt, *, quality='balanced', model_id=None, extra=None)[source]#
Edit an image with a natural-language instruction.
- Return type:
- falaw.establish_environment(name, description, *, time_of_day='', lighting='', image_url='', quality='high')[source]#
Create an Environment with a canonical establishing image.
- Return type:
- falaw.estimate_call_cost(record, *, count=1, seconds=None, megapixels=None, tokens=None)[source]#
Cost of one fal call against
record.Returns
Nonewhen the cost is unknown, so callers can distinguish “free” from “we can’t say”. That happens when the record carries nocost_estimateat 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.0for a missing quantity would be actively dangerous: aper_secondclip 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.Noneinstead propagates toCallPlan.estimated_cost_usdand lights upPlan.has_unknown_costs, which exists for exactly this case. Callers that know the quantity should pass it;per_secondcallers can fall back torecord.max_clip_secondsfor an upper bound.per_megapixelis deliberately different: an image’s pixel budget has a sane house default (below), so it stays priceable.
- 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_modelsemantics the renderer uses, then sums per-call costs. Returns a structuredCostRollupwith per-line breakdowns, plus a list of “skipped” entries the caller should surface (typically: a model with nocost_estimatepopulated).shot_secondsis the assumed clip length used to priceshots_as_video. AShotcarries 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 inCostRollup.skippedrather than silently priced at $0.00. Pass the length you expect (the beat path has the same knob asestimated_seconds).- Return type:
- 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). Useexecute_isolated()when one bad call must not discard the rest of a fan-out; it returns anExecutionReportwith 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_idandurl=None. Useful for exercising downstream composition without an API key.use_cache (bool) – When True (default), executes via
cached_call_falso 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, leaveuse_cache=Trueand passrefresh.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 aforce/ “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. WhenNone(default), a built-in converter handles the common shapes ({images: [{url}]},{video: {url}},{audio: {url}}). Mutually exclusive withcontent_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 owncost_usd: the executor stamps it from the observed run outcome after conversion, overwriting whatever the converter set (falaw#26).content_store – Injected
lacing.ArtifactStorethat media bytes are materialized into. Defaults tofalaw.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_idis the SHA-256 of its bytes. Defaults toDFLT_FETCH_BYTES(true), overridable process-wide viaFETCH_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 meansasset_idis not a content hash, in violation oflacing.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 tofalaw.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 onfalaw.content.using_url_fetcher()— because a suite reaching falaw through its own public API has noexecutecall site to pass this to. Passing it here still wins over any installed default. ($FALAW_FETCH_ARTIFACT_BYTES=0also silences the network, but by turning content addressing off — seeFETCH_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 callN. 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), soconcurrencymultiplies the peak.
- Return type:
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:
urlis kept,bytes_sizestays 0,asset_idis a digest of the response and is not claimed to be a content hash, and aUserWarningis emitted. Downstream key resolution readsbytes_size == 0and 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 integerN) 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.ArtifactperCallPlaninplan.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(). Whereexecuteraises on the first failure — discarding every artifact produced before it, each of which fal has already billed — this returns anExecutionReportcarrying oneCallOutcomeper 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 againstreport.outcomesand stay aligned.- Parameters:
halt_on_failure (bool) – When True, stop submitting work as soon as any call fails; everything not yet started is reported
blockedwith a run-level reason. This is whatexecute()uses, and atconcurrency=1it reproduces the historical sequential behaviour exactly. Note that atconcurrency > 1calls 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#
failedandblockedare 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 callNdid 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.
- 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_sizeids (the endpoint caps atMAX_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).
- 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:
- 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_sizeis a fal preset (landscape_16_9…); models that size byaspect_ratio(nano-banana-pro) get the equivalent ratio, withextra={"resolution": "2K"}for the pixel tier. A prompt over the model’s cap raisesfalaw.FalPromptTooLongbefore any call. Checkfalaw.model_params()for enums whose default may not be what you want (Recraftstyledefaults to photoreal).- Return type:
- 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 theimage_editcategory (Flux Kontext et al.) and threads the references asimage_url(first)image_urls(all) — the same wire shape image-edit models understand.
Pass
model_idto override the picked model.- Return type:
- falaw.get_llm_price(provider, model, *, prices=None)[source]#
The row for
model(exact id), orNonewhen the table does not list it.
- falaw.get_llm_rate(model, *, rates=None)[source]#
The row for
model, orNonewhen the table does not price it.Noneis 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.
- falaw.image_to_video(image_url, prompt='', *, quality='high', model_id=None, extra=None)[source]#
Animate a still image into a video.
- Return type:
- 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"}. Withconcurrency=1results arrive in submission order (shots before beats). Withconcurrency > 1they arrive in completion order (use the"shot_id"/"beat_id"keys to re-key by identity).Cache hits are immediate: a fully-cached scene yields all results in close succession even at
concurrency=1.
- falaw.lipsync(video_url, 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:
- falaw.list_llm_prices(provider=None, *, prices=None)[source]#
Every row, or the rows of one
provider.
- falaw.list_llm_rates(*, rates=None)[source]#
Every row in the table, in file order — the catalogue view.
- falaw.llm_call_cost_usd(provider, model, usage, *, prices=None)[source]#
(USD, known)for one first-party call from the vendor’s reportedusage.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.
knownisFalsewhen 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 –
provideris not one ofPROVIDERS.- Return type:
- falaw.llm_ceiling_usd(model, *, input_tokens=None, max_output_tokens=None, count=1, rates=None)[source]#
Ceiling cost in USD of
countcalls routingmodel, orNone.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_tokensin plusmax_output_tokensout 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_tokenscaps 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) – Themodelargument the call will send to the router.input_tokens (
Optional[int]) – Upper bound on prompt tokens.Nonetogether withmax_output_tokensselects the per-request basis alone.max_output_tokens (
Optional[int]) – The response cap (any-llm’smax_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
countthat is not a plainint. Abooland afloatboth do the arithmetic silently —Trueprices one token,1.5e4prices 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:
- 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:
- 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’scache_hitevent, the response’s usage block, the registry’s estimate). Library surface only — deliberately NOT a registered tool: the tool surface keeps the one string-returningllm_complete.- Return type:
- falaw.llm_cost_basis(routed_model, *, input_tokens=None, max_output_tokens=None, custom_rates=False)[source]#
A
falaw.CostBasisfor a routed LLM call priced from the rate table.custom_rates=Truerecords that the quote came from a caller-suppliedrates=table rather than the committed one: there is no file to digest, so the version stays empty and the table names itselffalaw.llm_rates.CUSTOM_LLM_RATES_TABLE.reprice_plan()then refuses to re-quote it with the committed table, reportingunknown— 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 aPricerover your table that names that same identity.- Return type:
- falaw.llm_prices_table_version()[source]#
The table’s version, a digest of its bytes (see
falaw.base.data_table_version()).- Return type:
- 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
CallPlanand (optionally) check the cache.When
consult_cache=True(the default), the cache is peeked using the same key the eventual call would produce;cache_statusis set to"hit"if a cached entry exists,"miss"otherwise. This makesPlan.total_cost_usdhonest: a fully-cached Plan reports $0.When
consult_cache=False(e.g. for unit tests or “what would a fresh run cost?” reporting),cache_statusis"unknown".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’scache_hit_savings_usdon every chained call, which under prepaid billing (a quote that may be deducted) is a billing bug.cache_statusis"unknown"here instead, exactly the caseCacheStatusdocuments it for.cost_basisrecords howestimated_cost_usdwas arrived at so the quote can be re-run later (falaw#60). It is descriptive only: it never reaches the cache key orplan_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:
- 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:
the file is already on disk here (no store lookup, no network) — immutable URLs only, see below;
the bytes are in the content store (no network, or one validating round-trip) — so it still works after fal has expired the URL;
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 theurl -> hashindex 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, wherefalaw.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 tofalaw.content.default_content_store().fetcher – injected byte source (
url -> Iterable[bytes]); defaults to theurllib-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:
- 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_secondsetc. areNone/ empty when unknown for that model.- Return type:
- falaw.model_params(id)[source]#
Per-model parameters worth knowing before a call,
{name: spec}.Each spec is
{"enum": [...], "default": ...}; resolves aliases. Forfal-ai/recraft/v3/text-to-imagethis showsstyledefaults torealistic_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:
- 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
rawonly — callers can readresult.rawfor anything we miss.- Return type:
- 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:
- 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:
- 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 setquality="high"(which picks omnihuman).- Return type:
- 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:
- 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
frozensetper call, in plan order, sodeps[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 whatexecute_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 integerN; anNoutside the plan; or anNthat 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:
>>> 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. ...
- 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:
- falaw.plan_from_dict(d)[source]#
Rebuild a
Planfrom aplan_to_dict()dict.Raises
ValueErrorifdcarries an unrecognizedschematag — a plan written by an incompatible future version should fail loudly, not silently lose calls. A missingschemais tolerated (treated as v1) so hand-written plans stay easy.- Return type:
- 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.kindselects the default model (seeGENERATE_AUDIO_DEFAULTSfor why these are explicit ids).duration_sreaches the model as its integerdurationargument where the model takes one (mmaudio does) AND feeds the cost estimate; for models with no duration argument it is estimator-only.- Return type:
- 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:
- 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_editcategory (Flux Kontext et al.) and threads the references asimage_url(first) +image_urls(all), the same wire shape the eager op uses — so a planned and an eager call with identical inputs collapse to one cache entry.- Return type:
- 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 notool).plan_hashand 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 infalaw.canonicalwith one shared byte-form (falaw.canonical.canonical_blob()— sorted keys, nodefault=strfallback, no NaN), so an argument the form cannot represent faithfully raisesfalaw.errors.FalNonCanonicalArgumentinstead 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, ortool— or the order of calls — changes the hash.- Return type:
>>> 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_sis used only for cost estimation when the model is pricedper_second; it does not get passed to fal unless the caller puts it inextra(different models have different argument names).- Return type:
- 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:
- 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-llmwith the exact same application id and argument shape as the eagerfalaw.llm_complete(), so a planned call and an eager call with identical inputs collapse to the same cache entry.output_kindis"text"for a free-form completion or"json"when the prompt asks for a strict-JSON response — it tellsfalaw.execute()what kind oflacing.Artifactto materialize. Either way the LLM response is materialized to a content-addressed cache file (Artifact.path), because LLM output is text, not a URL.Cost is a ceiling for the routed model, not the router’s flat price (falaw#50, option A).
fal-ai/any-llmis one registry record routing thirty models at two published request tiers, so the record’s singleper_callfigure under-quotes every premium model tenfold — including this function’s own default.falaw.llm_ceiling_usd()prices themodelargument instead:no token hints → fal’s published per-request price for that model;
input_tokensandmax_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_usdisNone, which lights upfalaw.plan.Plan.has_unknown_costsand forces approval. It isnever quietly the router’s flat price.
input_tokensis what opts a call into a token quote. Given one,max_output_tokensdefaults to whatever the caller already put underMAX_OUTPUT_TOKENS_ARGUMENTinextra— the cap is a fact of the call, not a second thing to remember. Without one,extrais 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_ratesswaps in a caller’s own rate table — the seam for someone who has reconciled real numbers against their fal invoice.- Return type:
- 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.voicesemantics are model-specific.duration_sandtokensare optional hints used only by the cost estimator — they never enterarguments, so they cannot move the cache identity.duration_spricesper_secondrecords (the produced audio’s actual duration comes back on the materialized Artifact);tokenspricesper_tokenrecords. For character-billed TTS (ElevenLabs — its registry record says so explicitly) passtokens=len(text). Without the matching hint a quantity-priced record staysestimated_cost_usd=None— unknown, never free, which forces approval downstream.- Return type:
- falaw.plan_to_dict(plan)[source]#
Convert a
Planto a plain JSON-serializable dict.The result round-trips through
plan_from_dict(). This is the substrate primitive a consumer (a persistence layer, an MCP transport, a plan-diff tool) builds on — falaw owns the wire shape of its own Plan so every consumer agrees on it. Carries aschematag (PLAN_DICT_SCHEMA) so a future breaking change is detectable.- Return type:
- 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, sorebillable_entriescounts 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 atimedelta.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 tofalaw.content.default_content_store().
- Returns:
with
area="assets".- Return type:
- 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 atimedelta.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 tofalaw.content.default_content_store(), whose deletes remove the file. An injected store keeps its own backend’s delete policy: a stockArtifactStore.from_directorystore 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:
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:
- 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, sorebillable_entriesalways equals the candidate count.- Parameters:
older_than (
Union[float,int,timedelta,None]) – drop entries stored more than this ago — seconds, or atimedelta. Read from the manifest’s ownstored_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:
- 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:
- 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 committedllm_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. Withwrite=True, the freshly-derived table is written toproposed_path(default:llm_rates.proposed.jsonnext to the committed file) and the diff report todiff_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 areNonewhenwrite=False.- Return type:
- falaw.refresh_llms(*, docs_dir=None, journal=True)[source]#
Refresh
llms.txtandllms-full.txt; return a summary dict.- Return type:
- falaw.refresh_model_prices(*, write=False, api_key=None, http_get=None, models_path=None, today=None)[source]#
Refresh
models.jsoncost estimates from fal’s pricing API.Returns a summary dict;
write=False(default) reports what would change without touching the file.models_pathandtodayexist for tests (the fetch date lands in each estimate’snotes).- Return type:
- 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. Settingwrite=False(the default) reports what would change without touching the file.- Return type:
- falaw.refresh_state()[source]#
Return the saved per-source refresh state (etags, last fetch times).
- Return type:
- falaw.register_tool(**spec_kwargs)[source]#
Decorator: register the wrapped function as a falaw tool.
- Return type:
>>> @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:
- 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 overtts_quality-basedpick_model. Use this to force a specific TTS engine for one beat (e.g. eleven-v3 for emotional delivery, multilingual-v2 for consistency).avatar_model_id (
Optional[str]) – Override the avatar/lipsync model (e.g."fal-ai/bytedance/omnihuman/v1.5"to bypass the defaultai-avatarwhich is known to hang).
- Return type:
- 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.
concurrencycontrols how many shots/beats run in parallel against fal. The work is HTTP-bound, so a thread pool is enough. Default1preserves serial behavior. Useiter_render_scene()instead if you want results yielded as each unit completes (for live UI updates).- Return type:
- 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:
- Return type:
- 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
planagainst 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.CostBasisis 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 toNone: 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 upfalaw.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 aPricernaming that table to re-quote against the same books.cache_statusis 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:
plan (
Plan) – The plan to re-quote, typically just deserialized withfalaw.plan_from_dict().pricers (
Mapping[str,Pricer]) – Pricing rules byfalaw.CostBasis.pricername. The seam for a caller who has reconciled real numbers against their own invoice — seePricer.
- Return type:
>>> 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:
- falaw.storyboard_shot(shot, *, environment=None, characters=(), style='', quality='balanced')[source]#
Render a storyboard still for a Shot.
- Return type:
- falaw.subscribe(callback)[source]#
Register
callbackto receive every emitted ProgressEvent.Returns the callback unchanged so it can be used as a decorator:
@subscribe def log_to_file(ev: ProgressEvent) -> None: ...
- Return type:
- 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:
- falaw.text_to_speech(text, *, quality='balanced', voice=None, model_id=None, extra=None)[source]#
Synthesize speech.
voicesemantics are model-specific.- Return type:
- falaw.text_to_video(prompt, *, quality='high', model_id=None, extra=None)[source]#
Generate a video from a text prompt.
- Return type:
- falaw.unsubscribe(callback)[source]#
Remove a previously
subscribe()’d callback. No-op if absent.- Return type:
- falaw.upscale_image(image_url, *, scale=2.0, model_id=None, extra=None)[source]#
Upscale an image.
- Return type:
- falaw.using_fal_credentials(key)[source]#
Bind
keyas the fal credential for everycall_fal()in this context.Intended for server-side bring-your-own-key flows: wrap a unit of work that will make one or more fal calls, and they all authenticate with
keyinstead of the server’sFAL_KEYenv var — without any intermediate function needing a credential parameter.A falsy
keyis a deliberate no-op (the context is left untouched), so a caller can pass an optional header value straight through without special-casing “no BYO key — fall back to the server/env key”.Thread/async safe: this is
ocracy.kit.using_credentials(fal=key), backed by acontextvars.ContextVar, so the binding is visible only within the entering context (and asyncio tasks or copied contexts it spawns), never to concurrent requests.
- falaw.using_url_fetcher(fetcher)[source]#
Make
fetcherfalaw’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_FETCHERfor 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:
>>> 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_constraintsfor every video model in the catalog — the data a shot-list builder shows as its model-limits reference.
- falaw.voice_clone(reference_audio_url, text, *, model_id=None, extra=None)[source]#
Generate speech in a cloned voice.
- Return type:
Modules
Agent journal: append-only log of notes, issues and improvements. |
|
Account health probe. |
|
Execution backend registry — where |
|
Core types: ToolSpec and ModelRecord. |
|
Bridges: derive auxiliary surfaces (skill, MCP, HTTP service) from the tool registry. |
|
Content-addressed cache for fal calls. |
|
One canonical byte-form for everything falaw hashes (falaw#17). |
|
Content addressing for fal-produced media — identify a file by what it is. |
|
Wrapper over |
|
Extract model records from |
|
Cost estimation: ModelRecord.cost_estimate + Scene rollups. |
|
Where falaw sends "I carried on, but you should know" — one collection point. |
|
Typed exceptions for falaw operations. |
|
Structured progress events for fal calls. |
|
Is a catalogued endpoint still deployed? (falaw#76) |
|
One first-party LLM price table: what a direct Anthropic / OpenAI call costs (falaw#81). |
|
Per-routed-model LLM rates: the data behind a ceiling quote (falaw#50, option A). |
|
Refresh proposals for the LLM rate table, never a silent overwrite (falaw#56). |
|
Local utilities: ffmpeg + PIL glue for stitching fal outputs. |
|
Task-level verbs: stable, agent-friendly entry points. |
|
Per-call outcomes of running a |
|
Plan / Execute primitives — separate planning (data) from execution (effects). |
|
Read fal's pricing API into |
|
Capacity management for the falaw cache: see what is on disk, and reclaim it. |
|
Refresh fal.ai docs locally with conditional GETs. |
|
Tool and model registries. |
|
Re-quote a persisted |
|
Result wrapper: parse fal responses into typed assets, lazy download. |
|
Scene IR: the editable structure that survives all the way to the pixels. |
|
Export the Python SSOT as committed JSON for the TypeScript twin ( |
|
Session: optional stateful controller over a sequence of falaw operations. |
|
Make a suite that uses falaw genuinely offline — the fake asset transport. |