0xray 4.0.14 → 4.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/init.sh +14 -4
  3. package/.opencode/skills/orchestrator/SKILL.md +20 -1
  4. package/.xray/features.json +2 -2
  5. package/AGENTS-consumer.md +10 -5
  6. package/AGENTS.md +10 -7
  7. package/README.md +8 -7
  8. package/SKILLS.md +3 -2
  9. package/dist/AGENTS.md +10 -7
  10. package/dist/CHANGELOG.md +71 -0
  11. package/dist/README.md +8 -7
  12. package/dist/SKILLS.md +3 -2
  13. package/dist/cli/commands/hermes-install.js +2 -1
  14. package/dist/cli/commands/openclaw-install.js +2 -1
  15. package/dist/cli/commands/opencode-install.js +2 -1
  16. package/dist/cli/commands/validate.d.ts +26 -0
  17. package/dist/cli/commands/validate.js +164 -0
  18. package/dist/cli/index.js +6 -13
  19. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +11 -0
  20. package/dist/integrations/grok/grok-cli.js +2 -1
  21. package/dist/integrations/hooks/station-hook-runtime.cjs +23 -2
  22. package/dist/integrations/hooks/station-hook-runtime.mjs +2 -0
  23. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  24. package/dist/reporting/log-parser.js +2 -2
  25. package/dist/skills/orchestrator/SKILL.md +20 -1
  26. package/llms.txt +8 -7
  27. package/package.json +2 -1
  28. package/scripts/foundry/CHANGELOG.md +33 -1
  29. package/scripts/foundry/README.md +12 -5
  30. package/scripts/foundry/assert-packed-dist-cli.mjs +6 -0
  31. package/scripts/foundry/blip-kapow.cjs +2 -0
  32. package/scripts/foundry/blip-looker.cjs +2 -0
  33. package/scripts/foundry/blip-render.cjs +2 -1237
  34. package/scripts/foundry/blip-rippel-organs.cjs +2 -0
  35. package/scripts/foundry/blip-rippel.cjs +2 -1963
  36. package/scripts/foundry/blip-scene.cjs +2 -0
  37. package/scripts/foundry/blip-stroke.cjs +2 -0
  38. package/scripts/foundry/blip-vibe.cjs +2 -0
  39. package/scripts/foundry/blip.mjs +23 -97
  40. package/scripts/foundry/cli.mjs +2 -2
  41. package/scripts/foundry/inspect.mjs +62 -19
  42. package/scripts/foundry/mill-plant.cjs +598 -0
  43. package/scripts/foundry/mint-suit.cjs +189 -99
  44. package/scripts/foundry/package.json +12 -1
  45. package/scripts/foundry/plant/agents/blip-inspect.yml +1 -1
  46. package/scripts/foundry/plant/agents/blip-looker.yml +5 -0
  47. package/scripts/foundry/plant/agents/blip-vibe.yml +5 -0
  48. package/scripts/foundry/plant/agents/blip.yml +1 -1
  49. package/scripts/foundry/plant/agents/inspect.yml +1 -1
  50. package/scripts/foundry/plant/agents/mill.yml +1 -1
  51. package/scripts/foundry/plant/agents/sound-inspect.yml +1 -1
  52. package/scripts/foundry/plant/agents/sound-mixer.yml +5 -0
  53. package/scripts/foundry/plant/agents/sound.yml +1 -1
  54. package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +15 -9
  55. package/scripts/foundry/plant/motions/registry.json +5 -4
  56. package/scripts/foundry/plant/skills/blip/SKILL.md +4 -4
  57. package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +2 -2
  58. package/scripts/foundry/plant/skills/blip-looker/SKILL.md +30 -0
  59. package/scripts/foundry/plant/skills/blip-vibe/SKILL.md +30 -0
  60. package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
  61. package/scripts/foundry/plant/skills/sound/SKILL.md +3 -1
  62. package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +1 -1
  63. package/scripts/foundry/plant/skills/sound-mixer/SKILL.md +28 -0
  64. package/scripts/foundry/sound-bed.cjs +2 -454
  65. package/scripts/foundry/sound-mixer.cjs +2 -0
  66. package/scripts/foundry/sound-rippel.cjs +2 -1434
  67. package/scripts/foundry/sound-taste.cjs +2 -0
  68. package/scripts/foundry/sound-voice.cjs +2 -0
  69. package/scripts/foundry/sound.mjs +23 -81
  70. package/scripts/node/install-bridges.cjs +39 -4
  71. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  72. package/src/skills/orchestrator/SKILL.md +20 -1
  73. package/xray/features.json +2 -2
