Source code for foley

"""foley — a retrieval-first façade for sound effects.

foley finds (or generates) the right sound effect for a moment of narration and
weaves it in. It is the SFX sibling of ``arioso`` (a unified façade over AI
music-generation backends): one simple surface over many sound *sources* (a
bring-your-own library, service APIs like Freesound, and generative-AI models),
a searchable *index* of every sound (by keyword *and* meaning, via CLAP
embeddings + hybrid search), an *agent* that selects the right sound for a
narrative context, and a *compositor* that places it under the voice.

Four stages::

    SOURCE  ->  INDEX  ->  SELECT  ->  WEAVE
    (get)      (find)      (choose)    (compose)

The façade (v1 — Epic #13 complete; the surface below is live — see
``misc/docs/design.md`` / ``misc/docs/roadmap.md`` for the roadmap)::

    import foley

    foley.find("She pushed open the heavy oak door; rain hammered outside.")
    foley.search("distant thunder rumble", k=10)
    foley.generate("a single wooden door creak", backend="stable_audio")
    foley.ingest("~/my_sounds/")

The design is grounded in the research reports under ``misc/docs/research/``.

The whole four-stage surface (source → index → select → weave) plus the MCP server and
the licensing/provenance, evaluation, and observability layers is implemented and
re-exported here (see ``__all__`` and the ``find`` / ``search`` / ``generate`` /
``ingest`` / ``weave`` façade functions below). The retrieval-agnostic **foundation**
every later stage stands on:

    * **Data models** (``foley.base``) — the SSOT dataclasses/enums shared across
      layers (:class:`SoundRecord`, :class:`LicenseRecord`, :class:`Candidate`,
      :class:`SoundEvent`, :class:`Verdict`, :class:`IntendedUse`), the two
      affordance registries, and generic dict/JSON (de)serialization.
    * **License policy** (``foley.licensing``) — the ``license_id`` -> flag-set
      SSOT (:data:`LICENSE_FLAGS`), flag derivation, and the fail-closed
      :func:`keep` gate.
    * **Storage** (``foley.stores``) — content-addressed byte store + metadata
      store built from ``dol``, and :func:`store_sound` (the by-value vs
      by-reference gate driven by ``LicenseRecord.cache_bytes_ok``).
    * **QC** (``foley.qc``) — Tier-0 deterministic audio checks
      (:func:`run_qc` -> :class:`QCReport`, thresholds in :class:`QCThresholds`).
    * **Audio** (``foley.audio``) — I/O + DSP primitives. Exposed as a submodule
      (``foley.audio``) with the key functions also re-exported here.

Import cost: ``import foley`` pulls only ``dol`` (a light core dependency used by
``foley.stores``); ``numpy``/``soundfile``/``soxr``/``librosa``/``pyloudnorm`` are
lazy-imported inside the audio/QC functions that need them (install via the
``foley[audio]`` extra), so a bare install imports cleanly.
"""

