foley.agent#
The SELECT stage — the search-agent that finds the right sound for a context (#7).
The headline capability: a narrative context → verified, license-clean sound candidates, and the sparse plan that places them. The loop (report 05 §5):
- decompose_context → (per event) refine_query / search_sounds
→ gate_candidates (fail-closed license gate, FIRST) → verify_match (clap → listen → judge ladder) → decide (retrieve-vs-generate, the single branch) → place_in_timeline → a sparse SoundDesignTimeline
Design discipline (mirrors foley.index): this package is dol-only at import —
the LLM rungs sit behind the Decomposer / Judge / Refiner
protocols with deterministic fakes as defaults, and anthropic (the foley[agent]
extra) is imported lazily inside the real impls only. So foley.find("a paragraph")
works out of the box and the whole loop is exercisable in CI with no network / key /
heavy dependency. Every find() opens one foley.obs run scope, so search +
verify + generate aggregate into a single reproducible run-manifest.
Functions
|
The headline: a narrative context → verified, license-clean sound candidates. |
|
Fold verified candidates into the SPARSE |
|
Decompose a passage into |
|
Expand |
|
Hybrid search for one query, or a multi-query RRF-merge (the SELECT retrieval tool). |
|
Verify |
|
Fail-closed license gate — run BEFORE verification/ranking (invariant #3). |
|
The single generate-vs-retrieve branch — a PURE function (report 05 §4). |
|
Generate a clip for |
|
Emit the SPARSE per-item plan subset ( |
Classes
|
Narrative context → a sparse, salience-ranked, diegetic-tagged event list. |
|
One rung of the verify ladder: does this candidate match this event? (report 10 §4.2). |
|
One event query → 2–4 paraphrases/expansions (query-expansion for retrieval). |
|
Bounded-cost accounting: per-event loop counts, and a cumulative dollar cap (#57). |
|
The tiny result of |
|
What |
Deterministic cue-lexicon decomposer — the zero-dependency default and CI fake. |
|
|
LLM-backed decomposer ( |
Deterministic template-expansion refiner — the default and CI fake. |
|
|
LLM-backed refiner ( |
The zero-config |
|
|
Deterministic stand-in for the |
|
LLM arbiter for the |
|
The audio-LM |
- class foley.agent.AnthropicDecomposer(*, client=None, model='claude-opus-4-8', max_tokens=2000)[source]#
Bases:
objectLLM-backed decomposer (
foley[agent]): Claude → a structured event list.anthropicis imported lazily insidedecompose()soimport foleystays dol-only. Stashes the returnedMessageonself.last_responsesodecompose_context()can record the GenAI span (token usage / model / stop_reason). Model, thinking, and structured-output shape follow theclaude-apiconventions (claude-opus-4-8, adaptive thinking — neverbudget_tokens).
- class foley.agent.AnthropicJudge(*, client=None, model='claude-opus-4-8', max_tokens=500)[source]#
Bases:
objectLLM arbiter for the
judgerung (foley[agent]): Claude → aVerdict.anthropicimported lazily insidejudge(). Stashes theMessageonself.last_responsefor the GenAI span.
- class foley.agent.AnthropicRefiner(*, client=None, model='claude-opus-4-8', max_tokens=500)[source]#
Bases:
objectLLM-backed refiner (
foley[agent]): Claude → a paraphrase list.anthropicimported lazily insiderefine(). Stashes theMessageonself.last_responsefor the GenAI span.
- class foley.agent.AudioLMJudge(*, pipeline=None, model='Qwen/Qwen2-Audio-7B-Instruct', max_tokens=300, tau=0.5)[source]#
Bases:
objectThe audio-LM
listenrung (foley[fit]): Qwen2-Audio “does this contain {event}?”.transformers/torchare imported lazily insidejudge(); an injectedpipeline(the test seam, mirroring the Stable-Audio fake-pipeline seam) drives the whole byte-decode → prompt → P(yes) →Verdictpath with no torch. Stashesself.model+self.last_responsesoverify_match’s GenAI span stays informative (same getattr contractAnthropicJudgeuses).
- class foley.agent.Budget(max_refine_loops=1, max_generations=1, allow_generate=True, max_usd=1.0, approve_unknown_cost=None, _refines=0, _gens=0, _spent_usd=0.0, _unknown_calls=0, _lock=<factory>)[source]#
Bases:
objectBounded-cost accounting: per-event loop counts, and a cumulative dollar cap (#57).
Two kinds of bound:
Per event —
max_refine_loops/max_generations: the loop callsrefine_ok()/gen_ok()to test, thenspend_refine()/spend_gen()to charge;reset()zeroes them at each event.Per run, cumulative —
max_usd(default $1): every paid call (generation, paid LLM rung) is checked withauthorize()before it is made and recorded withcharge()after.reset()never clears it, so the cap holds across the wholefind/scorerun. A call whose cost is unknown (estimateNone) is refused unlessapprove_unknown_cost(or$FOLEY_APPROVE_UNKNOWN_COST=1).
- authorize(estimate_usd, *, what)[source]#
Alias of
check()(kept for callers that only test affordability).- Return type:
- check(estimate_usd, *, what)[source]#
Raise if this run cannot afford
estimate_usd(nothing is reserved).- Return type:
- reserve(estimate_usd, *, what)[source]#
Check, then count
estimate_usdas spent at once (atomic under a lock).Reserving before the call is what keeps concurrent calls on one budget (MCP tools run in a threadpool) from all passing the check before any is charged.
settle()later replaces the reservation with the actual cost, if known.- Return type:
- reset()[source]#
Zero the spend counters so the caps apply per event, not per passage.
The
findloop calls this at the top of each event so one hard event’s refine/generate spend never starves later events (the documented per-event semantics).- Return type:
- class foley.agent.ClapJudge[source]#
Bases:
objectThe zero-config
claprung: gate on the retrieval cosine (no ML, no network).matchiff the candidate’sclap_scoreclearstau_clap;confidenceis the score clamped to0..1so it is comparable to the higher rungs’ 0..1 scores.
- class foley.agent.DecideAction(*values)[source]#
-
What
decide()chose for one event (the single branch’s outcomes).
- class foley.agent.Decision(action, candidate=None, reason='')[source]#
Bases:
objectThe tiny result of
decide();reasonfeeds the refine hint + the audit Step.
- class foley.agent.Decomposer(*args, **kwargs)[source]#
Bases:
ProtocolNarrative context → a sparse, salience-ranked, diegetic-tagged event list.
Turns a story/commentary passage into
<= max_eventsSoundEvents, enforcing the salience/density budget and the anachronism (era_place) guard. The default is the deterministicKeywordDecomposer;AnthropicDecomposeris the LLM-backed impl.
- class foley.agent.Judge(*args, **kwargs)[source]#
Bases:
ProtocolOne rung of the verify ladder: does this candidate match this event? (report 10 §4.2).
levelselects the rung —clap(cheap score gate),listen(audio-LM),judge(LLM arbitration + scene consistency). The returnedVerdictcarrieslevel== the rung that produced it. Only thejudgerung’s real impl calls the LLM.
- class foley.agent.KeywordDecomposer[source]#
Bases:
objectDeterministic cue-lexicon decomposer — the zero-dependency default and CI fake.
Scans the passage against
_CUE_LEXICON, emits oneSoundEventper matched cue in first-appearance order (so salience descends with reading order), dedupes by canonical query, and truncates tomax_events(the sparse density budget). No RNG, no network, noanthropic— same passage → identical list.
- class foley.agent.KeywordRefiner[source]#
Bases:
objectDeterministic template-expansion refiner — the default and CI fake.
Expands a query into
ndistinct paraphrases via fixed descriptor templates; a verify-failurehintnudges one extra variant. No RNG, noanthropic— same query → identical paraphrase list.
- class foley.agent.Refiner(*args, **kwargs)[source]#
Bases:
ProtocolOne event query → 2–4 paraphrases/expansions (query-expansion for retrieval).
hintcarries the verify-failure reason so the re-retrieval loop can steer the next paraphrases. v1 feeds the list into a multi-query RRF search (embedding-fusion is a later drop-in behind this same seam). The default is the deterministicKeywordRefiner.
- class foley.agent.StringOverlapJudge(*, threshold=0.3)[source]#
Bases:
objectDeterministic stand-in for the
listen/judgerungs (the hermetic CI fake).Jaccard token overlap of the event query vs the candidate’s caption + tags. Lets the full ladder +
decide()branch run with zero ML/network. The returnedVerdictechoes back the requestedlevel.
- foley.agent.decide(event, kept, verified, *, tau_retrieve, budget, loop)[source]#
The single generate-vs-retrieve branch — a PURE function (report 05 §4).
Chooses among
DecideActionfrom the already-gated (kept) and already-verified (verified) sets. It performs no I/O and never callskeep/search/generate— thefoley.agent.toolsloop acts on the returnedDecision.Policy (report 05 §4):
a verified clip clearing
tau_retrieve→USE(the best one);verified-but-low-confidence with refine budget →
REFINE(feed the reason back);a non-diegetic cue, or a diegetic gap with no verified match, with generate budget →
GENERATE;otherwise →
DROP(silence), unless a lower-confidence verified clip exists and generation is off, in which case fall back to that best-effort pick.
- Parameters:
event (
SoundEvent) – The event being resolved.kept (
list[Candidate]) – The license-clean candidates (eachlicense_ok is True).verified (
list[Candidate]) – The subset ofkeptwhose verdict matched.tau_retrieve (
float) – The confidence threshold for auto-accepting a retrieved clip.budget (
Budget) – The per-event cost budget.loop (
int) – The current refine-loop index (for the audit reason).
- Return type:
- Returns:
A
Decision.
- foley.agent.decompose_context(context, *, max_events=6, seconds=None, decomposer=None, _span=None)[source]#
Decompose a passage into
<= max_eventssparseSoundEvents.The pure SELECT tool (Python-API == agent == future-MCP surface): resolves the default decomposer when
decomposerisNone, calls it, and records the GenAI span on the real path (the fake’slast_responseisNone→ no-op).- Parameters:
context (
str) – The narrative passage.max_events (
int) – The sparse density cap.seconds (
Optional[float]) – Optional passage duration (density-window hint; forwarded, else ignored).decomposer (
Optional[Decomposer]) – An injectedDecomposer(the DI seam); defaults to_default_decomposer()._span – Internal — the obs span handle
find()opens for GenAI recording.
- Return type:
- foley.agent.find(context, *, max_events=6, seconds=None, intended_use=None, backend='auto', verify='listen', stream=False, k=10, tau_retrieve=0.5, tau_clap=0.35, max_refine_loops=1, budget=None, library=None, decomposer=None, judge=None, refiner=None, llm=None)[source]#
The headline: a narrative context → verified, license-clean sound candidates.
decompose → (per event) refine/search → verify_match ladder → decide (with the fail-closed license gate FIRST) → place(report 05 §5). Works out of the box —foley.find("She pushed open the heavy oak door; rain hammered outside.")— with deterministic defaults; every model / threshold / seam is an optional keyword.- Parameters:
context (
str) – The narrative passage.max_events (
int) – The sparse density cap on decomposed events.seconds (
Optional[float]) – Optional passage duration (density-window hint; forwarded).intended_use (
Optional[IntendedUse]) – The caller’s rights intent (default:foley.licensing.DEFAULT_INTENDED_USE— commercial publishing; NC / SA material is refused unless you pass a different intent).backend (
str) – Generation backend for the fallback ('auto'→foley.generate’s default).verify (
Union[str,VerifyLevel]) – The max verify rung —'clap'|'listen'|'judge'.stream (
bool) – IfTrue, return a generator yielding oneCandidateper resolved event; else return the collectedlist.k (
int) – Retrieval shortlist depth per query.tau_retrieve (
float) – Confidence threshold to auto-accept a retrieved clip.tau_clap (
float) – Theclap-rung gate threshold.max_refine_loops (
int) – Max refine→re-retrieve passes per event (also the defaultBudget).budget (
Optional[Budget]) – An explicitBudget(overridesmax_refine_loops). It is also the run’s spend cap:Budget(max_usd=...)(default $1; $3 whenllm='anthropic'or$FOLEY_LLM=anthropic, whose default run is bounded at about $2.52 —foley.cost.DEFAULT_MAX_USD_BY_LLM) bounds every paid call the run makes — generations and paid LLM rungs — cumulatively, and a call of unknown cost needsBudget(approve_unknown_cost=True)(#57).library – Target
SoundLibrary(default: the process-wide default).refiner (decomposer / judge /) – Injected DI seams (
Decomposer/Judge/Refiner); each defaults to thellmprovider’s implementation.llm (
Optional[str]) – Which LLM the decompose / refine / judge rungs use when not injected —'fake'|'local'|'anthropic'.Nonereads$FOLEY_LLM, then falls back to the free default (a configured local endpoint, else the deterministic fake). A key being present never opts in to paid calls.
- Return type:
- Returns:
list[Candidate](stream=False) or anIterator[Candidate](stream=True) — one verified, license-clean candidate per resolved event.
- foley.agent.gate_candidates(candidates, intended_use)[source]#
Fail-closed license gate — run BEFORE verification/ranking (invariant #3).
For each candidate: apply
keep_sound()(any exception ⇒False, fail-closed), record the result oncandidate.license_ok, and keep ONLY where it isTrue— aNone(never gated) orFalseis dropped. This is the single rights-rejection point;verify_match()asserts its survivors are license-clean.- Parameters:
candidates (
list[Candidate]) – The retrieved shortlist (license_oktypicallyNone).intended_use (
IntendedUse) – The caller’s declared rights intent.
- Return type:
- Returns:
The license-clean sublist (each with
license_ok is True), order preserved.
- foley.agent.generate_sound(query, *, backend='auto', library=None)[source]#
Generate a clip for
query(the fallback tool) — a thin wrapper overfoley.generate.Returns a
Candidate(origin=generated); raises theGenerationErrorhierarchy on refusal / no-result (caught fail-closed by_generate_and_reverify()).- Return type:
- foley.agent.place_in_timeline(clip, *, onset=None, gain=0.0, layer=Layer.sfx_fg, loop=False)[source]#
Emit the SPARSE per-item plan subset (
clip_ref·onset·gain·layer·loop) — no more.onsetstays a SYMBOLIC anchor string (never a resolved offset); mix / processing / alignment / mastering are WEAVE’s (#8) job, not SELECT’s.- Return type:
- foley.agent.plan(candidates, *, transcript=None)[source]#
Fold verified candidates into the SPARSE
SoundDesignTimeline(the SELECT→WEAVE bridge).One
TimelineItemper candidate (onset·gain·layer·looponly, from itsSoundEvent), joined to the run-artifact viarun_manifest_ref— the reserved #8plan_refslot is filled when called inside an activefoley.obsrun scope (None-safe otherwise).
- foley.agent.refine_query(query, *, n=3, hint=None, refiner=None, _span=None)[source]#
Expand
queryinto up tonparaphrases for multi-query retrieval.- Parameters:
query (
str) – The event query to expand.n (
int) – Number of paraphrases.hint (
Optional[str]) – Optional verify-failure reason to steer re-retrieval.refiner (
Optional[Refiner]) – An injectedRefiner(the DI seam); defaults to_default_refiner()._span – Internal — the obs span handle for GenAI recording.
- Return type:
- foley.agent.search_sounds(queries, *, k=10, library=None, filters=None)[source]#
Hybrid search for one query, or a multi-query RRF-merge (the SELECT retrieval tool).
Delegates a single query VERBATIM to
SoundLibrary.search()(so the retrieval ranking the nDCG@10 gate measures is untouched); a multi-query list runs each query and RRF-merges (k=RRF_K) — the query-expansion recall lever, exercised only on a refine pass.- Parameters:
- Return type:
- foley.agent.verify_match(event, candidate, *, level=VerifyLevel.clap, judge=None, tau_clap=0.35, _span=None)[source]#
Verify
candidateagainsteventup to runglevel(AND-confirming ladder).Runs the
clapgate always; iflevelis higher and the clap gate passed, escalates to the injected/default judge for that rung and returns its verdict (Verdict.level== the producing rung).- Parameters:
event (
SoundEvent) – The wantedSoundEvent.candidate (
Candidate) – A license-cleanCandidate— this MUST run after thegate_candidates()gate (asserted).level (
str|VerifyLevel) – The max rung to climb (clap|listen|judge).judge (
Optional[Judge]) – An injectedJudgefor the higher rungs (the DI seam; defaults per_default_judge()).tau_clap (
float) – The clap-gate threshold._span – Internal — the obs span handle for GenAI recording on the LLM rung.
- Raises:
AssertionError – If
candidate.license_okis notTrue(verify-before-gate is a bug — the license gate is the fail-closed first pass).- Return type:
Modules
|
|
Which LLM the SELECT rungs use — one explicit, spend-safe resolver (the |
|
Offline / local-LLM SELECT rungs — OpenAI-compatible |
|
The MCP surface — foley's façade as agent-callable tools (py2mcp, #12, report 05/10). |
|
The SELECT policy: the fail-closed rights gate + the single generate-vs-retrieve branch. |
|
Audition UX — preview a sound, find similar ones, and refine by relevance feedback (#12). |
|
The structural DI seams of the SELECT stage — |
|
|
|
Session-scoped audition state — cached candidates, picks, and rejects (#12). |
|
The SELECT orchestration: |
|
|