Skip to content

core-tool-sansaarm

core-tool-sansaarm is the fleet’s flagship generation worker: it turns a prompt, reference, or existing song into a finished master — full songs, sung vocals, and pure instrumentals. Like every generation tool it is a long-running queue worker, not an HTTP service (no ingress): it boots a WorkerFleet over ALL_TOOLS, claims tasks, runs a synchronous process() in a thread pool, streams a live preview through core-storage, uploads the finished master(s), and drains on SIGTERM. If you have read Add a tool worker, the shape is familiar — this page is the field guide to what makes sansaarm specific.

The one hard constraint that shapes the whole repo: it calls a third-party generation provider that is aliased everywhere as sansaarm / sansaar.

The old kitchen-sink song_gen / music_gen families were split by input modality, so each tool has a small, obvious schema instead of one form whose fields are mostly mutually exclusive. The result is 9 families, each in three version variants (v5 / v6 / v7)27 registered tools — all listed in app/tools/__init__.py.

Family Tool name stem Input Upstream control Source
AI song song_gen_v* prompt + optional lyrics + gender prompt / lyrics song_gen.py
Similar song similar_song_gen_v* a reference song (mp3/m4a) reference_id song_gen.py
Melody → song melody_song_gen_v* a melody clip (mp3/m4a, 5–60s) melody_id song_gen.py
MIDI → song midi_song_gen_v* a .mid file melody_id song_gen.py
AI singing vocals_gen_v* lyrics + style + optional voice sample reference_id song_gen.py
Similar singing similar_sing_gen_v* a reference vocal reference_id song_gen.py
AI music music_gen_v* prompt (instrumental, no vocals) prompt music_gen.py
Similar music similar_music_gen_v* an instrumental reference instrumental_id music_gen.py
Extend song extend_song_v* a library song + continuation lyrics upload_audio_id + extend_at extend_song.py

Each family shares one orchestration base (SongOrchestration / _MusicOrchestration) and one pure payload builder in app/sansaarm/payload.py that encodes the upstream’s exclusivity rules; a family differs only in its input model and which reference field it uploads. Versions bind the per-tier model ids: song_gen_v5SANSAARM_MODEL_V5, music_gen_v6SANSAARM_MODEL_MUSIC_V6, and so on. model on the class (e.g. song-gen-v5) is the stable Soundverse id used for pricing, registration, and provenance — it is never the env-resolved upstream model string.

  1. Resolve provider config. resolve_credentials() may override endpoint / model / key per task scope; anything unset falls back to SANSAARM_* env. A missing base URL / key / model raises "song generation is not configured" before any work starts.

  2. Upload references. The agent or panel pings a file id; the SDK’s file-input seam resolves it (ownership + format/size constraints), lazily mints a signed URL, and hands the tool bytes — so the tool never touches a SAS URL. The provider-upload filename extension is derived from the resolved (post-conversion) mime, because the framework may have rebound a mislabeled .wav to an mp3 variant and the upstream 400s a filename whose extension isn’t a supported format (see provider_filename).

  3. Submit + poll for a stream. The tool submits the built payload and polls for the first streaming choice, emitting coarse emit_progress steps.

  4. Live preview via core-storage. The first streaming choice is pumped through ctx.live_audio_ingest(), so the caller only ever sees a Soundverse-hosted HLS URL — the provider stream is never exposed. A preview failure is swallowed and degrades to a plain final-master upload; it must never fail the whole generation.

  5. Collect masters. On completion, up to versions masters are surfaced. The tool prefers the provider’s lossless FLAC (flac_url) as the canonical master and falls back to the lossy final url — it never re-encodes lossy → FLAC. The live-finalized version is role="primary"; the rest upload as role="alternate", each with generate_streaming_variant=True.

  6. Persist metadata + report usage. Structured lyrics, provenance, license, and lineage are written per file (below); then ctx.report_usage(count) reports the raw version count. The worker never bills — core-mcp / the gateway price and settle.

Phase wall-clock markers (setup+submit, submit→first_stream, first_stream→complete, complete→return) are logged on every run so a real generation shows the provider-vs-ours time split. See app/tools/song_gen.py for the full process().

Every family declares LicensePricing for all five license tiers (ROYALTY_FREE, STANDARD, DISTRIBUTION, SYNC, MASTER) — not one — because core-mcp’s header resolver hardcodes STANDARD for agent calls while the consumer gateway uses the user’s chosen tier, and FetchResolvedPricing is an exact license match with no fallback. Prices mirror the 2.0 UI’s displayed credits. Model-quality tiers cost more, and a tool that returns exactly one asset is priced COST_MODE_STATIC so it charges once (not the DYNAMIC(cost_base=X, cost_increment=X, usage_field="") shape, which reserves X but settles to X + X × usage = for a one-unit result):

  • AI Song / AI Music (song_gen / music_gen) are version-tiered: DYNAMIC, cost_base=0, cost_increment = {v5: 400, v6: 600, v7: 800} with usage_field = "versions", so the reserve holds increment × versions up front and settle reconciles to increment × report_usage.
  • Similar / Melody / MIDI / Singing families stay at the flat 200 (DYNAMIC, cost_increment = 200, usage_field = "versions").
  • Remix (remix_song) is COST_MODE_STATIC at 800 (flat — the remix endpoint takes no model param, so it does not tier).
  • Extend Song / Stem Gen (extend_song / stem_gen) are COST_MODE_STATIC, version-tiered {v5: 200, v6: 300, v7: 400}.
  • Inpaint / Extend Music (inpaint / extend_music) are single-engine (the sansaars provider has one engine), so they are COST_MODE_STATIC at a flat 200.

