tests/voice.test.ts
Rama main · réplica de lectura del repositorio original.
// Voice notes (3.24.0): the note-writing rule (short → a bullet in the day's
// inbox, long → a note of its own), the filename scheme for recordings and
// for long notes, the queue (one job at a time, FIFO, and the recording is
// never the thing that is lost), and the landing against a throwaway vault.
// The engine is FAKED throughout: what whisper hears is measured in
// CONTRACTS.md, and a test that ran a 574 MB model would not be a unit test.
import assert from "node:assert/strict";
import { after, before, describe, it } from "node:test";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import {
appendBullet,
isLongTranscript,
LONG_TRANSCRIPT_WORDS,
planVoiceNote,
sniffRecording,
tidyTranscript,
voiceAudioDir,
voiceAudioPath,
voiceBullet,
voiceEffective,
voiceInboxPath,
voiceNotePath,
voiceStamp,
wordCount,
} from "../shared/voice.ts";
import { createVoiceQueue, type VoiceWork } from "../server/voiceQueue.ts";
import { resample, speechWindows, WHISPER_RATE, WINDOW_MAX } from "../server/voiceAudio.ts";
import { appendWithPrecondition, landVoiceNote } from "../server/voice.ts";
import { initIndexer, registerAttachment } from "../server/indexer.ts";
import { listUnusedAttachments } from "../server/unusedAttachments.ts";
import { initSite } from "../server/site.ts";
import { initVault } from "../server/vault.ts";
import { makeDir, makeVault, removeVault } from "./helpers/vault.ts";
import { VOICE_COPY } from "../client/voice/copy.ts";
const words = (n: number, word = "word"): string => Array.from({ length: n }, (_, i) => `${word}${i}`).join(" ");
describe("the filename scheme", () => {
it("stamps a recording by the device's day and minute, colon dropped", () => {
assert.equal(voiceStamp("2026-09-23", "14:02"), "2026-09-23 1402");
assert.equal(voiceStamp("2026-01-05", "00:00"), "2026-01-05 0000");
});
it("never reuses a name: the second recording in a minute is (2)", () => {
assert.equal(voiceAudioPath("attachments/Voice", "2026-09-23 1402", "webm"), "attachments/Voice/2026-09-23 1402.webm");
assert.equal(voiceAudioPath("attachments/Voice", "2026-09-23 1402", "webm", 2), "attachments/Voice/2026-09-23 1402 (2).webm");
assert.equal(voiceAudioPath("Voice", "2026-09-23 1402", "ogg", 3), "Voice/2026-09-23 1402 (3).ogg");
});
it("follows the attachment-location setting, asked from the inbox", () => {
assert.equal(voiceAudioDir({ mode: "specified", folder: "attachments" }), "attachments/Voice");
assert.equal(voiceAudioDir({ mode: "specified", folder: "Attachments" }), "Attachments/Voice");
assert.equal(voiceAudioDir({ mode: "vault-root", folder: "attachments" }), "Voice");
assert.equal(voiceAudioDir({ mode: "same-folder", folder: "attachments" }), "Inbox/Voice");
assert.equal(voiceAudioDir({ mode: "subfolder", folder: "assets" }), "Inbox/assets/Voice");
});
it("files a day's short notes in the phone's inbox note", () => {
assert.equal(voiceInboxPath("2026-09-23"), "Inbox/2026-09-23.md");
});
it("names a long note by its first words, cleaned for a filename and a wikilink", () => {
assert.equal(voiceNotePath("Remember: the map is an argument, always. And more words here"), "Inbox/Voice — Remember the map is an argument.md");
assert.equal(voiceNotePath("ذهبت هذا الصباح إلى المكتبة، قبل أن تفتح أبوابها"), "Inbox/Voice — ذهبت هذا الصباح إلى المكتبة قبل.md");
assert.equal(voiceNotePath("a/b [c] #d e? f"), "Inbox/Voice — a b c d e f.md");
assert.equal(voiceNotePath("Same words again", 2), "Inbox/Voice — Same words again (2).md");
});
});
describe("the note-writing rule", () => {
it("counts words by whitespace in both scripts", () => {
assert.equal(wordCount(""), 0);
assert.equal(wordCount(" one two\nthree "), 3);
assert.equal(wordCount("ذهبت هذا الصباح"), 3);
assert.equal(isLongTranscript(words(LONG_TRANSCRIPT_WORDS)), false);
assert.equal(isLongTranscript(words(LONG_TRANSCRIPT_WORDS + 1)), true);
});
it("a short transcript is one bullet in Inbox/<day>.md with the recording linked", () => {
const plan = planVoiceNote({ transcript: "Buy bread", audioPath: "attachments/Voice/2026-09-23 1402.webm", date: "2026-09-23", time: "14:02" });
assert.deepEqual(plan, {
kind: "bullet",
path: "Inbox/2026-09-23.md",
bullet: "- 14:02 — Buy bread [[attachments/Voice/2026-09-23 1402.webm#t=0|🎙]]\n",
});
});
it("eighty words is still a bullet; eighty-one is a note", () => {
const at = { audioPath: "Voice/x.webm", date: "2026-09-23", time: "09:00" };
assert.equal(planVoiceNote({ transcript: words(80), ...at }).kind, "bullet");
const long = planVoiceNote({ transcript: words(81), ...at });
assert.equal(long.kind, "note");
assert.ok(long.kind === "note");
assert.equal(long.body, `![[Voice/x.webm]]\n\n${words(81)}\n`);
});
it("with no recording kept, the words stand alone; with no words, the link does", () => {
assert.equal(voiceBullet("14:02", "Buy bread", null), "- 14:02 — Buy bread\n");
assert.equal(voiceBullet("14:02", null, "Voice/a.webm"), "- 14:02 — [[Voice/a.webm#t=0|🎙]]\n");
const plan = planVoiceNote({ transcript: words(90), audioPath: null, date: "2026-09-23", time: "09:00" });
assert.ok(plan.kind === "note");
assert.equal(plan.body, `${words(90)}\n`);
});
it("a transcript that is only whisper's furniture is no transcript", () => {
assert.equal(tidyTranscript(" [BLANK_AUDIO] "), "");
assert.equal(tidyTranscript("[Music] Hello there (applause)\n"), "Hello there");
assert.equal(tidyTranscript("[موسيقى] مرحبا"), "مرحبا");
const plan = planVoiceNote({ transcript: "[BLANK_AUDIO]", audioPath: "Voice/a.webm", date: "2026-09-23", time: "08:15" });
assert.ok(plan.kind === "bullet");
assert.equal(plan.bullet, "- 08:15 — [[Voice/a.webm#t=0|🎙]]\n");
});
it("appends exactly one newline's worth — the share sheet's join", () => {
assert.equal(appendBullet("", "- a\n"), "- a\n");
assert.equal(appendBullet("- x\n", "- a\n"), "- x\n- a\n");
assert.equal(appendBullet("- x", "- a\n"), "- x\n- a\n");
});
it("defaults: the model a processor runs faster than speech, auto backend and language, the audio kept", () => {
assert.deepEqual(voiceEffective(undefined), { model: "small-q5_1", backend: "auto", language: "auto", keepAudio: true });
assert.deepEqual(voiceEffective({ language: "ar", keepAudio: false }), { model: "small-q5_1", backend: "auto", language: "ar", keepAudio: false });
// The migration: a stored model is a choice and survives the new default.
assert.equal(voiceEffective({ model: "large-v3-turbo-q5_0" }).model, "large-v3-turbo-q5_0");
assert.equal(voiceEffective({ backend: "cpu" }).backend, "cpu");
});
});
describe("the recording", () => {
it("is known by its bytes, never its name", () => {
assert.equal(sniffRecording(new Uint8Array([0x1a, 0x45, 0xdf, 0xa3, 0, 0])), "webm");
assert.equal(sniffRecording(new TextEncoder().encode("OggS\0\0\0\0")), "ogg");
assert.equal(sniffRecording(new TextEncoder().encode("RIFF\0\0\0\0WAVEfmt ")), "wav");
assert.equal(sniffRecording(new TextEncoder().encode("\0\0\0 ftypM4A ")), null);
assert.equal(sniffRecording(new Uint8Array([])), null);
});
it("resamples to 16 kHz without folding the top of the band down", () => {
const rate = 48000;
const n = rate; // one second
const low = new Float32Array(n);
const high = new Float32Array(n);
for (let i = 0; i < n; i++) {
low[i] = Math.sin((2 * Math.PI * 440 * i) / rate);
high[i] = Math.sin((2 * Math.PI * 12000 * i) / rate); // above the new Nyquist
}
const outLow = resample(low, rate);
const outHigh = resample(high, rate);
assert.equal(outLow.length, WHISPER_RATE);
const rms = (a: Float32Array): number => Math.sqrt(a.reduce((s, v) => s + v * v, 0) / a.length);
assert.ok(rms(outLow) > 0.6, `speech band kept (${rms(outLow)})`);
assert.ok(rms(outHigh) < 0.05, `12 kHz filtered before decimation (${rms(outHigh)})`);
});
// The processor's engine hears thirty seconds at a time (server/voiceAudio.ts).
const tone = (seconds: number, quietAt: number[] = []): Float32Array => {
const out = new Float32Array(Math.round(seconds * WHISPER_RATE));
for (let i = 0; i < out.length; i++) out[i] = 0.3 * Math.sin((2 * Math.PI * 220 * i) / WHISPER_RATE);
// Half a second of silence centred on each pause.
for (const at of quietAt) out.fill(0, Math.round((at - 0.25) * WHISPER_RATE), Math.round((at + 0.25) * WHISPER_RATE));
return out;
};
it("keeps a short recording whole", () => {
assert.deepEqual(speechWindows(tone(10)), [[0, 10 * WHISPER_RATE]]);
});
it("cuts a long one into windows under thirty seconds, at its pauses, losing nothing", () => {
const samples = tone(70, [21, 44]);
const windows = speechWindows(samples);
assert.equal(windows.length, 3);
assert.equal(windows[0][0], 0);
assert.equal(windows[windows.length - 1][1], samples.length, "the last sample is heard");
for (let i = 1; i < windows.length; i++) assert.equal(windows[i][0], windows[i - 1][1], "contiguous: no gap, no overlap");
for (const [a, b] of windows) assert.ok(b - a <= WINDOW_MAX, `${(b - a) / WHISPER_RATE} s is under whisper's thirty`);
// Each cut lands inside a pause, not in the middle of a word.
const cut = (i: number): number => windows[i][1] / WHISPER_RATE;
assert.ok(Math.abs(cut(0) - 21) < 0.3, `first cut at ${cut(0)} s`);
assert.ok(Math.abs(cut(1) - 44) < 0.3, `second cut at ${cut(1)} s`);
});
it("cuts a recording with no pause at the window's length rather than never", () => {
const windows = speechWindows(tone(60));
assert.ok(windows.every(([a, b]) => b - a <= WINDOW_MAX));
assert.equal(windows[windows.length - 1][1], 60 * WHISPER_RATE);
});
it("does not send silence to a model that answers it with words", () => {
assert.deepEqual(speechWindows(new Float32Array(40 * WHISPER_RATE)), []);
const speechThenSilence = new Float32Array(50 * WHISPER_RATE);
speechThenSilence.set(tone(20));
const windows = speechWindows(speechThenSilence);
assert.equal(windows.length, 1, "the silent tail is dropped");
assert.equal(windows[0][0], 0);
});
});
describe("the queue", () => {
const work = (audio: string, over: Partial<VoiceWork> = {}): VoiceWork => ({
audio,
date: "2026-09-23",
time: "10:00",
language: "auto",
keepAudio: true,
...over,
});
it("runs one job at a time, first in first out", async () => {
const log: string[] = [];
let running = 0;
let most = 0;
const q = createVoiceQueue({
async transcribe(w) {
running++;
most = Math.max(most, running);
log.push(`start ${w.audio}`);
await new Promise((r) => setTimeout(r, 5));
log.push(`end ${w.audio}`);
running--;
return { text: `words of ${w.audio}`, language: "en" };
},
land: async (w, text) => ({ notePath: `Inbox/${w.date}.md`, kind: text ? "bullet" : "bullet" }),
discard: async () => {},
});
const a = q.submit(work("a"));
const b = q.submit(work("b"));
const c = q.submit(work("c"));
assert.equal(a.status, "transcribing", "the first job starts at once");
assert.equal(b.ahead, 1);
assert.equal(c.ahead, 2);
assert.equal(q.get(c.id)?.status, "queued");
await q.idle();
assert.equal(most, 1, "two transcriptions overlapped");
assert.deepEqual(log, ["start a", "end a", "start b", "end b", "start c", "end c"]);
for (const job of [a, b, c]) {
const done = q.get(job.id);
assert.equal(done?.status, "done");
assert.equal(done?.notePath, "Inbox/2026-09-23.md");
}
assert.equal(q.get(b.id)?.transcript, "words of b");
assert.equal(q.busy(), false);
assert.equal(q.waiting(), 0);
});
it("a failed transcription still lands the recording's link, and keeps it", async () => {
const landed: [string | null, string | null][] = [];
const discarded: string[] = [];
const q = createVoiceQueue({
transcribe: async () => {
throw Object.assign(new Error("no GPU today"), { code: "download" });
},
land: async (_w, text, audio) => {
landed.push([text, audio]);
return { notePath: "Inbox/2026-09-23.md", kind: "bullet" };
},
discard: async (a) => void discarded.push(a),
});
const job = q.submit(work("Voice/a.webm", { keepAudio: false }));
await q.idle();
const out = q.get(job.id);
assert.equal(out?.status, "failed");
assert.equal(out?.error, "download");
assert.equal(out?.audio, "Voice/a.webm");
assert.deepEqual(landed, [[null, "Voice/a.webm"]]);
assert.deepEqual(discarded, [], "a recording with no words was deleted");
});
it("'keep the audio' off deletes the recording only AFTER the words have landed", async () => {
const order: string[] = [];
const q = createVoiceQueue({
transcribe: async () => ({ text: "Buy bread", language: "en" }),
land: async (_w, text, audio) => {
order.push(`land ${text} ${audio}`);
return { notePath: "Inbox/2026-09-23.md", kind: "bullet" };
},
discard: async (a) => void order.push(`discard ${a}`),
});
const job = q.submit(work("Voice/a.webm", { keepAudio: false }));
await q.idle();
assert.deepEqual(order, ["land Buy bread null", "discard Voice/a.webm"]);
assert.equal(q.get(job.id)?.audio, null);
assert.equal(q.get(job.id)?.status, "done");
});
it("silence is done with nothing to say — and the recording is kept", async () => {
const discarded: string[] = [];
const q = createVoiceQueue({
transcribe: async () => ({ text: " ", language: null }),
land: async () => ({ notePath: "Inbox/2026-09-23.md", kind: "bullet" }),
discard: async (a) => void discarded.push(a),
});
const job = q.submit(work("Voice/quiet.webm", { keepAudio: false }));
await q.idle();
assert.equal(q.get(job.id)?.status, "done");
assert.equal(q.get(job.id)?.error, "silence");
assert.deepEqual(discarded, []);
});
it("a note that cannot be written fails the job without throwing out of the queue", async () => {
const q = createVoiceQueue({
transcribe: async () => ({ text: "x", language: "en" }),
land: async () => {
throw new Error("disk full");
},
discard: async () => {},
});
const a = q.submit(work("a"));
const b = q.submit(work("b"));
await q.idle();
assert.equal(q.get(a.id)?.status, "failed");
assert.equal(q.get(a.id)?.error, "write");
assert.equal(q.get(b.id)?.status, "failed", "the second job still ran");
});
});
describe("landing in a vault", () => {
const data = makeDir();
const root = makeVault({
"Inbox/2026-09-22.md": "- 08:00 — Shared from the phone\n",
});
before(async () => {
initSite({ ASTROLABE_DATA: data });
initVault(root);
await initIndexer();
});
after(() => {
removeVault(root);
removeVault(data);
});
const at = (date: string, time: string): VoiceWork => ({ audio: "attachments/Voice/x.webm", date, time, language: "auto", keepAudio: true });
it("a short one joins the day's inbox note after what the phone put there", async () => {
const out = await landVoiceNote(at("2026-09-22", "09:30"), "Call the dentist", "attachments/Voice/2026-09-22 0930.webm");
assert.deepEqual(out, { notePath: "Inbox/2026-09-22.md", kind: "bullet" });
assert.equal(
readFileSync(path.join(root, "Inbox/2026-09-22.md"), "utf8"),
"- 08:00 — Shared from the phone\n- 09:30 — Call the dentist [[attachments/Voice/2026-09-22 0930.webm#t=0|🎙]]\n",
);
});
it("a linked recording is a REFERENCED attachment, never swept as unused", async () => {
const rel = "attachments/Voice/2026-09-25 0800.webm";
mkdirSync(path.join(root, "attachments/Voice"), { recursive: true });
writeFileSync(path.join(root, rel), Buffer.from([0x1a, 0x45, 0xdf, 0xa3]));
registerAttachment(rel);
const before = await listUnusedAttachments();
assert.ok(before.files.some((f) => f.path === rel), "the bare recording should read as unused before it is linked");
await landVoiceNote(at("2026-09-25", "08:00"), "Short thought", rel);
const afterLink = await listUnusedAttachments();
assert.ok(!afterLink.files.some((f) => f.path === rel), "the moment link does not count as a reference");
});
it("the day's first makes the note", async () => {
const out = await landVoiceNote(at("2026-09-23", "07:05"), "First thing", null);
assert.equal(out.notePath, "Inbox/2026-09-23.md");
assert.equal(readFileSync(path.join(root, "Inbox/2026-09-23.md"), "utf8"), "- 07:05 — First thing\n");
});
it("a long one is its own note, and a second with the same words does not overwrite it", async () => {
const text = `Essay notes ${words(85)}`;
const one = await landVoiceNote(at("2026-09-23", "10:00"), text, "attachments/Voice/2026-09-23 1000.webm");
assert.equal(one.kind, "note");
assert.equal(one.notePath, "Inbox/Voice — Essay notes word0 word1 word2 word3.md");
assert.equal(readFileSync(path.join(root, one.notePath), "utf8"), `![[attachments/Voice/2026-09-23 1000.webm]]\n\n${text}\n`);
const two = await landVoiceNote(at("2026-09-23", "10:05"), text, null);
assert.equal(two.notePath, "Inbox/Voice — Essay notes word0 word1 word2 word3 (2).md");
});
it("appends after what another writer put there since the index last read it", async () => {
// Written behind the server's back, as the laptop's editor or the phone's
// share sheet would: the append reads the file as it is NOW and carries
// that read's mtime as its precondition, so the other writer's line
// survives rather than being written over from a stale copy.
const rel = "Inbox/2026-09-24.md";
writeFileSync(path.join(root, rel), "- 06:00 — one\n");
await appendWithPrecondition(rel, "- 06:01 — two\n");
assert.equal(readFileSync(path.join(root, rel), "utf8"), "- 06:00 — one\n- 06:01 — two\n");
});
});
describe("the recorder's own copy (client/voice/copy.ts)", () => {
// check-i18n walks only the dictionary; the recorder's sentences travel in its
// lazy chunk, so the parity the gate would have checked is checked here.
for (const [key, text] of Object.entries(VOICE_COPY)) {
it(`${key} has both halves, the same placeholders, and Arabic in the Arabic`, () => {
assert.ok(text.en.trim().length > 0, "empty en");
assert.ok(text.ar.trim().length > 0, "empty ar");
assert.match(text.ar, /[\u0600-\u06ff]/, "the ar half has no Arabic in it");
const holes = (s: string): string[] => [...s.matchAll(/\{(\w+)\}/g)].map((m) => m[1]).sort();
assert.deepEqual(holes(text.ar), holes(text.en));
});
}
});