Skip to content

Commit 3221d40

Browse files
committed
Add oída→germ akousma bridge (three buttons) over the shared store
- oida/akousma_bridge.py: build akousma from a listen, persist to the shared akousmata store, and mint germ deep links for the three buttons (open as sound / open as prompt / explore lineage). Lazy-imports FastAPI so it stays usable in tests. - server.py: registers /germ/handoff (POST) and /germ/link (GET); optional so oída still boots without the akousma package. Fixed wildcard-bind message to name OIDA_AUTH_TOKEN. - pyproject: akousma dependency via [tool.uv.sources] path to earworm/py-akousma. - tests: bridge + cross-app round-trip (listen→A, germ child B, lineage A→B, algophony query). Full suite 107 tests pass.
1 parent e28626f commit 3221d40

4 files changed

Lines changed: 226 additions & 1 deletion

File tree

oida/akousma_bridge.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""oída → germ bridge over the shared akousma protocol.
2+
3+
oída is generative ears; germ is generative voice. After a listen, oída persists an
4+
**akousma** (the sound's memory record) into the shared **akousmata** store and hands germ
5+
an ``akousma_id`` via a deep link. The three UI buttons map to three modes:
6+
7+
- ``sound`` — "open as sound": load the listened fragment as an audio source in germ.
8+
- ``prompt`` — "open as prompt": open the listening result as a generation prompt in germ.
9+
- ``lineage`` — "explore lineage": open germ's genetic-ancestry explorer on this akousma.
10+
11+
Requires the ``akousma`` package (earworm/packages/py-akousma).
12+
"""
13+
from __future__ import annotations
14+
15+
import os
16+
from typing import Any
17+
from urllib.parse import urlencode
18+
19+
import akousma
20+
21+
MODES = ("sound", "prompt", "lineage")
22+
23+
24+
def germ_base_url() -> str:
25+
"""Germ's base URL for deep links (``OIDA_GERM_URL``, default local dashboard)."""
26+
return os.getenv("OIDA_GERM_URL", "http://127.0.0.1:5178").rstrip("/")
27+
28+
29+
def germ_deep_link(akousma_id: str, mode: str) -> str:
30+
if mode not in MODES:
31+
raise ValueError(f"mode must be one of {MODES}, got {mode!r}")
32+
query = urlencode({"akousma": akousma_id, "mode": mode})
33+
return f"{germ_base_url()}/import?{query}"
34+
35+
36+
def _origin_to_source_type(origin: str) -> str:
37+
"""Map oída's capture origin to Earworm's provenance source_type vocabulary."""
38+
return {
39+
"live-input": "recorded",
40+
"system-output": "recorded",
41+
"file": "imported",
42+
"generated": "generated",
43+
}.get(origin, "unknown")
44+
45+
46+
def build_akousma_from_listen(
47+
*,
48+
audio: dict[str, Any],
49+
listening: dict[str, Any] | None = None,
50+
origin: str = "file",
51+
device: str | None = None,
52+
session_id: str | None = None,
53+
tags: list[str] | None = None,
54+
) -> dict[str, Any]:
55+
"""Build a valid akousma record from an oída listen result.
56+
57+
``audio`` needs at least ``asset_id`` (and ideally ``uri``/``content_hash``/duration).
58+
``listening`` is namespaced per producer, e.g. ``{"oida.signal": {...}, "akouo.describe": {...}}``.
59+
"""
60+
record = akousma.new_akousma(
61+
audio=audio,
62+
originating_app="oida",
63+
source_type=_origin_to_source_type(origin),
64+
origin=origin,
65+
listening=listening or {},
66+
operation="listen",
67+
tags=tags,
68+
session_id=session_id,
69+
)
70+
if device:
71+
record["provenance"]["device"] = device
72+
return record
73+
74+
75+
def handoff_to_germ(
76+
record: dict[str, Any],
77+
mode: str,
78+
*,
79+
store: "akousma.AkousmataStore | None" = None,
80+
) -> dict[str, Any]:
81+
"""Persist ``record`` to the shared akousmata store and return the germ deep link.
82+
83+
Returns ``{"akousma_id", "mode", "germ_url"}``. Backing the three oída buttons.
84+
"""
85+
if mode not in MODES:
86+
raise ValueError(f"mode must be one of {MODES}, got {mode!r}")
87+
owns_store = store is None
88+
store = store or akousma.AkousmataStore()
89+
try:
90+
akousma_id = store.put(record)
91+
finally:
92+
if owns_store:
93+
store.close()
94+
return {"akousma_id": akousma_id, "mode": mode, "germ_url": germ_deep_link(akousma_id, mode)}
95+
96+
97+
def build_germ_router():
98+
"""FastAPI router backing the three oída→germ buttons. Imported lazily so this
99+
module stays usable without FastAPI (e.g. in cross-app tests)."""
100+
from fastapi import APIRouter, HTTPException
101+
from pydantic import BaseModel
102+
103+
class GermHandoffRequest(BaseModel):
104+
mode: str
105+
audio: dict[str, Any]
106+
listening: dict[str, Any] | None = None
107+
origin: str = "file"
108+
device: str | None = None
109+
session_id: str | None = None
110+
tags: list[str] | None = None
111+
112+
router = APIRouter(prefix="/germ", tags=["germ"])
113+
114+
@router.post("/handoff")
115+
def germ_handoff(req: GermHandoffRequest) -> dict[str, Any]:
116+
try:
117+
record = build_akousma_from_listen(
118+
audio=req.audio,
119+
listening=req.listening,
120+
origin=req.origin,
121+
device=req.device,
122+
session_id=req.session_id,
123+
tags=req.tags,
124+
)
125+
return handoff_to_germ(record, req.mode)
126+
except ValueError as exc:
127+
raise HTTPException(status_code=400, detail=str(exc)) from exc
128+
129+
@router.get("/link")
130+
def germ_link(akousma_id: str, mode: str = "lineage") -> dict[str, Any]:
131+
try:
132+
return {
133+
"akousma_id": akousma_id,
134+
"mode": mode,
135+
"germ_url": germ_deep_link(akousma_id, mode),
136+
}
137+
except ValueError as exc:
138+
raise HTTPException(status_code=400, detail=str(exc)) from exc
139+
140+
return router

oida/server.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,10 +473,18 @@ async def lifespan(_app: Any):
473473
static_dir = Path(__file__).resolve().parent / "static"
474474
app.mount("/static", StaticFiles(directory=static_dir), name="static")
475475

476+
# oída→germ bridge (three buttons) over the shared akousma store; optional.
477+
try:
478+
from .akousma_bridge import build_germ_router
479+
480+
app.include_router(build_germ_router())
481+
except ImportError:
482+
pass # akousma package not installed; oída still boots without the bridge
483+
476484
wildcard_bind = str(config.host) in {"0.0.0.0", "::", ""}
477485
if wildcard_bind and not config.auth_token:
478486
raise RuntimeError(
479-
"Refusing to bind oida on a wildcard host without HMM_AUTH_TOKEN or AEAR_AUTH_TOKEN. "
487+
"Refusing to bind oida on a wildcard host without OIDA_AUTH_TOKEN (or legacy HMM_/AEAR_AUTH_TOKEN). "
480488
"Use 127.0.0.1 for tokenless local operation."
481489
)
482490
if wildcard_bind:

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ requires-python = ">=3.12"
77
license = { text = "Apache-2.0" }
88
authors = [{ name = "Sonic Field Labs" }]
99
dependencies = [
10+
"akousma>=0.1.0",
1011
"fastapi>=0.115",
1112
"jsonschema>=4.22",
1213
"numpy>=1.26",
@@ -16,6 +17,9 @@ dependencies = [
1617
"uvicorn[standard]>=0.30",
1718
]
1819

20+
[tool.uv.sources]
21+
akousma = { path = "../earworm/packages/py-akousma", editable = true }
22+
1923
[project.optional-dependencies]
2024
moss = [
2125
"torch>=2.9",

tests/test_akousma_bridge.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""oída→germ bridge + cross-app akousma round-trip (Phase 4 acceptance)."""
2+
import tempfile
3+
import unittest
4+
5+
import akousma
6+
from oida import akousma_bridge
7+
8+
9+
class TestGermDeepLinks(unittest.TestCase):
10+
def test_deep_link_format(self):
11+
url = akousma_bridge.germ_deep_link("akm_1", "prompt")
12+
self.assertIn("/import?", url)
13+
self.assertIn("akousma=akm_1", url)
14+
self.assertIn("mode=prompt", url)
15+
16+
def test_rejects_unknown_mode(self):
17+
with self.assertRaises(ValueError):
18+
akousma_bridge.germ_deep_link("akm_1", "bogus")
19+
20+
def test_origin_maps_to_earworm_source_type(self):
21+
rec = akousma_bridge.build_akousma_from_listen(
22+
audio={"asset_id": "a1"}, origin="live-input"
23+
)
24+
self.assertEqual(rec["provenance"]["source_type"], "recorded")
25+
self.assertEqual(rec["provenance"]["origin"], "live-input")
26+
self.assertEqual(rec["provenance"]["originating_app"], "oida")
27+
28+
29+
class TestCrossAppRoundTrip(unittest.TestCase):
30+
def setUp(self):
31+
self.tmp = tempfile.TemporaryDirectory()
32+
self.store = akousma.AkousmataStore(self.tmp.name)
33+
34+
def tearDown(self):
35+
self.store.close()
36+
self.tmp.cleanup()
37+
38+
def test_listen_to_generate_to_lineage(self):
39+
# 1) oída listens to a file → akousma A, "open as prompt" hands it to germ.
40+
a = akousma_bridge.build_akousma_from_listen(
41+
audio={"asset_id": "file1", "uri": "akousmata://objects/x.wav", "duration_seconds": 8.0},
42+
origin="file",
43+
listening={"oida.signal": {"class": "tonal"}, "akouo.describe": {"summary": "struck bell"}},
44+
)
45+
handoff = akousma_bridge.handoff_to_germ(a, "prompt", store=self.store)
46+
A = handoff["akousma_id"]
47+
self.assertIn("mode=prompt", handoff["germ_url"])
48+
49+
# 2) germ generates a child B whose lineage points at A.
50+
b = akousma.new_akousma(
51+
audio={"asset_id": "gen1"},
52+
originating_app="germ",
53+
source_type="generated",
54+
origin="generated",
55+
parent_akousma_ids=[A],
56+
operation="transform",
57+
prompt="make it metallic",
58+
model="stable-audio-3",
59+
)
60+
B = self.store.put(b)
61+
62+
# 3) germ's lineage explorer walks ancestry; "explore lineage" from oída shows A→B.
63+
self.assertEqual(self.store.ancestors(B), [A])
64+
self.assertEqual(self.store.children(A), [B])
65+
66+
# 4) algophony batch query retrieves germ generations from the shared store.
67+
germ_generations = [r["akousma_id"] for r in self.store.query(originating_app="germ")]
68+
self.assertIn(B, germ_generations)
69+
self.assertEqual(len(self.store.query()), 2) # both A and B live in one store
70+
71+
72+
if __name__ == "__main__":
73+
unittest.main()

0 commit comments

Comments
 (0)