Pricing is upserted only on worker registration, so a pricing change needs a worker restart to take effect (and the resolved-price cache has a short TTL). A tool that registers with no pricing row fails every call with NotFound — the trap the pricing guide and tool-pricing model exist to prevent.

The chat player keys off the master blob hash, not the terminal streaming URL, and the BFF falls back to the directly-playable master when a streaming rendition isn’t ready yet. That makes the HLS render safe to background — early plays fall back to the master until the rendition lands. A “song gen takes ~260s” investigation traced most of the removable tail to core-storage’s synchronous CMAF/HLS transcode blocking the tool’s terminal emit_progress(100); the fix backgrounds that render in core-storage (no call-site change here) and tightened the poll cadence (SANSAARM_POLL_INTERVAL 2.0s → 1.0s, SANSAARM_MAX_POLLS 450 → 900 to keep the same ceiling). Full detail lives on the Storage & media plane page.

Per produced version, the tool persists onto the file row (all best-effort — a metadata hiccup never fails a run):

  • Lyrics — the provider’s timestamped sections mapped to the canonical Lyrics shape.
  • Provenance — the prompt plus the stable Soundverse model / tool id (e.g. song-gen-v5), never the env-resolved upstream model string.
  • License — the task’s chosen license (default ROYALTY_FREE), the link the library “License Info” panel reads.
  • Lineage — reference/melody sources are auto-captured as parents; song families stamp VARIATION_OF, the extend family stamps DERIVED_FROM.

The extend “which slice is new” region is written to storage.attributes (namespace extend, via ctx.set_attributes) — the durable, authoritative store, not the storage.files metadata JSONB bag — and is also returned transiently on versions[].extend so the chat card can highlight the extended band live. This is the attributes-not-JSONB directive in practice.

Env var Purpose
SANSAARM_API_BASE_URL / SANSAARM_API_KEY Upstream endpoint + key (fallbacks; resolve_credentials() can override per task)
SANSAARM_MODEL_V5 / _V6 / _V7 Per-tier song model ids
SANSAARM_MODEL_MUSIC_V5 / _V6 / _V7 Per-tier instrumental model ids
SANSAARM_MODEL_EXTEND_V5 / _V6 / _V7 Per-tier extend model ids — optional; falls back to the base SANSAARM_MODEL_V*
SANSAARM_EXTEND_HEAD_MODELS Comma-list of model ids that allow head extension; a head request on any other model fails fast
SANSAARM_POLL_INTERVAL / SANSAARM_MAX_POLLS Provider status-poll cadence + budget per phase (default 1.0s / 900)
SANSAARM_REQUEST_TIMEOUT / SANSAARM_REDACT Upstream request timeout; extra brand terms to scrub from errors/logs

Fleet-wide connection/secret vars (INTERNAL_RPC_SECRET, REDIS_ADDR, CORE_DATABASE_GRPC, CORE_STORAGE_GRPC, and the per-service CORE_DATABASE_USE_TLS / CORE_STORAGE_USE_TLS toggles) are owned by soundverse.config; worker tuning (MAX_CONCURRENCY, POLL_INTERVAL for the fleet’s idle task-claim poll — distinct from SANSAARM_POLL_INTERVAL, ENVIRONMENT) lives in app/settings.py.

  • Directorycore-tool-sansaarm/
    • Directoryapp/
      • main.py SERVICE_NAME + WorkerFleet(ALL_TOOLS).start()
      • settings.py worker + SANSAARM_* provider config
      • Directorytools/
        • __init__.py ALL_TOOLS — the 27 registered tools
        • song_gen.py song + singing families + shared SongOrchestration
        • music_gen.py instrumental families + _MusicOrchestration
        • extend_song.py song-extension families
        • inputs.py typed file inputs (accept lists, size/duration caps)
      • Directorysansaarm/ provider isolation — never named outside here
        • client.py upload / generate / poll / iter_stream
        • payload.py pure payload builders + combination rules
        • errors.py redactor + SansaarmError
        • models.py provider DTOs
  • make run — run the worker locally (needs a .env plus local Redis / core-database / core-storage). It registers every tool in ALL_TOOLS on boot.
  • make check — the full gate (ruff + pyright strict + pytest). Tools are unit-tested by calling process() with the FakeTaskContext fixture; the test suite also holds the leak-guard assertions that fail if the upstream vendor name escapes into any output.
  • Deploys are by-convention: pushing the staging branch runs deploy-aca-staging.yml with enable_ingress: false (a no-ingress worker). A schema change to any tool also requires restarting core-mcp, which caches tool schemas in memory.