Skip to content

Commit ded79cc

Browse files
committed
Implement the oída→germ receiving side of the akousma bridge
- GET /import?akousma=<id>&mode=sound|prompt|lineage — the three buttons land: 'sound' imports the record's audio into germ's library through the standard audio-import flow (lineage metadata preserved, germ.import extension stamped back onto the shared record); 'prompt' derives a generation prompt from the listening block (preferred namespaces akouo.describe > oida.moss > oida.signal); 'lineage' serves a self-contained lineage-explorer page (record, parents, children, ancestry, listen-with-oída link). - JSON surface: GET /akousma/record/{id}, GET /akousma/lineage/{id}, POST /akousma/generation (writes a germ akousma with parent lineage; the audio stays in place via file:// uri + content hash). - server/akousma_store.py: lazy shared-store access (AKOUSMATA_PATH honored at call time; 503 when the akousma package is absent), prompt derivation, record_generation helper for the pipeline. - pyproject: akousma dep via [tool.uv.sources] (earworm/packages/py-akousma). - docs/api_reference.md: all four routes documented (route-coverage gate). - 7 new tests against an isolated temp store; full suite 105 passed; ruff clean.
1 parent 845cfe5 commit ded79cc

7 files changed

Lines changed: 979 additions & 0 deletions

File tree

docs/api_reference.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,44 @@ analysis, render, provenance, and retention records.
4040
When `persist=true`, the session JSON is written under `output/metadata/` and the
4141
response includes `session_file` plus the inline `session` object.
4242

43+
## GET /import
44+
45+
The oída→germ handoff (the three buttons). Query params: `akousma` (record id in the
46+
shared akousmata store), `mode` (`sound` | `prompt` | `lineage`), `format`
47+
(`html` default, or `json`). `sound` imports the record's audio into germ's library
48+
via the standard audio-import flow and stamps `extensions["germ.import"]` back onto
49+
the shared record; `prompt` derives a generation prompt from the record's listening
50+
block; `lineage` opens the lineage explorer (parents, children, ancestry).
51+
52+
## GET /akousma/record/{akousma_id}
53+
54+
Returns the raw akousma record from the shared store (404 if unknown, 503 if the
55+
`akousma` package is not installed).
56+
57+
## GET /akousma/lineage/{akousma_id}
58+
59+
Returns `{record, parents, children, ancestor_ids}` with parent/child records
60+
resolved from the shared store — the data behind the lineage explorer.
61+
62+
## POST /akousma/generation
63+
64+
Writes a germ generation into the shared store as a new akousma whose
65+
`lineage.parent_akousma_ids` point at its sources.
66+
67+
```json
68+
{
69+
"audio_path": "output/audio/example.wav",
70+
"prompt": "make it metallic",
71+
"model": "stable-audio-3",
72+
"operation": "audio-to-audio",
73+
"parent_akousma_ids": ["akm_..."],
74+
"tags": ["metallic"]
75+
}
76+
```
77+
78+
Unknown parent ids are rejected with 404; the response returns `akousma_id` plus the
79+
full record. The audio stays in place (referenced by `file://` uri + content hash).
80+
4381
## GET /huggingface/status
4482

4583
Checks Hugging Face CLI auth for the gated Stable Audio 3 Python-provider weights.

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ readme = "README.md"
66
license = "MPL-2.0"
77
requires-python = ">=3.10"
88
dependencies = [
9+
"akousma>=0.1.0",
910
"fastapi>=0.111.0",
1011
"httpx>=0.27.0",
1112
"pydantic>=2.7.0",
@@ -14,6 +15,9 @@ dependencies = [
1415
"uvicorn[standard]>=0.30.0",
1516
]
1617

18+
[tool.uv.sources]
19+
akousma = { path = "../earworm/packages/py-akousma", editable = true }
20+
1721
[project.optional-dependencies]
1822
python-provider = [
1923
"stable-audio-3 @ git+https://github.com/Stability-AI/stable-audio-3.git",

server/akousma_store.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""germ ↔ akousmata: access to the shared sonic-memory store.
2+
3+
The akousma protocol (earworm/docs/akousma_spec_v1.md) gives every sound one memory
4+
record; the shared store (``~/Documents/SFL/akousmata``, ``$AKOUSMATA_PATH``) spans
5+
oída, germ, and algophony. germ reads records handed over by oída ("open as sound /
6+
prompt / explore lineage") and writes a new akousma for material it generates, with
7+
``lineage.parent_akousma_ids`` pointing at the sources.
8+
9+
The ``akousma`` package is the Python reference implementation that lives in the
10+
earworm repo (``earworm/packages/py-akousma``). It is imported lazily so the server
11+
still boots without it; the routes degrade to 503.
12+
"""
13+
from __future__ import annotations
14+
15+
from hashlib import sha256
16+
from pathlib import Path
17+
from typing import Any
18+
19+
20+
class AkousmaUnavailable(RuntimeError):
21+
"""The akousma reference package is not installed."""
22+
23+
24+
def _akousma():
25+
try:
26+
import akousma
27+
except ModuleNotFoundError as exc: # pragma: no cover - environment-dependent
28+
raise AkousmaUnavailable(
29+
"the 'akousma' package is not installed; "
30+
"pip install -e <SFL>/earworm/packages/py-akousma"
31+
) from exc
32+
return akousma
33+
34+
35+
def open_store():
36+
"""Open the shared akousmata store (honors AKOUSMATA_PATH at call time)."""
37+
akousma = _akousma()
38+
return akousma.AkousmataStore()
39+
40+
41+
def resolve_audio_path(store, record: dict[str, Any]) -> Path | None:
42+
"""Resolve a record's audio to a local file path (store object or file uri)."""
43+
audio = record.get("audio") or {}
44+
uri = str(audio.get("uri") or "")
45+
if not uri:
46+
return None
47+
if uri.startswith("akousmata://"):
48+
path = store.resolve_uri(uri)
49+
return path if path and path.exists() else None
50+
if uri.startswith("file://"):
51+
path = Path(uri[len("file://"):])
52+
return path if path.exists() else None
53+
path = Path(uri).expanduser()
54+
return path if path.is_absolute() and path.exists() else None
55+
56+
57+
_PROMPT_KEYS = ("summary", "caption", "description", "text", "prompt")
58+
_PREFERRED_NAMESPACES = ("akouo.describe", "oida.moss", "oida.signal")
59+
60+
61+
def derive_prompt(record: dict[str, Any]) -> str:
62+
"""Turn a record's listening block into a generation prompt for germ."""
63+
listening = record.get("listening") or {}
64+
fragments: list[str] = []
65+
66+
def collect(block: Any) -> None:
67+
if isinstance(block, str) and block.strip():
68+
fragments.append(block.strip())
69+
elif isinstance(block, dict):
70+
for key in _PROMPT_KEYS:
71+
value = block.get(key)
72+
if isinstance(value, str) and value.strip():
73+
fragments.append(value.strip())
74+
75+
for namespace in _PREFERRED_NAMESPACES:
76+
collect(listening.get(namespace))
77+
if not fragments:
78+
for value in listening.values():
79+
collect(value)
80+
if fragments:
81+
break
82+
83+
if not fragments:
84+
tags = [str(t) for t in record.get("tags") or []]
85+
if tags:
86+
fragments.append(", ".join(tags))
87+
88+
seen: set[str] = set()
89+
unique = [f for f in fragments if not (f in seen or seen.add(f))]
90+
return ". ".join(unique[:2])
91+
92+
93+
def record_generation(
94+
*,
95+
audio_path: str | Path,
96+
prompt: str = "",
97+
model: str = "",
98+
operation: str = "generate",
99+
params: dict[str, Any] | None = None,
100+
parent_akousma_ids: list[str] | None = None,
101+
listening: dict[str, Any] | None = None,
102+
tags: list[str] | None = None,
103+
extensions: dict[str, Any] | None = None,
104+
store=None,
105+
) -> dict[str, Any]:
106+
"""Write a germ generation into the shared store as a new akousma.
107+
108+
The audio stays where germ wrote it (referenced by ``file://`` uri +
109+
content hash); lineage points at the source akousmata.
110+
"""
111+
akousma = _akousma()
112+
path = Path(audio_path).expanduser().resolve()
113+
data = path.read_bytes()
114+
115+
owns_store = store is None
116+
store = store or open_store()
117+
try:
118+
record = akousma.new_akousma(
119+
audio={
120+
"asset_id": path.stem,
121+
"type": "generation",
122+
"uri": f"file://{path}",
123+
"content_hash": f"sha256:{sha256(data).hexdigest()}",
124+
},
125+
originating_app="germ",
126+
source_type="generated",
127+
origin="generated",
128+
listening=listening,
129+
parent_akousma_ids=parent_akousma_ids or [],
130+
operation=operation,
131+
prompt=prompt or None,
132+
model=model or None,
133+
params=params,
134+
tags=tags,
135+
extensions=extensions,
136+
)
137+
store.put(record)
138+
return record
139+
finally:
140+
if owns_store:
141+
store.close()

server/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from server.config import get_settings
1010
from server.identity import LEGACY_ENGINE_NAME, PRODUCT_DESCRIPTION, PRODUCT_NAME
1111
from server.routes import (
12+
akousma,
1213
audio_tools,
1314
audio_to_audio,
1415
continue_audio,
@@ -69,6 +70,7 @@
6970
app.include_router(health.router)
7071
app.include_router(diagnostics.router)
7172
app.include_router(earworm.router)
73+
app.include_router(akousma.router)
7274
app.include_router(huggingface.router)
7375
app.include_router(models.router)
7476
app.include_router(performance.router)

0 commit comments

Comments
 (0)