Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ ASA_MSST_ROOT="" # path to the MSST-WebUI checkout (adde
ASA_MSST_MODEL="scnet_4stem" # MSST model registry id in separation_backend._MSST_MODEL_REGISTRY. Default `scnet_4stem` (4-stem parity). `bs_roformer_vocals` is research/A-B-only (2-stem, leaves bass/drums empty). Unknown ids fall back to the default.
ASA_MSST_MODEL_DIR="" # optional config/checkpoint root (MSST configs/ + pretrain/ layout); defaults to ASA_MSST_ROOT.
ASA_MSST_DEVICE="" # optional device for MSST inference (auto|cpu|cuda|mps); defaults to auto.
ASA_PHASE2_PROVIDER="gemini" # Phase 2 interpretation provider: `gemini` (default, product path, unchanged) or `moss` to route the producer_summary interpretation to a self-hosted OpenMOSS MOSS-Audio FastAPI sidecar. Default-off experiment; both providers flow through the identical parse/citation/catalogue validators. STEP ONE licence gate: MOSS-Audio *weights* are Apache-2.0 but the *modeling code* has no effective licence (the GitHub repo's pyproject points at a non-existent LICENSE), so the sidecar ships no OpenMOSS code and the real-model path is a 501 stub — research-only, not promotable. See apps/backend/phase2_provider.py + docs/PHASE2_PROVIDER.md.
ASA_MOSS_SIDECAR_URL="http://127.0.0.1:8200" # base URL of the MOSS sidecar (apps/backend/moss_sidecar/app.py) when ASA_PHASE2_PROVIDER=moss.
ASA_MOSS_SIDECAR_TIMEOUT_SECONDS="180" # per-request timeout for the MOSS sidecar call.
ASA_MOSS_MODEL_ID="" # optional MOSS model id forwarded to the sidecar; defaults to the run's model_name.
ASA_MOSS_SIDECAR_MODE="mock" # sidecar-side (its own venv): `mock` (default, deterministic schema-valid Phase-1-grounded output) or `model` (a 501 licence-gated stub that executes no OpenMOSS code).
```

Phase 2 is gated by `VITE_ENABLE_PHASE2_GEMINI`. `GEMINI_API_KEY` is backend-only. `SONIC_ANALYZER_ADMIN_KEY` is backend-only and never exposed to clients.
Expand Down
79 changes: 79 additions & 0 deletions apps/backend/moss_sidecar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# MOSS-Audio Phase 2 Sidecar

A standalone FastAPI service that emits the **identical `Phase2Result` schema** as
the Gemini Phase 2 path, fed the same inputs (built prompt + audio + authoritative
Phase 1 JSON + `response_schema`). The ASA backend reaches it via
[`phase2_provider.py`](../phase2_provider.py) when `ASA_PHASE2_PROVIDER=moss`.

**Default-off research experiment.** Gemini stays the product default. This sidecar
runs in its **own venv**, isolated from the product venv (mirrors the MSST
separation runner) so its dependencies never touch `analyze.py`'s contract.

## ⚠️ Licence status (STEP ONE gate)

- **Weights** (`OpenMOSS-Team/MOSS-Audio-*` on HuggingFace): **Apache-2.0** — clean,
permits self-host + derivative + commercial use.
- **Modeling code** (the forward pass you must run): **no effective licence.** The
`OpenMOSS/MOSS-Audio` GitHub repo has no `LICENSE` file; its `pyproject.toml`
declares `license = { file = "LICENSE" }` — a dangling reference to a file that
does not exist. The HF weights repo has no `modeling_*.py` and its `auto_map` has
no `AutoModel` entry, so you cannot run the model without the GitHub `src/` code.

Consequence: **this sidecar ships no OpenMOSS code.** The real-model path
(`ASA_MOSS_SIDECAR_MODE=model`) is a 501 stub. Only the deterministic `mock` mode
runs. See [`docs/PHASE2_PROVIDER.md`](../../../docs/PHASE2_PROVIDER.md) and the
`asa-moss-audio-licence-gate` memory. Promotion is unblocked only when OpenMOSS
publishes the missing LICENSE (or `moss_audio` is upstreamed into Apache-2.0
`transformers`), or a cleanly-licensed runtime can serve the Apache-2.0 weights for
audio-in inference.

## Run (mock mode)

```bash
# From apps/backend (own venv — NOT the product venv):
python3.11 -m venv moss_sidecar/.venv
moss_sidecar/.venv/bin/pip install -r moss_sidecar/requirements.txt
moss_sidecar/.venv/bin/python -m moss_sidecar.app # serves 127.0.0.1:8200
```

Then point the backend at it:

```bash
ASA_PHASE2_PROVIDER=moss ASA_MOSS_SIDECAR_URL=http://127.0.0.1:8200 \
<start the ASA backend> # producer_summary interpretations now route to MOSS
```

`stem_summary` interpretations stay on Gemini; only the recommendation-emitting
`producer_summary` profile routes to MOSS.

## Contract

`POST /v1/phase2`

```jsonc
{
"prompt": "<full Phase 2 system prompt, already embeds Phase 1 + device catalogue>",
"response_schema": { /* the Phase2Result schema server.py would hand Gemini */ },
"phase1": { /* authoritative measurement JSON, for citation grounding */ },
"model": "moss-audio-4b-instruct",
"audio": { "filename": "x.flac", "mime_type": "audio/flac", "base64": "..." } // or null
}
```

→ `{ "result": <Phase2Result> | null, "provider": "moss-mock" | "moss", "warnings": [] }`

`GET /healthz` → `{ "status": "ok", "mode": "mock", "provider": "moss-mock" }`

The backend re-serialises `result` and runs it through the **same** parse +
`_validate_phase2_citation_paths` + `apply_live12_catalogue_gates` path as Gemini,
so the schema and the chain-of-custody contract are enforced identically.

## Eval

[`scripts/evaluate_phase2_providers.py`](../scripts/evaluate_phase2_providers.py)
scores citation accuracy across providers on the fixed
`tests/fixtures/recommendation_tracks/` corpus. The mock's citation accuracy is
~1.0 **by construction** (it cites only resolving paths) — it proves the pipeline
and scorer, not the real model's quality. Live MOSS-vs-Gemini numbers are blocked
(no Gemini key locally, no audio renders in the corpus, model not runnable under
the licence gate); see the eval's `--help` and `docs/PHASE2_PROVIDER.md`.
5 changes: 5 additions & 0 deletions apps/backend/moss_sidecar/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""ASA MOSS-Audio Phase 2 sidecar package.

Default-off research experiment. Runs in its OWN venv, isolated from the product
venv (mirrors the MSST separation runner). See ``app.py`` and ``README.md``.
"""
133 changes: 133 additions & 0 deletions apps/backend/moss_sidecar/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""MOSS-Audio Phase 2 sidecar — a standalone FastAPI service.

Fed the same inputs as Gemini (built prompt + audio + authoritative Phase 1 JSON
+ ``response_schema``), it returns a ``Phase2Result`` that the ASA backend
validates through the *identical* parse / citation / catalogue path as Gemini
(see ``apps/backend/phase2_provider.py``). Run it on its **own venv** (FastAPI +
uvicorn — and, only if/when licensing is resolved, the model stack), isolated
from the product venv exactly like the MSST separation runner.

Modes (env ``ASA_MOSS_SIDECAR_MODE``):
- ``mock`` (default): deterministic, schema-valid, Phase-1-grounded output via
``mock_interpreter``. No model, no GPU. This is what the experiment + eval use.
- ``model``: the real MOSS-Audio path — a **gated stub**. STEP ONE found the
MOSS-Audio *weights* are Apache-2.0 but the *modeling code* you must run has
no effective licence (the GitHub repo's ``pyproject`` points at a ``LICENSE``
file that does not exist). So this service ships **no** OpenMOSS code and the
``model`` path raises a 501 explaining what would unblock it. See
``docs/PHASE2_PROVIDER.md`` and the README in this directory.

Run:
uvicorn moss_sidecar.app:app --host 127.0.0.1 --port 8200 # from apps/backend
# or: python -m moss_sidecar.app
"""

from __future__ import annotations

import os
from typing import Any

try: # allow both `uvicorn moss_sidecar.app:app` and `python -m moss_sidecar.app`
from .mock_interpreter import build_mock_phase2_result
except ImportError: # pragma: no cover - direct-script fallback
from mock_interpreter import build_mock_phase2_result # type: ignore

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field


SIDECAR_MODE = os.getenv("ASA_MOSS_SIDECAR_MODE", "mock").strip().lower()

app = FastAPI(title="ASA MOSS-Audio Phase 2 Sidecar", version="0.1.0")


class AudioPayload(BaseModel):
filename: str
mime_type: str
base64: str


class Phase2SidecarRequest(BaseModel):
prompt: str
response_schema: dict[str, Any] = Field(default_factory=dict)
phase1: dict[str, Any] = Field(default_factory=dict)
model: str | None = None
request_id: str | None = None
audio: AudioPayload | None = None


class Phase2SidecarResponse(BaseModel):
result: dict[str, Any] | None
provider: str
warnings: list[str] = Field(default_factory=list)


def _run_model_interpreter(request: Phase2SidecarRequest) -> dict[str, Any]:
"""Real MOSS-Audio inference — intentionally a licence-gated stub.

This deliberately imports and executes **no** OpenMOSS code. Two independent
conditions must hold before a real implementation may live here:

1. The MOSS-Audio inference/modeling code carries an effective licence
permitting self-host + derivative use (today it does not — the GitHub
repo has no ``LICENSE`` file; only the *weights* are Apache-2.0), OR a
cleanly-licensed runtime (e.g. an Apache/MIT reimplementation) can serve
the Apache-2.0 weights for audio-in inference.
2. The operator opts in explicitly via ``ASA_MOSS_ALLOW_UNLICENSED_MODEL=1``
and supplies a runner path — and accepts the licence risk.

Until then this raises 501 so the failure is loud and honest rather than a
silent fabrication.
"""
if os.getenv("ASA_MOSS_ALLOW_UNLICENSED_MODEL", "").strip() != "1":
raise HTTPException(
status_code=501,
detail=(
"MOSS-Audio model mode is not wired: the modeling code has no "
"effective licence (STEP ONE gate). Use ASA_MOSS_SIDECAR_MODE=mock, "
"or resolve the licence and supply a cleanly-licensed runtime. See "
"docs/PHASE2_PROVIDER.md."
),
)
raise HTTPException(
status_code=501,
detail=(
"Real MOSS-Audio inference is not implemented in this sidecar by "
"design — it would execute currently-unlicensed OpenMOSS modeling "
"code. Provide a cleanly-licensed runtime and implement "
"_run_model_interpreter against it."
),
)


@app.get("/healthz")
def healthz() -> dict[str, Any]:
return {"status": "ok", "mode": SIDECAR_MODE, "provider": _provider_label()}


def _provider_label() -> str:
return "moss-mock" if SIDECAR_MODE == "mock" else "moss"


@app.post("/v1/phase2", response_model=Phase2SidecarResponse)
def interpret(request: Phase2SidecarRequest) -> Phase2SidecarResponse:
if SIDECAR_MODE == "mock":
result = build_mock_phase2_result(request.phase1, prompt=request.prompt)
return Phase2SidecarResponse(result=result, provider="moss-mock", warnings=[])
if SIDECAR_MODE == "model":
result = _run_model_interpreter(request)
return Phase2SidecarResponse(result=result, provider="moss", warnings=[])
raise HTTPException(
status_code=500,
detail=f"Unknown ASA_MOSS_SIDECAR_MODE '{SIDECAR_MODE}' (expected 'mock' or 'model').",
)


if __name__ == "__main__": # pragma: no cover
import uvicorn

uvicorn.run(
app,
host=os.getenv("ASA_MOSS_SIDECAR_HOST", "127.0.0.1"),
port=int(os.getenv("ASA_MOSS_SIDECAR_PORT", "8200")),
)
Loading