Files
skills/orchestrator/templates/voice.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 a388c6ef77 fix(orchestrator): S1 fit, rename once, Deutschlandfunk e2e, squelch, tree cause
Requirement 001 phase 6, S1:

- Fit: Agents and Artifacts in a graph view fill the window from 861 px
  up (body.fit flex column); the e2e walk probes #agents/orbit at every
  width for a page no taller than the window.
- Rename once: a session seen under two names by one source (two claims)
  flipped its name on every state build, so the Stream logged a rename
  each poll. Names.observe keeps only the newest sighting per session
  and source, and the timeline emits each (session, before, after) pair
  once.
- Deutschlandfunk: root cause was the signed media-src, which never
  listed the redirect target f111.rndfnk.com that st01.sslstream.dlf.de
  answers with (fixed in ba97f7c). The e2e now replays that chain, a
  302 from one https origin to a second, in Chromium and Firefox, with
  self-signed throwaway certificates the drivers accept.
- Squelch: a settings slider 0..1 scales every tone; at 0 no beep plays,
  music on or off (Voice.beep skips a silent tone). Stored settings
  without the key load with 1.0.
- Tree: the missing tree file error names `pkgmgr branch tree` and the
  release and install step that makes the timer write it.

Evidence: make test 655 OK, ruff and markdownlint clean
(/tmp/s1-make-test-1.log); e2e 17 OK, none skipped (/tmp/s1-e2e-3.log);
MEDUSA 14 files, 0 findings.

🎯 Fix: 94 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:37:02 +00:00

74 lines
2.5 KiB
JavaScript

const Voice = (() => {
"use strict";
const RANKS = {mayday: 0, "pan-pan": 1, security: 2};
const ALARMS = new Set(["mayday", "pan-pan"]);
const ROUTINE = 3;
function toneOf(row, human) {
if (ALARMS.has(row.class)) {
return "mayday";
}
return row.class === "security" || row.to.includes(human) ? "direct" : "radio";
}
function loudness(tone, settings) {
const level = tone === "mayday" ? Math.max(...Object.values(settings.sounds).map((sound) => sound.volume), settings.music.volume) : settings.sounds[tone].volume;
return level * settings.squelch;
}
function beep(present, settings, kinds) {
return kinds.find((kind) => present[kind] && settings.sounds[kind].on && loudness(kind, settings) > 0) || null;
}
function speakable(row, speech) {
if (!speech.on || !row.speech || !row.voice) {
return false;
}
return row.class === "mayday" || (speech.classes.includes(row.class) && !speech.muted.includes(row.source));
}
function rank(item) {
return Object.hasOwn(RANKS, item.cls) ? RANKS[item.cls] : ROUTINE;
}
function enqueue(pending, item, limit) {
const at = pending.findIndex((other) => rank(other) > rank(item));
const next = at < 0 ? [...pending, item] : [...pending.slice(0, at), item, ...pending.slice(at)];
return next.slice(0, limit);
}
function frames(duration, rate, seconds) {
return Math.max(Math.min(Math.ceil(duration * rate), rate * seconds), 0);
}
function wav(samples, rate) {
const size = samples.length * 2;
const buffer = new ArrayBuffer(44 + size);
const view = new DataView(buffer);
const text = (offset, value) => [...value].forEach((char, index) => view.setUint8(offset + index, char.charCodeAt(0)));
text(0, "RIFF");
view.setUint32(4, 36 + size, true);
text(8, "WAVE");
text(12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, rate, true);
view.setUint32(28, rate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
text(36, "data");
view.setUint32(40, size, true);
samples.forEach((sample, index) => {
const level = Math.max(-1, Math.min(1, sample));
view.setInt16(44 + index * 2, level < 0 ? level * 0x8000 : level * 0x7fff, true);
});
return buffer;
}
return {toneOf, loudness, beep, speakable, enqueue, frames, wav};
})();
if (typeof module === "object" && module.exports) {
module.exports = Voice;
}