Skip to content

Commit 17d3d4e

Browse files
committed
Release Oída-informed Germ cultivation
1 parent 9f6f523 commit 17d3d4e

53 files changed

Lines changed: 3647 additions & 547 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Local server. GERM_* is preferred; GERMINATOR_* remains supported as a legacy
22
# fallback for old shells and launchers.
33
GERM_HOST=127.0.0.1
4-
GERM_PORT=8765
4+
GERM_PORT=5178
55
GERM_ACTIVE_PROVIDER=mock
66
GERM_OUTPUT_DIR=output
77

@@ -49,12 +49,15 @@ GERM_LISTENER_MAX_DURATION_SECONDS=600
4949
HF_TOKEN=
5050
HUGGINGFACE_HUB_TOKEN=
5151

52-
# Future Stability API provider. Not required for local generation.
52+
# Optional hosted Stable Audio 3 provider. Local providers do not require this key.
5353
STABILITY_API_KEY=
54+
GERM_STABILITY_API_URL=https://api.stability.ai
55+
GERM_STABILITY_POLL_SECONDS=10
5456

55-
# Optional local listener fallback.
56-
OLLAMA_BASE_URL=http://127.0.0.1:11434
57-
OLLAMA_MODEL=llama3.2
57+
# Oída owns machine listening and audio understanding. Germ uses this bridge only
58+
# when "Re-listen with Oída" is requested.
59+
GERM_OIDA_URL=http://127.0.0.1:8765
60+
GERM_OIDA_TIMEOUT_SECONDS=1800
5861

5962
# Dev conveniences read by the launch scripts (not by the server itself):
6063
# GERM_RELOAD=1 makes launch_germ.command start uvicorn with --reload.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ output/sessions/
3636

3737
apps/macos/.build/
3838
apps/macos/dist/
39+
apps/germ.app/
40+
.codex/
3941