from . import audio
from .audio import (
    WORKING_SAMPLE_RATE,
    encode,
    ensure_channels,
    fade,
    load,
    loudness_normalize,
    resample,
    save,
    to_mono,
    to_working,
    trim_silence,
)
from .base import (
    GENERATION_AFFORDANCES,
    MASTER_PROFILES,
    QUERY_AFFORDANCES,
    SCHEMA_VERSION,
    AcquisitionMethod,
    Affordance,
    Anchor,
    Candidate,
    CandidateOrigin,
    IntendedUse,
    Layer,
    LicenseRecord,
    MasterProfile,
    Placement,
    Processing,
    Salience,
    SerializableMixin,
    SoundDesignTimeline,
    SoundEvent,
    SoundRecord,
    StorageMode,
    TimelineItem,
    Verdict,
    VerifyLevel,
    resolve_master,
)
from .licensing import (
    LICENSE_FLAGS,
    LICENSE_META,
    UNKNOWN_LICENSE_FLAGS,
    UNKNOWN_LICENSE_META,
    LicenseFlags,
    LicenseMeta,
    apply_license_flags,
    derive_license_flags,
    keep,
    keep_sound,
    license_id_from_cc_url,
    license_meta,
)
from .qc import (
    DEFAULT_QC_THRESHOLDS,
    QCReport,
    QCStatus,
    QCThresholds,
    dc_offset,
    detect_clipping,
    duration_s,
    estimate_snr,
    has_nan_inf,
    is_silent,
    measure_lufs,
    needs_edge_fade,
    run_qc,
    true_peak_dbtp,
)
from .stores import (
    DEFAULT_AUDIO_DIR,
    DEFAULT_META_DIR,
    DEFAULT_RUN_DIR,
    FOLEY_DATA_DIR,
    content_key,
    make_byte_store,
    make_meta_store,
    make_run_store,
    store_sound,
)
from . import index
from .index import (
    CLAP_SAMPLE_RATE,
    DEFAULT_CANDIDATE_K,
    DEFAULT_CLAP_DIM,
    DEFAULT_CLAP_MODEL_ID,
    RRF_K,
    Captioner,
    CatIdResolution,
    ClapEmbedder,
    ClapZeroShotTagger,
    Embedder,
    FusedHit,
    IngestReport,
    IngestResult,
    KeywordIndex,
    LanceIndex,
    MemoryIndex,
    PannsTagger,
    SoundLibrary,
    SqliteVecIndex,
    Tagger,
    VectorIndex,
    default_embedder,
    default_index,
    default_library,
    default_tagger,
    default_zeroshot_tagger,
    fuse_hits,
    hybrid_search,
    ingest_folder,
    ingest_one,
    lancedb_available,
    parse_ucs_filename,
    reciprocal_rank_fusion,
    resolve_catid,
    sqlite_vec_loadable,
    vector_search,
)

# --- source: bulk-corpus bootstrap (stdlib-only at import; numpy is lazy) ------
from .bootstrap import bootstrap, demo

# --- source: live-source adapters (Freesound) + the add_from pull facade -------
# Auto-discovery is lazy, so the Freesound adapter (and requests) is not imported
# until first use — `import foley` stays dol-only.
from .sources import add_from, list_sources, register_source

# --- source: generation adapters (Stable Audio Open / ElevenLabs) — #6 ---------
# The generate facade + its helpers. The generator adapter packages (and their
# torch/requests deps) are auto-discovered lazily, so `import foley` stays dol-only.
from .sources import (
    GenerationError,
    RecognizableVoiceRefusal,
    SafetyRefusal,
    TrademarkRefusal,
    candidate_of,
)
from .sources import generate as _generate_backend

# --- eval: Tier-1 retrieval metrics + the nDCG PR gate (numpy lazy) -----------
from . import eval  # noqa: A004 - deliberate: foley.eval is the retrieval-eval subpackage

# --- provenance: TASL attribution / credits (stdlib-only; #9b disclosure later) --
from . import provenance
from .provenance import (
    CreditEntry,
    Credits,
    attribution_line,
    credit_entry,
    credits_for,
)

# --- obs: observability + reproducible run-artifact (#11) ----------------------
# The obs package is stdlib-only at import (opentelemetry loads lazily inside the
# OTel-backed tracer only, behind foley[obs]), so this keeps `import foley` dol-only.
# Off by default: a plain façade call is a byte-for-byte no-op until obs is enabled.
from . import obs
from .obs import RunManifest, SpanRecord

# --- agent: the SELECT stage — find() + the sparse plan (#7) --------------------
# The agent package is dol-only at import (the LLM rungs sit behind DI-seam
# protocols with deterministic fakes; anthropic — foley[agent] — is imported lazily
# inside the real impls only), so this keeps `import foley` dol-only.
from . import agent
from .agent import (
    Budget,
    Decision,
    Judge,
    decide,
    decompose_context,
    find,
    plan,
    refine_query,
    verify_match,
)

