From ed25d4fc94a12584b8d99119b58482ccf64c87e0 Mon Sep 17 00:00:00 2001 From: emeisazam <255706292+emeisazam@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:35:25 -0500 Subject: [PATCH] Release v0.3.0 --- .env.example | 11 + CHANGELOG.md | 14 + CITATION.cff | 4 +- README.md | 34 +- ROADMAP.md | 2 +- apps/macos/script/build_and_run.sh | 4 +- dashboard/static/app.js | 523 +++++++++++++++----- dashboard/static/audio_engine.js | 61 ++- dashboard/static/cosmoaudition.js | 115 +++++ dashboard/static/index.html | 76 ++- dashboard/static/micro_unicode.js | 19 +- dashboard/static/styles.css | 54 +++ dashboard/static/wavetable_synth.js | 13 +- docs/api_reference.md | 96 ++++ docs/germ_micro_architecture.md | 54 +++ pyproject.toml | 2 +- scripts/smoke_dashboard.mjs | 161 ++++++- server/config.py | 25 + server/control.py | 31 ++ server/cosmoaudition.py | 353 ++++++++++++++ server/identity.py | 2 +- server/main.py | 4 + server/masa_bridge.py | 710 ++++++++++++++++++++++++++++ server/matter_analysis.py | 401 ++++++++++++++++ server/routes/control.py | 2 + server/routes/cosmoaudition.py | 263 +++++++++++ server/routes/matter.py | 141 ++++++ server/schemas.py | 166 +++++++ server/storage.py | 78 ++- tests/test_project_consistency.py | 12 +- tests/test_server.py | 355 ++++++++++++++ uv.lock | 2 +- 32 files changed, 3606 insertions(+), 182 deletions(-) create mode 100644 dashboard/static/cosmoaudition.js create mode 100644 server/cosmoaudition.py create mode 100644 server/masa_bridge.py create mode 100644 server/matter_analysis.py create mode 100644 server/routes/cosmoaudition.py create mode 100644 server/routes/matter.py diff --git a/.env.example b/.env.example index 353eae6..359744f 100644 --- a/.env.example +++ b/.env.example @@ -60,6 +60,17 @@ GERM_STABILITY_POLL_SECONDS=10 GERM_OIDA_URL=http://127.0.0.1:8765 GERM_OIDA_TIMEOUT_SECONDS=1800 +# Optional local Cosmoaudition System bridge. Germ accepts only an HTTP +# loopback URL and never contacts astronomical, geological, weather, or other +# observatory providers itself. +GERM_COSMOAUDITION_URL=http://127.0.0.1:8797 +GERM_COSMOAUDITION_TIMEOUT_SECONDS=20 +GERM_COSMOAUDITION_MAX_RESPONSE_BYTES=2097152 + +# Write optional MASA 0.1 interoperability sidecars beside Germ's canonical +# lineage metadata. Set to 0 to disable without changing generation behavior. +GERM_MASA_SIDECARS=1 + # Dev conveniences read by the launch scripts (not by the server itself): # GERM_RELOAD=1 makes launch_germ.command start uvicorn with --reload. GERM_RELOAD=0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b82f88..763e3b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.3.0 — Cosmoaudition, Matter Analysis, and audio reliability + +- Added the loopback-only Cosmoaudition bridge, explicit observation mappings, + archives, module palette, Matter Analysis, and optional MASA 0.1 sidecars. +- Kept unavailable, errored, stale, and zero-valued observations distinct; + missing Mapping Loom inputs now execute their declared policy without a + fabricated neutral value. +- Streamed bounded spectral aggregation with cached FFT windows, validated + Matter lineage shapes, and exposed the final MASA companion state in API and + dashboard results. +- Flushed queued AudioWorklet PCM before recorder shutdown, preserved explicit + zero-gain wavetable previews, removed confirmed dead dashboard helpers, and + expanded backend, audio, and browser regression coverage. + ## 0.2.5 — Backend integrity and integration hardening - Hardened request, persisted JSON, path, upload, WAV, and provider artifact diff --git a/CITATION.cff b/CITATION.cff index b3b681e..6d0d719 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,8 +5,8 @@ authors: - family-names: "Isaza" given-names: "eme" affiliation: "Sonic Field Labs" -version: "0.2.5" -date-released: "2026-07-16" +version: "0.3.0" +date-released: "2026-07-31" license: "MPL-2.0" repository-code: "https://github.com/sonicfieldlabs/germ" abstract: "germ is a local generative microsound environment whose generated sounds retain prompts, parents, mutations, listening metadata, and Earworm-compatible lineage." diff --git a/README.md b/README.md index 9efdd11..0987b78 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ listened to, and traced through lineage. A listening from Oída can become a prompt or source in GERM; a successful render can become a descendant in Akousmata and return to Oída for another listening. -Current release: `0.2.5`. +Current release: `0.3.0`. GERM is an independent Sonic Field Labs project. It can use Stable Audio 3 providers, but it is not an official Stability AI product. @@ -42,6 +42,11 @@ checks but not a creative quality demonstration. Apple Silicon MLX, or opt-in Stability API providers. - Build a modular graph across grains, cells, swarms, tissues, controls, effects, and a realtime Chamber. +- Use local Cosmoaudition observations as attributed modulation and event + material through explicit mappings, archives, and missing-data policies. +- Measure spectral, temporal, spatial, and morphological properties with + Matter Analysis while keeping measurements, inference, and unavailable + states distinct. - Graft, inpaint, continue, compare, mutate, record, and collect sound while retaining prompts, seeds, models, parameters, parents, and operations. - Import an Oída listening as sound, prompt, or lineage and ask Oída to listen @@ -90,6 +95,11 @@ promising one universal hardware minimum. - Micro/Matter modules for grains, cells, swarms, membranes, spectral tissue, quanta, microscope analysis, saved matter profiles, biomes, and incubated evolution. +- A separate Cosmoaudition module category with cosmic, Earth, biosphere, + human-machine, relational, event, semantic, uncertainty, archive, mapping, + and matter-processing modules. +- Optional MASA 0.1 sidecars for successful generations and Matter Analysis + artifacts. Sonic Lineage `sound_id` remains GERM's canonical identity. - Wavetable Forge conversion, prompt, mutation, render, import/export, and audition routes. - LoRA loading and a persistent Strain registry for adapter identity, @@ -112,6 +122,22 @@ promising one universal hardware minimum. All three scales use the same module graph, semantic FX bridge, sessions, library, and lineage model. +## Sonic Matter Observatory integration + +GERM keeps the Observatory components distinct while making their contracts +usable inside the cultivation graph: + +| Component | Boundary in GERM | +| --- | --- | +| MASA 0.1 | Optional descriptive JSON sidecars under `output/masa/`; never replaces Sonic Lineage or changes a successful render into a failure. | +| MATERIA | Matter Analysis provides a bounded local analyzer informed by the shared measured / inferred / unavailable distinction; it is not a claim of listening. | +| Cosmoaudition System | A loopback-only, response-bounded HTTP bridge reads snapshots and source status. GERM never contacts observatory providers directly. | + +Cosmoaudition mappings are operator-authored control relations. They do not +claim that a dataset is the literal voice or identity of a source. Missing or +unfetched observations do not silently become zero or a neutral modulation; +the route remains inactive until its state is explicit. + ## Listening Stack integration **Oída hears. GERM cultivates. Akousmata remembers. AKOÚŌ structures. Earworm @@ -211,7 +237,8 @@ The complete request and response contract is in | Models | `/models`, `/models/load`, `/diagnostics`, `/huggingface/status` | | Listening | `/listener/enhance`, `/listener/score`, `/listener/relisten` | | Memory | `/earworm/export`, `/import`, `/akousma/*` | -| Micro/Matter | `/micro/matter-profile`, `/micro/biomes/*` | +| Micro/Matter | `/micro/matter-profile`, `/matter/analyze`, `/micro/matter-analysis`, `/micro/biomes/*` | +| Cosmoaudition | `/cosmoaudition/status`, `/cosmoaudition/modules`, `/cosmoaudition/sources`, `/cosmoaudition/snapshot`, `/cosmoaudition/map`, `/cosmoaudition/archives/*` | | Strains | `/strains/*`, `/lora/load`, `/lora/strength` | | Wavetables | `/wavetables/*` | | Control | `/control/*` | @@ -269,6 +296,9 @@ curl -X POST http://127.0.0.1:5178/generate \ derived material to their provider. The default mock route stays local. - GERM performs bounded signal checks but does not replace Oída's listening and claim-accountability layer. +- Cosmoaudition live data requires the separate local Cosmoaudition System; + fixture mode and archived observations remain available without granting + GERM direct provider access. The [roadmap](ROADMAP.md) identifies current research priorities and non-goals. diff --git a/ROADMAP.md b/ROADMAP.md index b673694..2f0657e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # GERM Roadmap -GERM 0.2.5 is a public alpha and open research release. This roadmap names the +GERM 0.3.0 is a public alpha and open research release. This roadmap names the work in view without promising dates or production stability. ## Working Now diff --git a/apps/macos/script/build_and_run.sh b/apps/macos/script/build_and_run.sh index ffd3da7..16708fe 100755 --- a/apps/macos/script/build_and_run.sh +++ b/apps/macos/script/build_and_run.sh @@ -6,8 +6,8 @@ APP_NAME="germ" EXECUTABLE_NAME="germ-macos" BUNDLE_ID="org.sonicfield.germ" MIN_SYSTEM_VERSION="13.0" -MARKETING_VERSION="0.2.5" -BUNDLE_VERSION="2" +MARKETING_VERSION="0.3.0" +BUNDLE_VERSION="3" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Keep the runnable bundle where repository users expect to find apps. The diff --git a/dashboard/static/app.js b/dashboard/static/app.js index c221a20..2d2a48f 100644 --- a/dashboard/static/app.js +++ b/dashboard/static/app.js @@ -1,6 +1,6 @@ import { escapeHtml, iconSvg } from "./ui_utils.js"; import { initOneBitDish } from "./dish.js?v=20260706-engine-p1"; -import { createGermSynthEngine } from "./wavetable_synth.js?v=20260706-engine-p1"; +import { createGermSynthEngine } from "./wavetable_synth.js?v=20260731-audit-p1"; import { SMOOTH_FAST, SMOOTH_UI, @@ -15,7 +15,15 @@ import { createGateNode, createWavRecorder, createVoicePool, -} from "./audio_engine.js?v=20260706-engine-p1"; +} from "./audio_engine.js?v=20260731-audit-p1"; +import { + COSMOAUDITION_MODULATOR_TYPES, + finiteNumberOrNull, + cosmoauditionSelectedSignal, + cosmoauditionUnitFor, + cosmoauditionPreviousOutput, + cosmoauditionMappingSignal, +} from "./cosmoaudition.js?v=20260731-audit-p1"; /* Appearance preferences ------------------------------------------------- Oída and germ share the same warm-neutral surface system. Germ keeps one @@ -250,7 +258,7 @@ let canvasPendingSourcePosition = null; let canvasConnectionDraft = null; let canvasToolsAnchor = null; let rackSelectedKeys = new Set(); -const CANVAS_SOURCE_MENU_TABS = new Set(["core", "time", "micro", "fx", "modulators", "genetic"]); +const CANVAS_SOURCE_MENU_TABS = new Set(["core", "time", "micro", "cosmoaudition", "fx", "modulators", "genetic"]); let canvasSourceMenuTab = CANVAS_SOURCE_MENU_TABS.has(localStorage.getItem("germinator-source-menu-tab")) ? localStorage.getItem("germinator-source-menu-tab") : "core"; @@ -334,6 +342,7 @@ const MODULATOR_TYPES = new Set([ "audio_to_control", "gesture_recorder", "macro_modulator", + ...COSMOAUDITION_MODULATOR_TYPES, ]); const PROMPT_MODULATOR_TYPES = new Set(["prompt_modulator", "prompt_morph"]); const GENERATION_VALUE_MODULATOR_TYPES = new Set([ @@ -355,6 +364,7 @@ const GENERATION_VALUE_MODULATOR_TYPES = new Set([ "region_envelope", "audio_to_control", "gesture_recorder", + ...COSMOAUDITION_MODULATOR_TYPES, ]); const REALTIME_VALUE_MODULATOR_TYPES = new Set([ "lfo_modulator", @@ -370,6 +380,7 @@ const REALTIME_VALUE_MODULATOR_TYPES = new Set([ "spectral_follower", "audio_to_control", "gesture_recorder", + ...COSMOAUDITION_MODULATOR_TYPES, ]); const CLOCKED_VALUE_MODULATOR_TYPES = new Set([ "lfo_modulator", @@ -386,6 +397,7 @@ const CLOCKED_VALUE_MODULATOR_TYPES = new Set([ "region_envelope", "audio_to_control", "gesture_recorder", + ...COSMOAUDITION_MODULATOR_TYPES, ]); const PROMPT_STACK_LAYERS = [ { key: "material", label: "Material" }, @@ -532,7 +544,6 @@ const WAVE_REGION_TYPE_ALIASES = { preserve_region: "preserve", }; const WAVE_REGION_MASK_TYPES = new Set(["mask", "variation", "texture", "silence", "bridge"]); -const WAVE_REGION_PROTECT_TYPES = new Set(["preserve", "accent"]); const GENETIC_MODULE_TYPES = new Set(["identity_extractor", "generation_sequencer"]); const GENETIC_IDENTITY_TRAITS = { rhythm: { @@ -763,7 +774,9 @@ const FX_MODULES = { spectral_tissue: { label: "Spectral Tissue", description: "Freeze, smear, mask, and reorganize spectra" }, quanta: { label: "Quanta", description: "Gabor-style micro-event generator" }, microscope: { label: "Microscope", description: "Inspect transients, partials, density, and grain behavior" }, + matter_analysis: { label: "Matter Analysis", description: "Measure spectral, temporal, spatial, and inferred material qualities without altering audio" }, incubator: { label: "Incubator", description: "Slowly evolve a sound population over time" }, + cosmo_matter_modulator: { label: "Matter Modulator", description: "Route observatory control into granular, spectral, and temporal sound-matter processing" }, loop_doctor: { label: "Loop Doctor", description: "Loop detection, seam repair, and cyclic export" }, space: { label: "Space", description: "Simple reverb modes" }, echo: { label: "Echo", description: "Delay modes and feedback" }, @@ -782,6 +795,7 @@ const MICRO_FX_TYPES = new Set([ "quanta", "microscope", "incubator", + "cosmo_matter_modulator", ]); const MICRO_FX_DEFAULTS = { grain_culture: { grainSizeMs: 35, density: 0.62, selection: 0.7, mix: 0.45 }, @@ -795,6 +809,7 @@ const MICRO_FX_DEFAULTS = { quanta: { rateHz: 18, durationMs: 28, frequencyScatter: 0.35, mix: 0.45 }, microscope: { zoom: 0.64, transientFocus: 0.6, partialFocus: 0.45, mix: 0.3 }, incubator: { generation: 0.5, mutation: 0.38, timeScale: 0.55, mix: 0.48 }, + cosmo_matter_modulator: { density: 0.5, spread: 0.45, rateHz: 12, smoothingMs: 180, mix: 0.42 }, }; const MICRO_FX_CONTROL_META = { grainSizeMs: { label: "Size", min: 8, max: 240, step: 1 }, @@ -982,6 +997,22 @@ const FX_SEMANTIC_PROFILES = { batchSpread: Number(params.generation ?? 0.5) * 0.16 * amount, }), }, + matter_analysis: { + family: "analysis", + prompt: () => "", + negative: () => "", + generation: () => ({}), + }, + cosmo_matter_modulator: { + family: "cosmoaudition", + prompt: ({ params }) => `observatory-shaped sound matter, ${Number(params.density ?? 0.5) > 0.6 ? "dense" : "open"} material pulses, bounded planetary modulation`, + negative: () => "literal one-to-one data sonification, source identity claim", + generation: ({ params, amount }) => ({ + inpaintDensity: Number(params.density ?? 0.5) * 0.18 * amount, + seedDrift: Number(params.spread ?? 0.45) * 0.14 * amount, + continuationDivergence: Math.min(1, Number(params.rateHz ?? 12) / 80) * 0.12 * amount, + }), + }, loop_doctor: { family: "loop repair", prompt: ({ params }) => { @@ -2055,23 +2086,6 @@ function updateModels(preserve = true) { updateHomeReadouts(); } -function parseRanges(value) { - return value - .split(/[;\n]/) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const parts = line.split(",").map((part) => Number(part.trim())); - if (parts.length !== 2 || !Number.isFinite(parts[0]) || !Number.isFinite(parts[1])) { - throw new Error(`Invalid inpaint range: ${line}`); - } - if (parts[1] <= parts[0]) { - throw new Error(`Invalid inpaint range, end must be greater than start: ${line}`); - } - return parts; - }); -} - function loraPayload() { const textStrength = Number.isFinite(Number($("loraStrength")?.value)) ? Number($("loraStrength").value) : null; const manual = $("loraPaths") @@ -3554,9 +3568,6 @@ function updatePetriPagination(page, totalPages) { Chamber — lineage-aware generative graph =================================================================== */ -const canvasLegacyStarterPrompt = "granular metallic insect texture, close mic, dry, irregular transient clusters"; -const canvasLegacyStarterNegative = "speech, vocals, melody, clipping, harsh artifact"; - function canvasId(prefix) { return `${prefix}-${crypto.randomUUID().slice(0, 8)}`; } @@ -3742,6 +3753,17 @@ function modulatorLabel(modulatorType) { audio_to_control: "Audio-to-Control", gesture_recorder: "Gesture Recorder", macro_modulator: "Macro", + cosmo_observation: "Observatory Source", + cosmo_cosmic_field: "Cosmic Field", + cosmo_earth_field: "Earth Field", + cosmo_biosphere_field: "Biosphere Field", + cosmo_human_machine_field: "Human–Machine Field", + cosmo_relational_index: "Relational Index", + cosmo_event_pulsar: "Event Pulsar", + cosmo_mapping_loom: "Mapping Loom", + cosmo_semantic_field: "Semantic Field", + cosmo_uncertainty_field: "Uncertainty Field", + cosmo_observation_archive: "Observation Archive", }[modulatorType] || "Modulator"; } @@ -4262,6 +4284,39 @@ function normalizeModulatorNode(node) { macro_modulator: { amount: 50, }, + cosmo_observation: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "all", + }, + cosmo_cosmic_field: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "cosmos", + }, + cosmo_earth_field: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "earth", + }, + cosmo_biosphere_field: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "biosphere", + }, + cosmo_human_machine_field: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "human-machine", + }, + cosmo_relational_index: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "all", + }, + cosmo_event_pulsar: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "all", + }, + cosmo_mapping_loom: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "all", missingData: "hold-explicitly", previousValue: null, + }, + cosmo_semantic_field: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "all", + }, + cosmo_uncertainty_field: { + mode: "fixture", latitude: 4.711, longitude: -74.0721, signalId: "", currentValue: null, available: false, status: "not fetched", sphere: "all", + }, + cosmo_observation_archive: { + mode: "archive", signalId: "", currentValue: null, available: false, status: "no archive", sphere: "all", archiveId: "", + }, }; const defaults = configDefaults[modulatorType] || configDefaults.macro_modulator; const routes = modulatorType === "mod_matrix" @@ -4275,34 +4330,39 @@ function normalizeModulatorNode(node) { const gesturePoints = Array.isArray(node.config?.points) && node.config.points.length ? node.config.points : defaults.points || []; + const normalizedConfig = { + ...defaults, + ...(node.config || {}), + ...(modulatorType === "mod_matrix" ? { + matrixRoutes: matrixRoutes.map((route) => ({ + id: route.id || canvasId("matrix"), + sourceType: route.sourceType || "lfo_modulator", + targetNodeId: route.targetNodeId || "", + targetPath: route.targetPath || "", + amount: Math.min(1, Math.max(0, Number(route.amount ?? 0.35))), + curve: route.curve || "linear", + enabled: route.enabled !== false, + config: { seed: 1, ...(route.config || {}) }, + })), + } : {}), + ...(modulatorType === "gesture_recorder" ? { + points: gesturePoints + .map((point) => ({ + t: Math.min(1, Math.max(0, Number(point.t ?? 0))), + value: Math.min(1, Math.max(0, Number(point.value ?? 0.5))), + })) + .sort((a, b) => a.t - b.t), + } : {}), + }; + if (COSMOAUDITION_MODULATOR_TYPES.has(modulatorType)) { + normalizedConfig.available = normalizedConfig.available === true + && finiteNumberOrNull(normalizedConfig.currentValue) !== null; + } return { ...node, modulatorType, label: node.label || modulatorLabel(modulatorType), - config: { - ...defaults, - ...(node.config || {}), - ...(modulatorType === "mod_matrix" ? { - matrixRoutes: matrixRoutes.map((route) => ({ - id: route.id || canvasId("matrix"), - sourceType: route.sourceType || "lfo_modulator", - targetNodeId: route.targetNodeId || "", - targetPath: route.targetPath || "", - amount: Math.min(1, Math.max(0, Number(route.amount ?? 0.35))), - curve: route.curve || "linear", - enabled: route.enabled !== false, - config: { seed: 1, ...(route.config || {}) }, - })), - } : {}), - ...(modulatorType === "gesture_recorder" ? { - points: gesturePoints - .map((point) => ({ - t: Math.min(1, Math.max(0, Number(point.t ?? 0))), - value: Math.min(1, Math.max(0, Number(point.value ?? 0.5))), - })) - .sort((a, b) => a.t - b.t), - } : {}), - }, + config: normalizedConfig, routes: routes.map((route) => ({ id: route.id || canvasId("route"), targetNodeId: route.targetNodeId || "", @@ -4314,6 +4374,131 @@ function normalizeModulatorNode(node) { }; } +function cosmoauditionNodeProvenance(node) { + if (!COSMOAUDITION_MODULATOR_TYPES.has(node?.modulatorType)) return null; + return { + contract: "cosmoaudition-germ/v0.1", + module: node.modulatorType, + mode: node.config?.mode || "fixture", + generated_at: node.config?.generatedAt || null, + signal_id: node.config?.signalId || null, + signal_label: node.config?.signalLabel || null, + source_id: node.config?.sourceId || null, + sphere: node.config?.sphere || null, + confidence: node.config?.confidence || null, + epistemic_status: node.config?.epistemicStatus || null, + temporal_character: node.config?.temporalCharacter || null, + mapping_decision: node.config?.mappingDecision || null, + value: node.config?.available === true ? finiteNumberOrNull(node.config?.currentValue) : null, + available: node.config?.available === true, + }; +} + +async function refreshCosmoauditionNode(nodeId, { archive = false } = {}) { + const index = canvasNodes.findIndex((item) => item.id === nodeId); + const node = normalizeModulatorNode(canvasNodes[index]); + if (!node || !COSMOAUDITION_MODULATOR_TYPES.has(node.modulatorType)) return; + beginWork(archive ? "Archiving Observation" : "Reading Observatory", node.label); + try { + let payload; + let archiveId = node.config?.archiveId || ""; + if (node.modulatorType === "cosmo_observation_archive") { + const listing = await api("/cosmoaudition/archives"); + const summary = (listing.archives || []).find((item) => item.id === archiveId) || listing.archives?.[0]; + if (!summary) throw new Error("No observation archive is available yet."); + archiveId = summary.id; + const artifact = await api(`/cosmoaudition/archives/${encodeURIComponent(summary.id)}`); + payload = artifact.snapshot || {}; + } else { + const query = new URLSearchParams({ mode: node.config?.mode === "live" ? "live" : "fixture" }); + const latitude = finiteNumberOrNull(node.config?.latitude); + const longitude = finiteNumberOrNull(node.config?.longitude); + if (latitude !== null) query.set("lat", String(latitude)); + if (longitude !== null) query.set("lon", String(longitude)); + const response = await api(`/cosmoaudition/snapshot?${query.toString()}`); + if (!response.available || !response.payload) throw new Error(response.error || "Cosmoaudition System is unavailable."); + payload = response.payload; + if (archive) { + const saved = await api("/cosmoaudition/archives", { + method: "POST", + body: JSON.stringify({ + label: `${node.label} ${payload.generatedAt || new Date().toISOString()}`, + module: node.modulatorType, + snapshot: payload, + notes: "Archived from the GERM Cosmoaudition module; mapping remains authored, not literal sonification.", + }), + }); + archiveId = saved.id || archiveId; + } + } + const signals = Array.isArray(payload.signals) ? payload.signals : []; + const selected = cosmoauditionSelectedSignal(node, signals); + let currentValue = cosmoauditionUnitFor(node, signals, selected, (signal) => + deterministicUnit(`${signal.label || ""}:${signal.notes || ""}:${signal.epistemicStatus || ""}`)); + let mappingDecision = null; + const previousOutput = cosmoauditionPreviousOutput(node); + if (node.modulatorType === "cosmo_mapping_loom") { + const signalId = selected?.id || String(node.config?.signalId || "").trim() || "unavailable-signal"; + mappingDecision = await api("/cosmoaudition/map", { + method: "POST", + body: JSON.stringify({ + mapping: { + id: `${node.id}:mapping`, + signalId, + layer: selected?.layer || "earth", + target: "germ:control", + scale: "linear", + inputRange: [0, 1], + outputRange: [0, 1], + smoothingMs: 120, + missingData: node.config?.missingData || "hold-explicitly", + description: "Operator-authored Cosmoaudition to GERM control route.", + epistemicNote: "This control relation is authored; it is not the source's voice or an identity claim.", + }, + signal: cosmoauditionMappingSignal(selected), + previousOutput, + amount: 1, + }), + }); + currentValue = finiteNumberOrNull(mappingDecision.outputValue); + } + const available = finiteNumberOrNull(currentValue) !== null; + const boundedValue = available ? Math.min(1, Math.max(0, Number(currentValue))) : null; + node.config = { + ...(node.config || {}), + ...(node.modulatorType === "cosmo_mapping_loom" ? { + previousValue: available ? boundedValue : previousOutput, + } : {}), + currentValue: available ? boundedValue : finiteNumberOrNull(node.config?.currentValue), + available, + status: mappingDecision?.status || (available ? "observed" : "unavailable"), + generatedAt: payload.generatedAt || null, + signalId: selected?.id || node.config?.signalId || "", + signalLabel: selected?.label || "No matching signal", + signalUnit: selected?.unit || "", + sourceId: selected?.sourceId || null, + confidence: selected?.confidence || null, + epistemicStatus: selected?.epistemicStatus || null, + temporalCharacter: selected?.temporalCharacter || null, + sphere: selected?.sphere || node.config?.sphere || "all", + observationCount: signals.length, + archiveId, + mappingDecision, + }; + canvasNodes[index] = node; + selectedCanvasNodeId = node.id; + canvasSaveState(); + renderCanvas(); + finishWork(archive ? "Observation Archived" : "Observation Ready", available ? "ok" : "muted", selected?.label || node.config.status); + } catch (error) { + node.config = { ...(node.config || {}), available: false, status: String(error?.message || error) }; + canvasNodes[index] = node; + canvasSaveState(); + renderCanvas(); + finishWork("Observatory Unavailable", "bad", node.config.status); + } +} + function modulationIntensityCount(intensity = 25) { const value = Number(intensity) || 25; if (value >= 75) return 3; @@ -4634,6 +4819,12 @@ function modulationGestureUnit(modulator, context = {}) { function modulationNumericUnit(modulator, route, target, baseValue, context = {}) { const type = modulator?.modulatorType || "macro_modulator"; const seed = `${modulator?.id}:${route?.id}:${modulator?.config?.seed ?? route?.config?.seed ?? 1}:${target?.path}:${context.eventIndex ?? ""}:${context.tick ?? ""}:${baseValue ?? ""}`; + if (COSMOAUDITION_MODULATOR_TYPES.has(type)) { + const observed = finiteNumberOrNull(modulator.config?.currentValue); + return modulator.config?.available === true && observed !== null + ? Math.min(1, Math.max(0, observed)) + : 0.5; + } if (type === "macro_modulator") return Math.min(1, Math.max(0, Number(modulator.config?.amount ?? 50) / 100)); if (type === "probability_modulator") { const chance = Math.min(1, Math.max(0, Number(modulator.config?.chance ?? 75) / 100)); @@ -4846,6 +5037,7 @@ function canvasApplyGenerationModulators(base = {}, context = {}) { }; canvasModulatorNodes().map(normalizeModulatorNode).forEach((modulator) => { if (modulator.modulatorType === "mod_matrix") return; + if (COSMOAUDITION_MODULATOR_TYPES.has(modulator.modulatorType) && modulator.config?.available !== true) return; (modulator.routes || []).forEach((route) => { if (!modulationRouteMatches(route, context)) return; const target = modulationTargetForRoute(route); @@ -4859,6 +5051,9 @@ function canvasApplyGenerationModulators(base = {}, context = {}) { target_path: target.path, target_label: target.label, seed: modulator.config.seed ?? route.config?.seed ?? null, + ...(COSMOAUDITION_MODULATOR_TYPES.has(modulator.modulatorType) + ? { cosmoaudition: cosmoauditionNodeProvenance(modulator) } + : {}), }; if (PROMPT_MODULATOR_TYPES.has(modulator.modulatorType) && ["prompt", "negative"].includes(target.type)) { const baseValue = target.type === "negative" ? result.negativePrompt : result.prompt; @@ -5388,11 +5583,6 @@ function canvasRegionIsGenerativeMask(region) { return WAVE_REGION_MASK_TYPES.has(type) || canvasRegionConfig(type).inpaint === true; } -function canvasRegionIsProtected(region) { - const type = canvasRegionType(region); - return WAVE_REGION_PROTECT_TYPES.has(type) || canvasRegionConfig(type).locked === true; -} - function canvasRegionForPurpose(node, purpose) { return (node?.regions || []).find((region) => { if (region.purpose === purpose) return true; @@ -6189,9 +6379,12 @@ async function runCanvasConnectChoice(button) { function canvasSourcePosition(offset = {}) { const base = canvasPendingSourcePosition || canvasBoardDefaultPoint(); + const board = $("canvasBoard"); + const viewportMinX = board ? (board.scrollLeft / canvasZoom) + 12 : 8; + const viewportMinY = board ? (board.scrollTop / canvasZoom) + 12 : 8; return { - x: Math.max(8, base.x + (offset.x || 0)), - y: Math.max(8, base.y + (offset.y || 0)), + x: Math.max(8, viewportMinX, base.x + (offset.x || 0)), + y: Math.max(8, viewportMinY, base.y + (offset.y || 0)), }; } @@ -6256,16 +6449,6 @@ function applyCanvasViewport() { if ($("canvasZoomReadout")) $("canvasZoomReadout").textContent = `${Math.round(canvasZoom * 100)}%`; } -function canvasIsLegacyStarterNode(node) { - return Boolean( - node?.type === "prompt" - && (node.prompt === canvasLegacyStarterPrompt || !node.prompt) - && (node.negativePrompt === canvasLegacyStarterNegative || !node.negativePrompt) - && !canvasAssets.length - && !canvasEdges.length, - ); -} - function canvasCreatePromptNode({ text = "", negative = "", x = 80, y = 88, handoff = null, relisten = null } = {}) { pushUndo(); const node = { @@ -7340,6 +7523,7 @@ function canvasCreateTimeNode(timeType = "colony_sequencer", { x = null, y = nul } function canvasDefaultFxParams(type, targetNode = null) { + if (type === "matter_analysis") return { fftSize: 1024, maxFrames: 32 }; if (MICRO_FX_TYPES.has(type)) return { ...MICRO_FX_DEFAULTS[type] }; return { gain: { amount: 1 }, @@ -7365,6 +7549,7 @@ function canvasCreateFxNode(type = "gain", { x = null, y = null, targetNode = nu const target = targetNode || canvasSelectedSoundNode(); const point = target ? canvasSnappedRightPoint(target) : canvasBoardDefaultPoint(); const isMicroFx = MICRO_FX_TYPES.has(type); + const isMatterAnalysis = type === "matter_analysis"; const node = { id: canvasId("node"), projectId: activeCulture.id, @@ -7372,11 +7557,11 @@ function canvasCreateFxNode(type = "gain", { x = null, y = null, targetNode = nu fxType: type, x: x ?? point.x + (target ? 16 : 0), y: y ?? point.y, - width: type === "filter" ? 300 : type === "granular" || type === "loop_doctor" || isMicroFx ? 284 : 236, - height: type === "filter" ? 226 : type === "granular" || type === "loop_doctor" || isMicroFx ? 226 : 178, + width: type === "filter" ? 300 : type === "granular" || type === "loop_doctor" || isMicroFx || isMatterAnalysis ? 284 : 236, + height: type === "filter" ? 226 : type === "granular" || type === "loop_doctor" || isMicroFx || isMatterAnalysis ? 240 : 178, label: fx.label, params: canvasDefaultFxParams(type, target), - semantic: { enabled: true, amount: 1 }, + semantic: { enabled: !isMatterAnalysis, amount: 1 }, targetNodeId: target?.id || null, }; canvasNodes.push(node); @@ -7520,10 +7705,10 @@ function canvasApplySemanticFxLayers(result, context = {}) { function canvasModulatorNodeSize(modulatorType = "prompt_modulator") { const normalizedType = normalizeModulatorType(modulatorType); - const wide = ["mod_matrix", "prompt_modulator", "prompt_morph", "lfo_modulator", "envelope_modulator", "step_sequencer_modulator", "gesture_recorder"].includes(normalizedType); + const wide = ["mod_matrix", "prompt_modulator", "prompt_morph", "lfo_modulator", "envelope_modulator", "step_sequencer_modulator", "gesture_recorder"].includes(normalizedType) || COSMOAUDITION_MODULATOR_TYPES.has(normalizedType); return { width: wide ? 430 : 390, - height: normalizedType === "prompt_morph" ? 540 : normalizedType === "mod_matrix" ? 390 : normalizedType === "prompt_modulator" ? 420 : normalizedType === "gesture_recorder" ? 390 : wide ? 360 : 320, + height: normalizedType === "prompt_morph" ? 540 : normalizedType === "mod_matrix" ? 390 : normalizedType === "prompt_modulator" ? 420 : normalizedType === "gesture_recorder" ? 390 : COSMOAUDITION_MODULATOR_TYPES.has(normalizedType) ? 430 : wide ? 360 : 320, }; } @@ -8314,25 +8499,6 @@ function canvasResetGraph() { renderCanvas(); } -function canvasNodeOriginClass(origin) { - return { - prompt: "origin-prompt", - upload: "origin-upload", - library: "origin-library", - recording: "origin-recording", - audio_snapshot: "origin-recording", - continuation: "origin-continuation", - inpaint: "origin-inpaint", - audio_to_audio: "origin-mutate", - mixdown: "origin-mix", - extract: "origin-extract", - audio_tool: "origin-extract", - time_pitch: "origin-extract", - time_render: "origin-mix", - time_one_shot: "origin-prompt", - }[origin] || "origin-library"; -} - function fxModeButton(node, mode, label) { const active = node.params?.mode === mode ? " active" : ""; return ``; @@ -8441,6 +8607,29 @@ function canvasFxControlsMarkup(node) {
`; } + if (node.fxType === "matter_analysis") { + const analysis = node.analysis?.analysis || {}; + const spectral = analysis.spectral || {}; + const morphology = analysis.morphology || {}; + const masaStatus = node.analysis + ? (node.analysis.masa?.status || (node.analysis.masa_sidecar_file ? "written" : "unavailable")) + : "not analyzed"; + return ` +Observations become attributed, bounded control material through authored mappings—not claims that data is a source's literal voice.
+ + + + + + + + + + + + + +