@@ -20,7 +20,7 @@ Missing mp4, unreadable mp4, or a thrown probe is **FAIL**. Never pass on a miss
20
20
  5. **Audio.** Stream present. Silent mp4 = FAIL. `--bed` or auto sound mill.
21
21
  6. **Resolution.** Every live blip ≥720p (`1280×720`), including the Power Plant ident (`still`).
22
22
 
23
- v0 ids: `still` · `orb` · `swirl` · `snap` · `waves` · `spark`. `kapow` is a growth stub (FAIL until a renderer ships). Unknown id FAIL. Receipt records the Power Plant palette.
23
+ v0 ids: `still` · `orb` · `swirl` · `snap` · `waves` · `spark`. `kapow` is a design opt (two-tier stamp on the stanza). Unknown id FAIL. Receipt records the Power Plant palette.
24
24
 
25
25
  Receipt: `.xray/blip/receipt.json`. Mp4 default: `.xray/blip/blip.mp4`.
26
26
 
@@ -29,4 +29,4 @@ npx @0xray/foundry blip render --brief "warm basement swirl" --mode motion:swirl
29
29
  npx @0xray/foundry blip inspect
30
30
  ```
31
31
 
32
- Plant-vs-worn allowlists `blip` + `blip-inspect` when `"plant": "blip"`. They are not a costume dump. Mill may be off on this seat.
32
+ Plant-vs-worn allowlists `blip` + `blip-inspect` + `blip-vibe` when `"plant": "blip"`. They are not a costume dump. Mill may be off on this seat.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: blip-looker
3
+ description: Factory-blip looker. Friend hitting replay. Hook → jewel → hold. Ship bar is 8. Not vibe density. Not last-blip inspect. Not costume.
4
+ ---
5
+
6
+ # Blip looker
7
+
8
+ This plant **fastens** the replay seat. Inspect quotes file / duration. Vibe quotes stamp density. Mixer quotes ears. This organ asks: **would a friend hit replay?**
9
+
10
+ It is not a model. It paints hook, turn, and tag, then scores the Short. Fail closed under 8.
11
+
12
+ **Seat:** fastened with `blip` + `blip-inspect` + `blip-vibe` when `foundry.json` `"plant": "blip"`. Not a 45-skill dump.
13
+
14
+ **Types:** `still` · `orb` · `swirl` · `snap` · `waves` · `spark` · `kapow`. Same agent. Same bar.
15
+
16
+ ## Spine
17
+
18
+ 1. **Look** — setup, jewel cut, hold. Three frames, not a poster.
19
+ 2. **Score** — setup, punch, hold, motion (hook≠turn≠tag), jewel, replay. Mean is 0–10. Kapow also needs the word on the cut.
20
+ 3. **Bar** — **8 to ship**. Under 8 is FAIL. Do not weaken the bar. Still’s jewel is the plate change, not a brighter frame.
21
+ 4. **Receipt** — `.xray/blip-looker-receipt.json` (`PASS` / `FAIL` + score + `byType`).
22
+
23
+ ```bash
24
+ npx @0xray/foundry blip look
25
+ npx @0xray/foundry blip replay
26
+ ```
27
+
28
+ A FAIL score is a phrase bug. The turn must hit. The tag must hold. Do not lower the bar.
29
+
30
+ A friend would hit replay: the hook sets a stamp, the jewel punches, the tag does not die.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: blip-vibe
3
+ description: Factory-blip vibe organ. Aesthetic, appeal, cool factor. Ship bar is 8. Not last-blip inspect. Not costume.
4
+ ---
5
+
6
+ # Blip vibe
7
+
8
+ This plant **fastens** the appeal seat. Inspect quotes duration / mode / file. This organ **scores the picture** before a mux ships.
9
+
10
+ videoReview can see and cannot hear. Do not sign a mix from this seat. Quote the score. Fail closed under 8.
11
+
12
+ **Seat:** fastened with `blip` + `blip-inspect` when `foundry.json` `"plant": "blip"`. Not a 45-skill dump.
13
+
14
+ **Types:** `still` · `orb` · `swirl` · `snap` · `waves` · `spark` · `kapow`. Same agent. Same bar.
15
+
16
+ ## Spine
17
+
18
+ 1. **Look** — is it a stamp a friend would replay, or a weak badge on void?
19
+ 2. **Score** — shared appeal: fill, chroma, contrast, outline, hook→turn flash, differ. Kapow adds word punch, two-tier pop, spike jag. Mean of those is 0–10.
20
+ 3. **Bar** — **8 to ship**. Under 8 is FAIL. Do not ship a cute daisy. Do not weaken the bar. Sparse organs are scored against their own fill mark, not a kapow disc.
21
+ 4. **Receipt** — `.xray/blip-vibe-receipt.json` (`PASS` / `FAIL` + score + `byType`).
22
+
23
+ ```bash
24
+ npx @0xray/foundry blip vibe
25
+ npx @0xray/foundry blip appeal
26
+ ```
27
+
28
+ A FAIL score is a picture bug. Sharpen the stamp. Do not weaken the bar.
29
+
30
+ A friend would see: Power Plant plates that live, Rippel five that hit the jewel cut, a comic two-tier burst (few irregular points, not a 20-needle firework) with `KAPOW!` you can read across the room.
@@ -12,8 +12,8 @@ Mill **does** these checks (`npx @0xray/foundry inspect`). Mint fails on a costu
12
12
  ## What mill inspects
13
13
 
14
14
  1. **Diff.** `git status` / `git diff --stat` of the milled cwd. Ignore the agent's summary if the diff disagrees.
15
- 2. **Plant vs worn.** `src/skills`, `src/opencode/agents` vs `.opencode/skills`, `.opencode/agents`. Same name: plant wins. Shop plant (`shop-extract`, `shop-witness`, `shop-pin`, plus `foundry.json` `shopPlant`) is first-class with mill plant. Sound plant (`sound`, `sound-inspect`) is first-class when `foundry.json` `"plant": "sound"` (mill may be off). Blip plant (`blip`, `blip-inspect`) is first-class when `foundry.json` `"plant": "blip"` (mill may be off). Costume dump (worn names they did not plant, mill did not fasten, sound did not fasten, blip did not fasten, and shop plant did not name) fails mint unless `foundry.json` has `"costume": true`.
16
- 3. **Mill receipt.** `.xray/foundry-inventory.json` — mill name/version, `suit` (`fastened` / `overlay` / `costume`), `plant`. Mill seats: `millPlant.skills` must include `mill` and `inspect`. Sound seats: `soundPlant.skills` must include `sound` and `sound-inspect`; last bed receipt is a separate check. Blip seats: `blipPlant.skills` must include `blip` and `blip-inspect`; last blip receipt is a separate check.
15
+ 2. **Plant vs worn.** `src/skills`, `src/opencode/agents` vs `.opencode/skills`, `.opencode/agents`. Same name: plant wins. Shop plant (`shop-extract`, `shop-witness`, `shop-pin`, plus `foundry.json` `shopPlant`) is first-class with mill plant. Sound plant (`sound`, `sound-inspect`, `sound-mixer`) is first-class when `foundry.json` `"plant": "sound"` (mill may be off). Blip plant (`blip`, `blip-inspect`) is first-class when `foundry.json` `"plant": "blip"` (mill may be off). Costume dump (worn names they did not plant, mill did not fasten, sound did not fasten, blip did not fasten, and shop plant did not name) fails mint unless `foundry.json` has `"costume": true`.
16
+ 3. **Mill receipt.** `.xray/foundry-inventory.json` — mill name/version, `suit` (`fastened` / `overlay` / `costume`), `plant`. Mill seats: `millPlant.skills` must include `mill` and `inspect`. Sound seats: `soundPlant.skills` must include `sound`, `sound-inspect`, and `sound-mixer`; last bed receipt is a separate check. Blip seats: `blipPlant.skills` must include `blip` and `blip-inspect`; last blip receipt is a separate check.
17
17
  4. **CI.** GitHub mill CI report (`.opencode/logs/ci-cd-monitor-report.json`), not a green check in chat. No token is a skip, not a pass invented.
18
18
  5. **Live PUT.** HTTP 200 on the npm `.tgz`. Metadata without a tarball is not shipped.
19
19
  6. **Isolated HOME.** Last-mile dest is project `.grok/plugins/0xray`. Isolated HOME may wear `$HOME/.grok/plugins/0xray` and must not write machine `~/.grok/plugins/0xray`. Shared HOME (passwd `$HOME`) does not last-wins clobber the machine plugin. Mill compares `$HOME` to the passwd home (`os.userInfo().homedir`), not `os.homedir()` (which follows `$HOME`).
@@ -24,6 +24,8 @@ npx @0xray/foundry sound inspect
24
24
  npx @0xray/foundry inspect --skip-live
25
25
  ```