# --- weave: the WEAVE stage — weave() renders the finished, editable mix (#8) ----
# The weave package is dol-only at import (the aligner + apply-strategy sit behind
# DI-seam protocols with deterministic defaults; whisperx/pyloudnorm/opentimelineio/
# audioseal/c2pa are imported lazily inside methods only). ``weave`` here is the
# façade: the ``weave`` package is made *callable* (see foley/weave/__init__.py), so
# ``foley.weave(narration, timeline)`` runs the façade while ``foley.weave.<submodule>``
# (``foley.weave.render`` etc.) stays fully importable.
from . import weave
from .weave import WeaveResult

# --- score: the one-call AI-first entry — narration text → tasteful woven sound design ---
# (composes find → plan → weave; numpy/torch stay lazy inside them, so this stays dol-only.)
from .score import ScoreResult, ScoredEvent, score

# --- the shipped agent kit installer (skill + slash command + subagent) ----------
from .agent_kit import install_agent_kit

# --- #12: MCP surface, preview UX, onboarding & offline mode ---------------------
# All dol-only at import (py2mcp/fastmcp are imported lazily inside build_mcp_server;
# numpy/soundfile only inside preview's encode path). ``mcp_server`` is the report-10
# façade alias of the builder.
from .agent.mcp import build_mcp_server
from .agent.mcp import build_mcp_server as mcp_server
from .agent.mcp import make_http_app, serve_http
from .agent.preview import preview, refine, similar_to
from .agent.session import SessionStore
from .requirements import capability_report, check_requirements, verify_and_setup
from .runtime import RuntimeConfig, is_offline, offline
from .stores import make_session_store

