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.
What it registers
Section titled “What it registers”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_v5 →
SANSAARM_MODEL_V5, music_gen_v6 → SANSAARM_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.
How a generation runs
Section titled “How a generation runs”-
Resolve provider config.
resolve_credentials()may override endpoint / model / key per task scope; anything unset falls back toSANSAARM_*env. A missing base URL / key / model raises"song generation is not configured"before any work starts. -
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
.wavto an mp3 variant and the upstream 400s a filename whose extension isn’t a supported format (seeprovider_filename). -
Submit + poll for a stream. The tool submits the built payload and polls for the first streaming choice, emitting coarse
emit_progresssteps. -
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. -
Collect masters. On completion, up to
versionsmasters 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 isrole="primary"; the rest upload asrole="alternate", each withgenerate_streaming_variant=True. -
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().
Pricing
Section titled “Pricing”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 = 2× 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}withusage_field = "versions", so the reserve holdsincrement × versionsup front and settle reconciles toincrement × report_usage. - Similar / Melody / MIDI / Singing families stay at the flat 200 (
DYNAMIC,cost_increment = 200,usage_field = "versions"). - Remix (
remix_song) isCOST_MODE_STATICat 800 (flat — the remix endpoint takes no model param, so it does not tier). - Extend Song / Stem Gen (
extend_song/stem_gen) areCOST_MODE_STATIC, version-tiered{v5: 200, v6: 300, v7: 400}. - Inpaint / Extend Music (
inpaint/extend_music) are single-engine (thesansaarsprovider has one engine), so they areCOST_MODE_STATICat 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.
Live preview & the async-HLS latency fix
Section titled “Live preview & the async-HLS latency fix”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.
Metadata it writes
Section titled “Metadata it writes”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
Lyricsshape. - 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 stampsDERIVED_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.
Config & env
Section titled “Config & env”| 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.
Key files
Section titled “Key files”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)
- __init__.py
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
- main.py
Run & test
Section titled “Run & test”make run— run the worker locally (needs a.envplus local Redis / core-database / core-storage). It registers every tool inALL_TOOLSon boot.make check— the full gate (ruff + pyright strict + pytest). Tools are unit-tested by callingprocess()with theFakeTaskContextfixture; 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
stagingbranch runsdeploy-aca-staging.ymlwithenable_ingress: false(a no-ingress worker). A schema change to any tool also requires restarting core-mcp, which caches tool schemas in memory.
Related
Section titled “Related”- Add a tool worker — the generic shape sansaarm specialises
- The soundverse-py SDK · TaskContext API —
WorkerFleet+ctx.* - Add pricing to a tool · Tool pricing model — the money path
- Storage & media plane — live preview + the async-HLS render
- Durable per-file metadata — the extend-region attributes path
- core-tool-agent · Other tool workers · Tool catalog