26
26
 
27
- Mint fastens `sound` + `sound-inspect`. It does not dump mill inspect onto a sound-only seat. Chat is not a bed receipt.
27
+ Mint fastens `sound` + `sound-inspect` + `sound-mixer`. It does not dump mill inspect onto a sound-only seat. Chat is not a bed receipt. `sound mix` levels every genre × tempo × motif (hats / plate / glue). Inspect still quotes the last bed.
28
+
29
+ A 4.44s lock is a Short: seed motif (hook cell → turn answer → tag stamps hook[0]). The crash lands on the jewel cut (`phraseMarks.turnAt`), not a sectionGain fader. Six genre bodies stay tellable.
28
30
 
29
31
  A friend would hear: the sound mill mills Dist beds like Rippel’s prototype grew up — kick membranes, metal hats, and a crystal-clear mixer, not a muddy toy and not a 3-sine pad.
@@ -25,4 +25,4 @@ npx @0xray/foundry sound render --brief "warm basement pad" --genre jazz
25
25
  npx @0xray/foundry sound inspect
26
26
  ```
27
27
 
28
- Plant-vs-worn allowlists `sound` + `sound-inspect` when `"plant": "sound"`. They are not a costume dump. Mill may be off on this seat.
28
+ Plant-vs-worn allowlists `sound` + `sound-inspect` + `sound-mixer` when `"plant": "sound"`. They are not a costume dump. Mill may be off on this seat. Mixer is the variation tuner (`sound mix`); this skill still quotes the last bed.
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: sound-mixer
3
+ description: Factory-sound tuner. Level every genre × tempo × motif — hats, plate, glue, phrase. Not last-bed inspect. Not costume.
4
+ ---
5
+
6
+ # Sound mixer
7
+
8
+ This plant **fastens** the mixer seat. Inspect quotes the last bed. This organ **tunes every variation** before a mux ships.
9
+
10
+ videoReview cannot hear. Do not sign a mix from a picture pass. Run the matrix. Quote the numbers. Fail closed.
11
+
12
+ **Seat:** fastened with `sound` + `sound-inspect` when `foundry.json` `"plant": "sound"`. Not a 45-skill dump.
13
+
14
+ ## Spine
15
+
16
+ 1. **Enumerate** — six bodies (`ambient` `techno` `jazz` `phonk` `rock` `timeless`) × each tempo table × motif roots 0/2/4 on the 4.44s lock, plus one unlock seat per body.
17
+ 2. **Render** — same `renderSamples` the blip mux uses (`syncopate` on lock).
18
+ 3. **Level** — inspect (chop / levels / peak / hum) plus mixer seats: plate hang, hat air, turn/body, crest, peak. Every voice in the bus, not just the crash.
19
+ 4. **Receipt** — `.xray/sound-mix-receipt.json` (`PASS` / `FAIL` + every seat).
20
+
21
+ ```bash
22
+ npx @0xray/foundry sound mix
23
+ npx @0xray/foundry sound mixer
24
+ ```
25
+
26
+ A FAIL seat is a mix bug. Do not weaken inspect. Dry the plate, pull hot hats, ease glue, or lift a quiet body floor.
27
+
28
+ A friend would hear: hats sit, the jewel still hits, the room does not wash the next bar.
@@ -1,454 +1,2 @@
1
- /**
2
- * Factory-sound spine: brief → checksum seed → genre → headless wav → metrics gate.
3
- * Render is the Rippel prototype mill (membrane / metal / mixer), crystal-clear mix.
4
- * Not mill inspect.
5
- */
6
-
7
- const crypto = require("crypto");
8
- const fs = require("fs");
9
- const path = require("path");
10
- const rippel = require("./sound-rippel.cjs");
11
-
12
- const SPINE = 2;
13
- const SAMPLE_RATE = 44100;
14
- const DEFAULT_SECONDS = 4;
15
- const WINDOW_SEC = 0.25;
16
- const INTRO_SEC = 0.75;
17
- const FADE_SEC = 1;
18
- const PEAK_LIMIT = 0.95;
19
- const CHOP_MAX = 0.1;
20
- const CHOP_STRONG = 0.05;
21
- const LEVELS_RATIO_MIN = 1.5;
22
- const LEVELS_RATIO_MAX = 3;
23
- const BODY_MIN_RMS = 0.05;
24
- const RECEIPT_REL = path.join(".xray", "sound-bed-receipt.json");
25
- const WAV_REL = path.join(".xray", "sound", "bed.wav");
26
-
27
- const GENRE_ALIASES = rippel.GENRE_ALIASES;
28
- const GENRES = rippel.GENRES;
29
-
30
- function canonicalJson(value) {
31
- if (value === null || typeof value !== "object") return JSON.stringify(value);
32
- if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
33
- const keys = Object.keys(value).sort();
34
- return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
35
- }
36
-
37
- function seedFromBrief(brief, genre) {
38
- const payload = canonicalJson({
39
- brief: String(brief || "").trim(),
40
- genre: resolveGenre(genre).id,
41
- spine: SPINE,
42
- });
43
- const digest = crypto.createHash("sha256").update(payload, "utf8").digest("hex");
44
- return `0x${digest}`;
45
- }
46
-
47
- function mulberry32(seedU32) {
48
- let t = seedU32 >>> 0;
49
- return function next() {
50
- t += 0x6d2b79f5;
51
- let x = t;
52
- x = Math.imul(x ^ (x >>> 15), x | 1);
53
- x ^= x + Math.imul(x ^ (x >>> 7), x | 61);
54
- return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
55
- };
56
- }
57
-
58
- function seedU32(seedHex) {
59
- const hex = String(seedHex || "").replace(/^0x/, "").slice(0, 8);
60
- const n = Number.parseInt(hex, 16);
61
- return Number.isFinite(n) ? n >>> 0 : 1;
62
- }
63
-
64
- function resolveGenre(name) {
65
- return rippel.resolveGenre(name);
66
- }
67
-
68
- function renderSamples({ brief, genre, seconds, seed, syncopate }) {
69
- const dur = Number(seconds) > 0 ? Number(seconds) : DEFAULT_SECONDS;
70
- const g = resolveGenre(genre);
71
- const seedHex = seed || seedFromBrief(brief, g.id);
72
- const rng = mulberry32(seedU32(seedHex));
73
- return rippel.renderRippelBed({
74
- brief,
75
- genre,
76
- seconds: dur,
77
- seedHex,
78
- rng,
79
- sampleRate: SAMPLE_RATE,
80
- syncopate: Boolean(syncopate),
81
- });
82
- }
83
-
84
- function writeWav16Mono(file, samples, sampleRate = SAMPLE_RATE) {
85
- const n = samples.length;
86
- const dataSize = n * 2;
87
- const buf = Buffer.alloc(44 + dataSize);
88
- buf.write("RIFF", 0);
89
- buf.writeUInt32LE(36 + dataSize, 4);
90
- buf.write("WAVE", 8);
91
- buf.write("fmt ", 12);
92
- buf.writeUInt32LE(16, 16);
93
- buf.writeUInt16LE(1, 20);
94
- buf.writeUInt16LE(1, 22);
95
- buf.writeUInt32LE(sampleRate, 24);
96
- buf.writeUInt32LE(sampleRate * 2, 28);
97
- buf.writeUInt16LE(2, 32);
98
- buf.writeUInt16LE(16, 34);
99
- buf.write("data", 36);
100
- buf.writeUInt32LE(dataSize, 40);
101
- for (let i = 0; i < n; i++) {
102
- const x = Math.max(-1, Math.min(1, samples[i]));
103
- buf.writeInt16LE(Math.round(x * 32767), 44 + i * 2);
104
- }
105
- fs.mkdirSync(path.dirname(file), { recursive: true });
106
- fs.writeFileSync(file, buf);
107
- return file;
108
- }
109
-
110
- function readWav16(file) {
111
- if (!file || !fs.existsSync(file)) {
112
- const err = new Error("wav missing");
113
- err.code = "SOUND_WAV_MISSING";
114
- throw err;
115
- }
116
- const buf = fs.readFileSync(file);
117
- if (buf.length < 44 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
118
- const err = new Error("wav not riff/wave");
119
- err.code = "SOUND_WAV_BAD";
120
- throw err;
121
- }
122
- let offset = 12;
123
- let audioFormat = 0;
124
- let channels = 0;
125
- let sampleRate = 0;
126
- let bits = 0;
127
- let dataOffset = -1;
128
- let dataSize = 0;
129
- while (offset + 8 <= buf.length) {
130
- const id = buf.toString("ascii", offset, offset + 4);
131
- const size = buf.readUInt32LE(offset + 4);
132
- const start = offset + 8;
133
- if (id === "fmt ") {
134
- audioFormat = buf.readUInt16LE(start);
135
- channels = buf.readUInt16LE(start + 2);
136
- sampleRate = buf.readUInt32LE(start + 4);
137
- bits = buf.readUInt16LE(start + 14);
138
- } else if (id === "data") {
139
- dataOffset = start;
140
- dataSize = size;
141
- break;
142
- }
143
- offset = start + size + (size % 2);
144
- }
145
- if (audioFormat !== 1 || bits !== 16 || channels < 1 || dataOffset < 0) {
146
- const err = new Error("wav must be 16-bit PCM");
147
- err.code = "SOUND_WAV_BAD";
148
- throw err;
149
- }
150
- const frames = Math.floor(dataSize / (2 * channels));
151
- const samples = new Float64Array(frames);
152
- for (let i = 0; i < frames; i++) {
153
- samples[i] = buf.readInt16LE(dataOffset + i * 2 * channels) / 32768;
154
- }
155
- return { samples, sampleRate };
156
- }
157
-
158
- function rmsOf(samples, start = 0, end = samples.length) {
159
- const a = Math.max(0, start);
160
- const b = Math.min(samples.length, end);
161
- if (b <= a) return 0;
162
- let acc = 0;
163
- for (let i = a; i < b; i++) acc += samples[i] * samples[i];
164
- return Math.sqrt(acc / (b - a));
165
- }
166
-
167
- function windowRms(samples, sampleRate, windowSec = WINDOW_SEC) {
168
- const win = Math.max(1, Math.floor(sampleRate * windowSec));
169
- const out = [];
170
- for (let i = 0; i + win <= samples.length; i += win) {
171
- out.push(rmsOf(samples, i, i + win));
172
- }
173
- return out;
174
- }
175
-
176
- function goertzelPower(samples, start, end, sampleRate, freq) {
177
- const n = end - start;
178
- if (n < 8) return 0;
179
- const w = (2 * Math.PI * freq) / sampleRate;
180
- const coeff = 2 * Math.cos(w);
181
- let s0 = 0;
182
- let s1 = 0;
183
- let s2 = 0;
184
- for (let i = start; i < end; i++) {
185
- s0 = samples[i] + coeff * s1 - s2;
186
- s2 = s1;
187
- s1 = s0;
188
- }
189
- const power = s1 * s1 + s2 * s2 - coeff * s1 * s2;
190
- return power > 0 ? power : 0;
191
- }
192
-
193
- function humBins() {
194
- const bins = [];
195
- const lo = Math.log(60);
196
- const hi = Math.log(1800);
197
- for (let i = 0; i < 20; i++) {
198
- bins.push(Math.exp(lo + ((hi - lo) * i) / 19));
199
- }
200
- return bins;
201
- }
202
-
203
- function failClosed(reason, extra) {
204
- return {
205
- status: "FAIL",
206
- failClosed: true,
207
- reason,
208
- ...extra,
209
- };
210
- }
211
-
212
- function evaluateMetrics(samples, sampleRate) {
213
- if (!samples || samples.length < Math.floor(sampleRate * 2)) {
214
- return failClosed("too-short", {
215
- metrics: { chop: { ok: false }, levels: { ok: false }, peak: { ok: false }, hum: { ok: false } },
216
- });
217
- }
218
-
219
- const rms = windowRms(samples, sampleRate);
220
- let chopMean = null;
221
- if (rms.length >= 2) {
222
- let acc = 0;
223
- for (let i = 1; i < rms.length; i++) acc += Math.abs(rms[i] - rms[i - 1]);
224
- chopMean = acc / (rms.length - 1);
225
- }
226
- const chop = {
227
- meanAbsDeltaRms: chopMean,
228
- max: CHOP_MAX,
229
- strong: CHOP_STRONG,
230
- ok: chopMean !== null && chopMean <= CHOP_MAX,
231
- };
232
-
233
- const introN = Math.floor(INTRO_SEC * sampleRate);
234
- const fadeN = Math.floor(FADE_SEC * sampleRate);
235
- const bodyEnd = samples.length - fadeN;
236
- const introRms = rmsOf(samples, 0, introN);
237
- const bodyMean = rmsOf(samples, introN, bodyEnd);
238
- const bodyWindows = windowRms(samples.subarray(introN, Math.max(introN, bodyEnd)), sampleRate);
239
- const bodyMin = bodyWindows.length ? Math.min(...bodyWindows) : 0;
240
- const ratio = introRms > 1e-9 ? bodyMean / introRms : Number.POSITIVE_INFINITY;
241
- const levels = {
242
- bodyIntroRatio: Number.isFinite(ratio) ? ratio : null,
243
- bodyMinRms: bodyMin,
244
- bodyMeanRms: bodyMean,
245
- introMeanRms: introRms,
246
- ratioMin: LEVELS_RATIO_MIN,
247
- ratioMax: LEVELS_RATIO_MAX,
248
- bodyMinRequired: BODY_MIN_RMS,
249
- ok:
250
- Number.isFinite(ratio) &&
251
- ratio >= LEVELS_RATIO_MIN &&
252
- ratio <= LEVELS_RATIO_MAX &&
253
- bodyMin >= BODY_MIN_RMS,
254
- };
255
-
256
- let peak = 0;
257
- for (let i = 0; i < samples.length; i++) {
258
- const a = Math.abs(samples[i]);
259
- if (a > peak) peak = a;
260
- }
261
- const peakGate = { maxAbs: peak, max: PEAK_LIMIT, ok: peak < PEAK_LIMIT };
262
-
263
- const bins = humBins();
264
- const win = Math.max(1, Math.floor(sampleRate * WINDOW_SEC));
265
- const dominant = [];
266
- const centroids = [];
267
- let same = 0;
268
- let shareAcc = 0;
269
- let lastBin = -1;
270
- for (let i = 0; i + win <= samples.length; i += win) {
271
- let total = 0;
272
- let best = 0;
273
- let bestIdx = -1;
274
- let weighted = 0;
275
- for (let b = 0; b < bins.length; b++) {
276
- const p = goertzelPower(samples, i, i + win, sampleRate, bins[b]);
277
- total += p;
278
- weighted += p * bins[b];
279
- if (p > best) {
280
- best = p;
281
- bestIdx = b;
282
- }
283
- }
284
- const share = total > 0 ? best / total : 0;
285
- if (total > 0) centroids.push(weighted / total);
286
- dominant.push({ bin: bestIdx, share });
287
- if (lastBin === bestIdx) same += 1;
288
- lastBin = bestIdx;
289
- shareAcc += share;
290
- }
291
- const windows = dominant.length;
292
- const sameRatio = windows > 1 ? same / (windows - 1) : 1;
293
- const meanShare = windows ? shareAcc / windows : 1;
294
- let centroidMean = 0;
295
- for (const c of centroids) centroidMean += c;
296
- centroidMean = centroids.length ? centroidMean / centroids.length : 0;
297
- let centroidVar = 0;
298
- for (const c of centroids) centroidVar += (c - centroidMean) * (c - centroidMean);
299
- const centroidCv =
300
- centroids.length > 1 && centroidMean > 0
301
- ? Math.sqrt(centroidVar / centroids.length) / centroidMean
302
- : 0;
303
- // Continuous drone: one bin owns the piece AND the centroid barely moves.
304
- // Kick membranes can own a low bin without being a flat sine.
305
- const drone = sameRatio >= 0.8 && meanShare >= 0.5 && centroidCv <= 0.045;
306
- const hum = {
307
- sameDominantRatio: sameRatio,
308
- meanDominantShare: meanShare,
309
- centroidCv,
310
- ok: !drone,
311
- };
312
-
313
- const gates = { chop: chop.ok, levels: levels.ok, peak: peakGate.ok, hum: hum.ok };
314
- const ok = gates.chop && gates.levels && gates.peak && gates.hum;
315
- return {
316
- status: ok ? "PASS" : "FAIL",
317
- failClosed: true,
318
- metrics: { chop, levels, peak: peakGate, hum },
319
- gates,
320
- };
321
- }
322
-
323
- function evaluateWavFile(file) {
324
- try {
325
- const wav = readWav16(file);
326
- return { ...evaluateMetrics(wav.samples, wav.sampleRate), wav: file, sampleRate: wav.sampleRate };
327
- } catch (err) {
328
- return failClosed(err instanceof Error ? err.message : String(err), {
329
- wav: file || null,
330
- metrics: { chop: { ok: false }, levels: { ok: false }, peak: { ok: false }, hum: { ok: false } },
331
- });
332
- }
333
- }
334
-
335
- function buildReceipt(input, evaled) {
336
- const genre = input.genre || { id: "ambient", bpm: 72, voices: ["pad", "air"] };
337
- return {
338
- kind: "sound-bed",
339
- status: evaled.status,
340
- failClosed: true,
341
- brief: String(input.brief || ""),
342
- seed: input.seed,
343
- genre: genre.id,
344
- alias: genre.alias || null,
345
- tempo: genre.bpm,
346
- voices: genre.voices,
347
- engine: input.engine || rippel.ENGINE,
348
- mix: input.mix || rippel.MIX,
349
- ssot: input.ssot || rippel.SSOT,
350
- topology: input.topology || rippel.TOPOLOGY,
351
- toneOffline: input.toneOffline || rippel.TONE_OFFLINE_BLOCKER,
352
- wav: input.wavRel || input.wav,
353
- durationSec: input.seconds,
354
- sampleRate: input.sampleRate || SAMPLE_RATE,
355
- metrics: evaled.metrics,
356
- gates: evaled.gates || {
357
- chop: false,
358
- levels: false,
359
- peak: false,
360
- hum: false,
361
- },
362
- reason: evaled.reason || null,
363
- spine: SPINE,
364
- writtenAt: new Date().toISOString(),
365
- };
366
- }
367
-
368
- function receiptPath(root) {
369
- return path.join(root, RECEIPT_REL);
370
- }
371
-
372
- function defaultWavPath(root) {
373
- return path.join(root, WAV_REL);
374
- }
375
-
376
- function writeReceipt(root, receipt) {
377
- const file = receiptPath(root);
378
- fs.mkdirSync(path.dirname(file), { recursive: true });
379
- fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
380
- return file;
381
- }
382
-
383
- function readReceipt(root) {
384
- const file = receiptPath(root);
385
- if (!fs.existsSync(file)) return null;
386
- try {
387
- const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
388
- return parsed && typeof parsed === "object" ? parsed : null;
389
- } catch {
390
- return { status: "FAIL", failClosed: true, reason: "receipt-unreadable" };
391
- }
392
- }
393
-
394
- function renderBed(opts = {}) {
395
- const root = opts.root || process.cwd();
396
- const brief = String(opts.brief || "factory-sound bed");
397
- const seconds = Number(opts.seconds) > 0 ? Number(opts.seconds) : DEFAULT_SECONDS;
398
- const wav = opts.out ? path.resolve(root, opts.out) : defaultWavPath(root);
399
- const rendered = renderSamples({ brief, genre: opts.genre, seconds, seed: opts.seed });
400
- writeWav16Mono(wav, rendered.samples, rendered.sampleRate);
401
- const evaled = evaluateMetrics(rendered.samples, rendered.sampleRate);
402
- const receipt = buildReceipt(
403
- {
404
- brief,
405
- seed: rendered.seed,
406
- genre: rendered.genre,
407
- engine: rendered.engine,
408
- mix: rendered.mix,
409
- ssot: rendered.ssot,
410
- topology: rendered.topology,
411
- toneOffline: rendered.toneOffline,
412
- wav,
413
- wavRel: path.relative(root, wav) || wav,
414
- seconds: rendered.seconds,
415
- sampleRate: rendered.sampleRate,
416
- },
417
- evaled,
418
- );
419
- writeReceipt(root, receipt);
420
- return { receipt, wav, seed: rendered.seed };
421
- }
422
-
423
- module.exports = {
424
- SPINE,
425
- SAMPLE_RATE,
426
- DEFAULT_SECONDS,
427
- RECEIPT_REL,
428
- WAV_REL,
429
- GENRE_ALIASES,
430
- GENRES,
431
- seedFromBrief,
432
- resolveGenre,
433
- tempoFromSeed: rippel.tempoFromSeed,
434
- motionGrid: rippel.motionGrid,
435
- renderSamples,
436
- writeWav16Mono,
437
- readWav16,
438
- evaluateMetrics,
439
- evaluateWavFile,
440
- renderBed,
441
- receiptPath,
442
- defaultWavPath,
443
- writeReceipt,
444
- readReceipt,
445
- buildReceipt,
446
- TONE_OFFLINE_BLOCKER: rippel.TONE_OFFLINE_BLOCKER,
447
- MIX: rippel.MIX,
448
- SSOT: rippel.SSOT,
449
- RIPPEL: rippel.RIPPEL,
450
- bandEnergy: rippel.bandEnergy,
451
- highpassEnergy: rippel.highpassEnergy,
452
- zeroCrossRate: rippel.zeroCrossRate,
453
- crestFactor: rippel.crestFactor,
454
- };
1
+ /** Shim — mill drawers live in @0xray/blip. */
2
+ module.exports = require("@0xray/blip/sound-bed");
@@ -0,0 +1,2 @@
1
+ /** Shim — mill drawers live in @0xray/blip. */
2
+ module.exports = require("@0xray/blip/sound-mixer");