__all__ = [
    # --- base: constants + enums ---------------------------------------------
    "SCHEMA_VERSION",
    "StorageMode",
    "AcquisitionMethod",
    "CandidateOrigin",
    "Salience",
    "Layer",
    "VerifyLevel",
    # --- base: affordances ---------------------------------------------------
    "Affordance",
    "QUERY_AFFORDANCES",
    "GENERATION_AFFORDANCES",
    # --- base: serialization + models ----------------------------------------
    "SerializableMixin",
    "LicenseRecord",
    "SoundRecord",
    "SoundEvent",
    "Verdict",
    "Candidate",
    "IntendedUse",
    "TimelineItem",
    "SoundDesignTimeline",
    "Anchor",
    "Placement",
    "Processing",
    "MasterProfile",
    "MASTER_PROFILES",
    "resolve_master",
    # --- licensing: policy ---------------------------------------------------
    "LicenseFlags",
    "LICENSE_FLAGS",
    "UNKNOWN_LICENSE_FLAGS",
    "LicenseMeta",
    "LICENSE_META",
    "UNKNOWN_LICENSE_META",
    "license_meta",
    "license_id_from_cc_url",
    "derive_license_flags",
    "apply_license_flags",
    "keep",
    "keep_sound",
    # --- stores: content-addressed storage + the storage gate ----------------
    "content_key",
    "make_byte_store",
    "make_meta_store",
    "make_run_store",
    "store_sound",
    "FOLEY_DATA_DIR",
    "DEFAULT_AUDIO_DIR",
    "DEFAULT_META_DIR",
    "DEFAULT_RUN_DIR",
    # --- qc: Tier-0 deterministic audio QC -----------------------------------
    "QCStatus",
    "QCThresholds",
    "DEFAULT_QC_THRESHOLDS",
    "QCReport",
    "run_qc",
    "has_nan_inf",
    "duration_s",
    "dc_offset",
    "is_silent",
    "detect_clipping",
    "true_peak_dbtp",
    "estimate_snr",
    "needs_edge_fade",
    "measure_lufs",
    # --- audio: I/O + DSP primitives -----------------------------------------
    "audio",
    "WORKING_SAMPLE_RATE",
    "load",
    "save",
    "encode",
    "resample",
    "to_mono",
    "ensure_channels",
    "trim_silence",
    "fade",
    "loudness_normalize",
    "to_working",
    # --- index: embeddings, hybrid search, library façade, taxonomy ----------
    "index",
    "SoundLibrary",
    "default_library",
    "search",
    "similar",
    "Embedder",
    "ClapEmbedder",
    "default_embedder",
    "VectorIndex",
    "KeywordIndex",
    "MemoryIndex",
    "LanceIndex",
    "SqliteVecIndex",
    "default_index",
    "lancedb_available",
    "sqlite_vec_loadable",
    "hybrid_search",
    "vector_search",
    "reciprocal_rank_fusion",
    "fuse_hits",
    "FusedHit",
    "RRF_K",
    "DEFAULT_CANDIDATE_K",
    "DEFAULT_CLAP_MODEL_ID",
    "DEFAULT_CLAP_DIM",
    "CLAP_SAMPLE_RATE",
    "resolve_catid",
    "parse_ucs_filename",
    "CatIdResolution",
    "library",
    # --- index: taggers + ingestion ------------------------------------------
    "Tagger",
    "Captioner",
    "ClapZeroShotTagger",
    "PannsTagger",
    "default_tagger",
    "default_zeroshot_tagger",
    "ingest",
    "ingest_one",
    "ingest_folder",
    "IngestResult",
    "IngestReport",
    # --- source: bulk-corpus bootstrap ---------------------------------------
    "bootstrap",
    "demo",
    # --- source: live-source adapters + add_from pull facade -----------------
    "add_from",
    "list_sources",
    "register_source",
    # --- source: generation adapters (Stable Audio Open / ElevenLabs) --------
    "generate",
    "candidate_of",
    "GenerationError",
    # --- provenance: generation disclosure / watermark / safety (#9b) --------
    "SafetyRefusal",
    "TrademarkRefusal",
    "RecognizableVoiceRefusal",
    "art50_checklist",
    "scan_prompt",
    # --- obs: observability + reproducible run-artifact (#11) -----------------
    "obs",
    "RunManifest",
    "SpanRecord",
    # --- agent: the SELECT stage — find() + sparse plan (#7) ------------------
    "agent",
    "find",
    "plan",
    "decompose_context",
    "refine_query",
    "verify_match",
    "decide",
    "Judge",
    "Budget",
    "Decision",
    # --- weave: the WEAVE stage — weave() (#8) --------------------------------
    "weave",
    "WeaveResult",
    # --- score: the one-call AI-first entry (narration → woven sound design) --
    "score",
    "ScoreResult",
    "ScoredEvent",
    "install_agent_kit",
    # --- #12: MCP surface, preview UX, onboarding & offline mode ---------------
    "mcp_server",
    "build_mcp_server",
    "make_http_app",
    "serve_http",
    "preview",
    "similar_to",
    "refine",
    "SessionStore",
    "make_session_store",
    "RuntimeConfig",
    "offline",
    "is_offline",
    "check_requirements",
    "verify_and_setup",
    "capability_report",
    # --- eval: Tier-1 retrieval metrics + nDCG gate --------------------------
    "eval",
    "evaluate",
    # --- eval: Tier-2 fit-judge + fidelity + reliability (#10b) ---------------
    "evaluate_fit",
    # --- provenance: TASL attribution / credits ------------------------------
    "provenance",
    "credits",
    "Credits",
    "CreditEntry",
    "credits_for",
    "attribution_line",
    "credit_entry",
]