4042
/*.png
4143
/*.jpg

NOTICE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ stable-audio-3 @ git+https://github.com/Stability-AI/stable-audio-3.git
1818
That dependency is not vendored in this repository. Install it only if your use
1919
case and environment are compatible with the upstream project and model terms.
2020

21-
Earworm integration surfaces map germ metadata into Earworm 0.1 protocol-shaped
21+
Earworm integration surfaces map germ metadata into Earworm 0.4-compatible
2222
sessions for local provenance, consent, retention, and lineage workflows.

README.md

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ exploration tool for working with Stable Audio 3 workflows.
1818
## Stable Audio 3 Focus
1919

2020
The app exposes a local FastAPI server, a browser dashboard, generated WAV files,
21-
metadata JSON, and provider switching across mock, Python, MLX, and future API
21+
metadata JSON, and provider switching across mock, Python, MLX, and Stability API
2222
routes. It keeps technical Stable Audio controls visible while adding a germ workflow
2323
layer for sound matter, micro modules, culture, strain, mutation, pruning,
2424
propagation, listening, curation, and harvest.
@@ -48,9 +48,11 @@ Official Stable Audio references:
4848
incubated evolution.
4949
- Control layer for audio-to-control, safe OSC, MIDI intent, CV-safe exports, and
5050
control ancestry, including the norns/Fates OSC bridge.
51-
- Listener prompt enhancement and heuristic scoring through `/listener/enhance`
52-
and `/listener/score`, with optional local Ollama fallback.
53-
- Earworm 0.1 export of generated metadata into local context-chain sessions.
51+
- Neutral, editable prompt compilation through `/listener/enhance`, local DSP-only
52+
signal checks through `/listener/score`, and explicit re-listening through oída at
53+
`/listener/relisten`. Machine listening and audio understanding stay in oída.
54+
- Earworm 0.4-compatible export of generated metadata into local context-chain
55+
sessions, plus Akousma spec v1.3 lineage, kinship, and covenant-aware handoffs.
5456
- Shared audio player with waveform drawing, metadata preview, path copy, open, and
5557
Finder reveal.
5658
- Diagnostics for provider readiness and Hugging Face access.
@@ -112,6 +114,9 @@ Native macOS app (embeds the same dashboard, supervises the same daemon):
112114
apps/macos/script/build_and_run.sh
113115
```
114116

117+
The build stages the runnable bundle at `apps/germ.app`. To build it without
118+
launching it, use `apps/macos/script/build_and_run.sh --build-only`.
119+
115120
Tailscale launch for another device in the same tailnet:
116121

117122
```bash
@@ -170,10 +175,13 @@ GERM_MAX_IMAGE_MB=8
170175
GERM_ENABLE_CLOUD_VISION=0
171176
GERM_LISTENER_MAX_AUDIO_MB=50
172177
GERM_LISTENER_MAX_DURATION_SECONDS=600
178+
STABILITY_API_KEY= # optional: hosted Stable Audio 3
179+
GERM_STABILITY_API_URL=https://api.stability.ai
180+
GERM_STABILITY_POLL_SECONDS=10
181+
GERM_OIDA_URL=http://127.0.0.1:8765
182+
GERM_OIDA_TIMEOUT_SECONDS=1800
173183
GERM_RELOAD=1 # launch_germ.command only: start uvicorn with --reload
174184
GERM_TAILSCALE_IP=... # scripts/run_tailscale.sh only: override the auto-detected IP
175-
OLLAMA_BASE_URL=http://127.0.0.1:11434
176-
OLLAMA_MODEL=llama3.2
177185
```
178186

179187
The older `GERMINATOR_*` names are still accepted as fallbacks for compatibility.
@@ -240,8 +248,8 @@ request is recorded, but a render already inside the model call may finish norma
240248
- Propagation: continuation workflow for loops, tails, beds, and textures.
241249
- Strains: creative LoRA palette and Culture Mix scaffold.
242250
- Herbarium: archive view over saved outputs.
243-
- Listener: prompt enhancer, heuristic scorer, repair proposals, and optional
244-
local Ollama prompt enhancement with no mandatory cloud key.
251+
- Listener: a neutral prompt compiler, local signal checks, repair proposals, and
252+
the oída re-listening bridge. **Oída hears. germ cultivates.**
245253
- Lab: placeholders for SAME latents, dataset prep, model comparison, benchmarks,
246254
and agent experiments.
247255

@@ -268,10 +276,10 @@ sections.
268276

269277
## Listener
270278

271-
Listener can enhance a rough idea into a Stable Audio prompt, merge a negative
272-
prompt, score WAV files with bounded local DSP features, propose loop-seam repair
273-
ranges, and fall back to local Ollama when available. It does not require a cloud
274-
LLM API for the app to run.
279+
Listener normalizes a rough idea without injecting a modality, scores WAV files with
280+
bounded local DSP features, and proposes measured loop-seam repairs. Re-listening and
281+
prompt derivation are delegated to oída; germ does not embed a listening or
282+
audio-understanding model and requires no general-purpose LLM API.
275283

276284
## API
277285

@@ -299,6 +307,7 @@ POST /lora/strength
299307
GET /listener/providers
300308
POST /listener/enhance
301309
POST /listener/score
310+
POST /listener/relisten
302311
GET /strains
303312
POST /strains
304313
GET /strains/{strain_id}
@@ -336,7 +345,7 @@ curl -X POST http://127.0.0.1:5178/generate \
336345
-d '{
337346
"provider": "mock",
338347
"model": "mock-sine",
339-
"prompt": "TrackType: SFX, dry close-microphone loop of metallic friction",
348+
"prompt": "dry close-microphone loop of metallic friction",
340349
"negative_prompt": "speech, vocals, melody, long reverb",
341350
"duration": 3.0,
342351
"steps": 8,
@@ -378,8 +387,8 @@ Generated metadata keeps existing compatibility fields and adds germ identity fi
378387

379388
Each generated metadata record also includes an `earworm` block with deterministic
380389
session, asset, and provenance IDs. Use `POST /earworm/export` to write a local
381-
Earworm 0.1 session JSON from a metadata path. The cross-stack fixture lives at
382-
`tests/fixtures/germ-organism-mapping.session.json`.
390+
Earworm context-session JSON from a metadata path. The export shape remains
391+
compatible with Earworm's current v0.4 session schema contract.
383392

384393
## Development Notes
385394

apps/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Apps
2+
3+
- `macos/` contains the SwiftPM source for the native germ shell.
4+
- `germ.app` is the generated runnable macOS bundle. Create or refresh it with
5+
`macos/script/build_and_run.sh --build-only`; it stays out of git because it
6+
contains machine-specific build output.
7+
8+
The native shell renders the daemon's `/dashboard` route in a `WKWebView`, so
9+
it uses the same HTML, CSS, JavaScript, backend state, and saved sessions as the
10+
browser version.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
import AppKit
2+
import Darwin
23

34
final class AppDelegate: NSObject, NSApplicationDelegate {
5+
private var terminationSignalSources: [DispatchSourceSignal] = []
6+
47
func applicationDidFinishLaunching(_ notification: Notification) {
8+
installGracefulTerminationHandler(for: SIGTERM)
9+
installGracefulTerminationHandler(for: SIGINT)
510
NSApp.setActivationPolicy(.regular)
611
NSApp.activate(ignoringOtherApps: true)
712
}
13+
14+
/// Route shell/build-script termination through AppKit so
15+
/// `willTerminateNotification` runs and the app can stop only the daemon
16+
/// process it owns before exiting.
17+
private func installGracefulTerminationHandler(for signalNumber: Int32) {
18+
Darwin.signal(signalNumber, SIG_IGN)
19+
let source = DispatchSource.makeSignalSource(signal: signalNumber, queue: .main)
20+
source.setEventHandler {
21+
NSApp.terminate(nil)
22+
}
23+
source.resume()
24+
terminationSignalSources.append(source)
25+
}
826
}

apps/macos/Sources/GermMacOS/App/GermMacOSApp.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ struct GermMacOSApp: App {
1111
ContentView()
1212
.environmentObject(store)
1313
.frame(minWidth: 980, minHeight: 640)
14+
.ignoresSafeArea(.container, edges: .top)
1415
.task {
1516
await store.bootstrap()
1617
}
1718
}
19+
.windowStyle(.hiddenTitleBar)
1820
.commands {
1921
CommandMenu("germ") {
2022
Button("Open Dashboard in Browser") {

apps/macos/Sources/GermMacOS/Services/DaemonSupervisor.swift

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,43 @@ final class DaemonSupervisor {
4141
throw DaemonSupervisorError.repositoryRootNotFound
4242
}
4343

44+
let helper = root
45+
.appendingPathComponent("apps", isDirectory: true)
46+
.appendingPathComponent("macos", isDirectory: true)
47+
.appendingPathComponent("script", isDirectory: true)
48+
.appendingPathComponent("managed_daemon.py", isDirectory: false)
49+
guard FileManager.default.isReadableFile(atPath: helper.path) else {
50+
throw DaemonSupervisorError.launchFailed("Missing \(helper.path)")
51+
}
52+
4453
let outputPipe = Pipe()
4554
let errorPipe = Pipe()
4655
let proc = Process()
47-
proc.executableURL = URL(fileURLWithPath: "/usr/bin/env")
48-
proc.arguments = [
49-
"uv",
50-
"run",
51-
"uvicorn",
56+
let helperArguments = [
57+
"-u",
58+
helper.path,
59+
"--parent-pid",
60+
"\(ProcessInfo.processInfo.processIdentifier)",
5261
"server.main:app",
5362
"--host",
5463
host,
5564
"--port",
5665
"\(port)"
5766
]
67+
let venvPython = root
68+
.appendingPathComponent(".venv", isDirectory: true)
69+
.appendingPathComponent("bin", isDirectory: true)
70+
.appendingPathComponent("python3", isDirectory: false)
71+
if FileManager.default.isExecutableFile(atPath: venvPython.path) {
72+
proc.executableURL = venvPython
73+
proc.arguments = helperArguments
74+
} else {
75+
// Bootstrap through uv only when no synced environment exists. The
76+
// helper still watches the app PID and tears down its whole
77+
// uvicorn process group if the app disappears.
78+
proc.executableURL = URL(fileURLWithPath: "/usr/bin/env")
79+
proc.arguments = ["uv", "run", "--frozen", "python"] + helperArguments
80+
}
5881
proc.currentDirectoryURL = root
5982
proc.standardOutput = outputPipe
6083
proc.standardError = errorPipe

apps/macos/Sources/GermMacOS/Services/LaunchAtLoginManager.swift

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,43 @@ import Foundation
22
import ServiceManagement
33

44
enum LaunchAtLoginManager {
5+
private static var status: SMAppService.Status {
6+
SMAppService.mainApp.status
7+
}
8+
59
static var statusLabel: String {
6-
switch SMAppService.mainApp.status {
10+
switch status {
711
case .enabled:
812
return "Enabled"
913
case .notRegistered:
1014
return "Disabled"
1115
case .requiresApproval:
1216
return "Requires approval"
1317
case .notFound:
14-
return "App not found"
18+
return "Requires signed install"
1519
@unknown default:
1620
return "Unknown"
1721
}
1822
}
1923

2024
static var isEnabled: Bool {
21-
SMAppService.mainApp.status == .enabled
25+
status == .enabled
26+
}
27+
28+
static var canEnable: Bool {
29+
status == .notRegistered
30+
}
31+
32+
static var canDisable: Bool {
33+
status == .enabled || status == .requiresApproval
34+
}
35+
36+
static var requiresApproval: Bool {
37+
status == .requiresApproval
38+
}
39+
40+
static var isUnavailable: Bool {
41+
status == .notFound
2242
}
2343

2444
static func setEnabled(_ enabled: Bool) throws {
@@ -28,4 +48,8 @@ enum LaunchAtLoginManager {
2848
try SMAppService.mainApp.unregister()
2949
}
3050
}
51+
52+
static func openSystemSettings() {
53+
SMAppService.openSystemSettingsLoginItems()
54+
}
3155
}

apps/macos/Sources/GermMacOS/Services/ShellStore.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,16 @@ final class ShellStore: ObservableObject {
4949
let stored = UserDefaults.standard.string(forKey: "germDaemonBaseURL")
5050
daemonBaseURL = (stored?.isEmpty == false ? stored! : "http://127.0.0.1:5178")
5151
// Quit stops only the daemon this shell started; externally started
52-
// daemons are observed, never owned. (A managed orphan would also die
53-
// on its next log write, since its stdio pipes into this app.)
54-
let supervisor = self.supervisor
52+
// daemons are observed, never owned. The managed process helper also
53+
// watches this app's PID so an unexpected exit cannot orphan uvicorn.
5554
NotificationCenter.default.addObserver(
5655
forName: NSApplication.willTerminateNotification,
5756
object: nil,
5857
queue: .main
59-
) { _ in
60-
supervisor.stop()
58+
) { [weak self] _ in
59+
Task { @MainActor [weak self] in
60+
self?.supervisor.stop()
61+
}
6162
}
6263
supervisor.onLogLine = { [weak self] line in
6364
Task { @MainActor [weak self] in

0 commit comments

Comments
 (0)