[docs] def evaluate(*, golden=None, k: int = 10): """Run the Tier-1 retrieval eval over the golden set (nDCG@10 / recall / mAP / MRR). Scores every golden query through the real :meth:`SoundLibrary.search` path against a deterministic, CLAP-free Ring-0 library — the same computation the PR gate asserts on. See :mod:`foley.eval`. Args: golden: Optional path to a golden-set JSON (default: the frozen seed). k: Retrieval cutoff and metric ``@k``. Returns: A :class:`foley.eval.RetrievalReport`. """ from .eval.golden import run_ring0_retrieval_eval kw = {"k": k} if golden is not None: kw["golden_path"] = golden return run_ring0_retrieval_eval(**kw)
[docs] def evaluate_fit( *, golden=None, sample=None, level=VerifyLevel.judge, fit_judge=None, embedder=None, seed: int = 0, k: int = 10, ): """Run the Tier-2 **fit** eval over the golden set — "does the accepted clip fit?" (#10b). The judge-based sibling of :func:`evaluate`: over a seeded stratified sample it runs the SELECT pipeline and audits each license-clean candidate with the authoritative fit-judge, returning a :class:`foley.eval.FitReport` (fit-precision / recall / F1 + fit-score + auto-accept-rate + per-stratum breakdown). Works out of the box on the Ring-0 fixture with the deterministic fake judge — no network, key, or heavy deps. **Nightly / pre-release and cost-gated**: report-only — gating is the caller's job via :meth:`FitReport.gate`. It never touches the retrieval ranking (the Tier-1 nDCG gate). Args: golden: Optional golden-set JSON path (default: the frozen Ring-0 seed). sample: Optional stratified sample cap (default: the whole set — the cost gate). level: The verify rung the fit-judge audits at — ``'listen'`` or ``'judge'`` (default ``VerifyLevel.judge``); ``'clap'`` is rejected. fit_judge: An injected authoritative judge (default: the auto-resolved fit-judge — the LLM arbiter :class:`~foley.agent.AnthropicJudge` when a key is configured, else the hermetic :class:`~foley.agent.StringOverlapJudge` fake; the audio-LM :class:`~foley.agent.AudioLMJudge` is injection-only in this slice). embedder: The Ring-0 embedder (default: the CLAP-free ``HashingBowEmbedder``). seed: The sampling RNG seed. k: Retrieval shortlist depth per event. Returns: A :class:`foley.eval.FitReport`. """ from .eval.fit import run_fit_eval kw = dict( sample=sample, level=level, fit_judge=fit_judge, embedder=embedder, seed=seed, k=k, ) if golden is not None: kw["golden_path"] = golden return run_fit_eval(**kw)
[docs] def credits( sounds, *, title: str = "Credits", only_required: bool = False, write_to=None ): """Build the TASL attribution :class:`~foley.provenance.Credits` for ``sounds``. Works standalone today (given any iterable of sounds), and is what the WEAVE stage will call at render time. Inspect ``.markdown`` (a ``CREDITS.md`` document) / ``.manifest`` (a JSON-serializable dict) on the result. Args: sounds: An iterable of :class:`SoundRecord` / :class:`Candidate` / :class:`LicenseRecord` (e.g. the result of :func:`search`, or the sounds placed in a timeline). title: The credits heading. only_required: Keep only legally-required attributions (drops CC0 / user-owned courtesy credits). Default credits everything. write_to: Optional directory; when given, writes ``CREDITS.md`` and ``credits.json`` into it (created if missing). Returns: A :class:`~foley.provenance.Credits`. """ result = provenance.credits_for(sounds, title=title, only_required=only_required) if write_to is not None: from pathlib import Path out = Path(write_to) out.mkdir(parents=True, exist_ok=True) (out / "CREDITS.md").write_text(result.markdown, encoding="utf-8") (out / "credits.json").write_text(result.to_json(indent=2), encoding="utf-8") return result
[docs] def similar(sound_id: str, *, k: int = 10): """Find sounds similar to a stored sound (audio<->audio) in the default library. See :meth:`foley.index.SoundLibrary.similar`. """ return default_library().similar(sound_id, k=k)
[docs] def generate( prompt: str, *, backend: str = "stable_audio", library=None, store: bool = True, adapter=None, watermark=None, on_flagged: str = "refuse", watermarker=None, provenance_store=None, **affordances, ): """Generate a sound effect for ``prompt`` and add it to the library (by-value). Progressive disclosure: ``foley.generate("a single wooden door creak")`` works out of the box (the local Stable Audio Open backend, into the process-wide default library). The generated audio is stored **by-value** with a content-hash id, so it becomes a first-class, re-searchable library entry — every generation is a future free retrieval (the generation flywheel). It flows through the SAME :func:`~foley.index.ingest.ingest_one` pipeline as every other source, with operator consent for the generator license's AI-training restriction (the record keeps ``ai_training_ok=False``, so :func:`keep` still refuses it for training uses). Args: prompt: The natural-language sound description. backend: A registered generate source — ``"stable_audio"`` (default, local; needs ``foley[stable-audio]``) or ``"elevenlabs"`` (hosted; ``foley[elevenlabs]`` + ``$ELEVENLABS_API_KEY``). library: Target library (default: the process-wide default library). store: If ``False``, synthesize + enrich a preview without adding it. adapter: Optional pre-built adapter (the DI seam; production omits it and the registry lazily builds one). watermark: ``True`` require an AudioSeal watermark, ``False`` never, ``None`` (default, auto) watermark iff ``foley[provenance]`` is installed (#9b). on_flagged: ``'refuse'`` (default, fail-closed) or ``'warn'`` for a prompt that trips the trademarked-audio / recognizable-voice safety gate (#9b). watermarker: An injected watermarker (the DI seam; tests pass a fake). provenance_store: A ``MutableMapping`` for content-credential sidecars (default: :func:`foley.stores.make_provenance_store`). **affordances: Unified generation affordances (``duration``, ``prompt_influence``, ``negative_prompt``, ``steps``, ``seed``, ``loop``, ``output_format`` — see :data:`GENERATION_AFFORDANCES`); a backend warns-and-drops the ones it does not support. Returns: The stored :class:`Candidate` (``origin=generated``) — its ``sound`` is the canonical, by-value :class:`SoundRecord` (a content-hash id). Raises: SafetyRefusal: If the prompt trips a safety gate and ``on_flagged='refuse'`` (a :class:`GenerationError` subclass — ``TrademarkRefusal`` / ``RecognizableVoiceRefusal``). GenerationError: If the backend yields no stored sound (QC-quarantined, rights-blocked, or a synthesis/ingest error). The exception carries the full ``report`` and terminal ``status`` so callers can react distinctly. """ report = _generate_backend( prompt, backend=backend, library=library, store=store, adapter=adapter, watermark=watermark, on_flagged=on_flagged, watermarker=watermarker, provenance_store=provenance_store, **affordances, ) results = report.results res = results[0] if results else None if res is None: raise GenerationError( f"generation via {backend!r} produced no result", report=report, status=None ) if res.status in ("pass", "warn"): return candidate_of(res) if res.status == "skipped_dup": # A byte-identical regeneration is already in the library — return it (the # desirable flywheel behavior: never store byte-twins). lib = library if library is not None else default_library() return Candidate(sound=lib[res.id], origin=CandidateOrigin.generated) raise GenerationError( f"generation via {backend!r} yielded no stored sound ({res.status})", report=report, status=res.status, )
[docs] def ingest( path, *, library=None, backend: str = "local", qc: bool = True, recursive: bool = True, **kw, ): """Ingest a folder (or single file) of sounds into the default library. ``probe -> QC -> tag -> zero-shot -> caption -> embed -> SoundRecord`` for each file, returning an :class:`~foley.index.IngestReport`. See :func:`foley.index.ingest_folder` / :func:`~foley.index.ingest_one` for the per-file options (``license``, taggers, ``min_status``, …). Args: path: A folder (walked) or a single audio file. library: Target library (default: the process-wide default library). backend: ``"local"`` ingests filesystem audio; other backends (a source adapter pull) route through ``add_from`` (subtask #5) — kept in the signature for forward-compat. qc: Run the Tier-0 QC gate (quarantines failing clips). recursive: Recurse into sub-folders. **kw: Forwarded to :func:`foley.index.ingest_one`. """ if backend != "local": # A non-local backend names a live source adapter (#5): treat ``path`` as # the query and route through the add_from pull facade (search -> license # gate -> download -> the shared ingest_one pipeline). return add_from(backend, query=path, library=library, **kw) return ingest_folder( path, library=library if library is not None else default_library(), recursive=recursive, do_qc=qc, **kw, )
def __getattr__(name: str): """Lazily expose ``foley.library`` + the #9b disclosure helpers. ``library`` is kept lazy so ``import foley`` never constructs the CLAP model or the index; the disclosure helpers are lazy so the (stdlib-only) :mod:`foley.provenance.disclosure` module is not imported until first use, keeping the eager ``import foley`` graph minimal. """ if name == "library": return default_library() if name in ("art50_checklist", "scan_prompt", "build_content_credential"): from .provenance import disclosure return getattr(disclosure, name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}")