@illuminis/comprism 0.1.4 → 0.1.5

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.
package/out/lib/ui.js CHANGED
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.LiveIndicator = exports.c = void 0;
20
20
  exports.width = width;
21
21
  exports.spectrumAt = spectrumAt;
22
+ exports.pixelArt = pixelArt;
22
23
  exports.prismRule = prismRule;
23
24
  exports.banner = banner;
24
25
  exports.money = money;
@@ -86,6 +87,39 @@ function spectrumAt(index, glyph) {
86
87
  const [r, g, b] = SPECTRUM[Math.abs(index) % SPECTRUM.length];
87
88
  return `\x1b[38;2;${r};${g};${b}m${glyph}\x1b[0m`;
88
89
  }
90
+ /**
91
+ * A small picture, drawn two pixels to a character with the half block glyph:
92
+ * the top pixel is the glyph's color and the bottom one its background, which
93
+ * makes each pixel square in an ordinary terminal font. Each row of `pixels`
94
+ * is six hex digits per pixel, `......` for a clear one, and two rows make one
95
+ * line. Returns nothing without color, because the picture is only color.
96
+ */
97
+ function pixelArt(pixels) {
98
+ if (!useColorNow())
99
+ return [];
100
+ const at = (row, x) => {
101
+ const hex = row?.slice(x * 6, x * 6 + 6);
102
+ return !hex || hex === '......' ? null
103
+ : [0, 2, 4].map((k) => parseInt(hex.slice(k, k + 2), 16)).join(';');
104
+ };
105
+ const lines = [];
106
+ for (let y = 0; y < pixels.length; y += 2) {
107
+ let line = '';
108
+ for (let x = 0; x < (pixels[y]?.length ?? 0) / 6; x++) {
109
+ const top = at(pixels[y], x), bottom = at(pixels[y + 1], x);
110
+ if (top && bottom)
111
+ line += `\x1b[38;2;${top}m\x1b[48;2;${bottom}m▀\x1b[0m`;
112
+ else if (top)
113
+ line += `\x1b[38;2;${top}m▀\x1b[0m`;
114
+ else if (bottom)
115
+ line += `\x1b[38;2;${bottom}m▄\x1b[0m`;
116
+ else
117
+ line += ' ';
118
+ }
119
+ lines.push(line);
120
+ }
121
+ return lines;
122
+ }
89
123
  function prismRule(w = width()) {
90
124
  if (!useColorNow())
91
125
  return '-'.repeat(w);
@@ -34,6 +34,42 @@ export interface Recording {
34
34
  * headed off with `missing` so the person gets a sentence rather than an error.
35
35
  */
36
36
  export declare function start(): Recording;
37
+ /** Live dictation in progress, on the Mac's own recogniser. */
38
+ export interface LiveDictation {
39
+ /** The loudness now, 0 to 1. */
40
+ level: () => number;
41
+ /** The words so far. */
42
+ words: () => string;
43
+ /** Stop listening and get the final words, or null when it could not listen. */
44
+ stop: () => Promise<string | null>;
45
+ /** Why it could not listen, when it could not. */
46
+ problem: () => string;
47
+ }
48
+ /**
49
+ * Words as they are spoken, the way the web app shows them.
50
+ *
51
+ * Nothing is recorded to a file and nothing leaves the machine: the Mac's own
52
+ * recogniser listens and reports what it hears, several times a second.
53
+ * Returns null where there is no helper, and the caller records and has the
54
+ * server write it down instead, as before.
55
+ */
56
+ export declare function listenLive(language?: string): LiveDictation | null;
57
+ /**
58
+ * How loud the last tenth of a second was, from 0 to 1.
59
+ *
60
+ * Read off the file as it is written, so somebody speaking sees the meter move
61
+ * and knows they are being heard, before a single word has come back. Sixteen
62
+ * bit mono at 16 kHz is 3,200 bytes a tenth of a second.
63
+ */
64
+ export declare function level(file: string): number;
65
+ /**
66
+ * What has been said so far, as a WAV file a transcriber can read.
67
+ *
68
+ * The recorder writes the header's lengths only when it stops, so a copy taken
69
+ * mid recording claims no audio at all. The copy gets the lengths of what is
70
+ * actually there. Returns null until there is at least half a second to send.
71
+ */
72
+ export declare function soFar(file: string): string | null;
37
73
  /** Delete the recording and the folder holding it. Always, and immediately. */
38
74
  export declare function discard(file: string): void;
39
75
  /**
package/out/lib/voice.js CHANGED
@@ -36,6 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.recorder = recorder;
37
37
  exports.missing = missing;
38
38
  exports.start = start;
39
+ exports.listenLive = listenLive;
40
+ exports.level = level;
41
+ exports.soFar = soFar;
39
42
  exports.discard = discard;
40
43
  exports.transcribe = transcribe;
41
44
  /**
@@ -102,7 +105,9 @@ const RECORDERS = [
102
105
  ...(process.platform === 'darwin'
103
106
  ? ['-f', 'avfoundation', '-i', ':default']
104
107
  : ['-f', 'alsa', '-i', 'default']),
105
- '-ac', '1', '-ar', RATE, '-y', out,
108
+ // Written as it is heard, not held until the end, so the meter and the
109
+ // words so far have something to read while the person is speaking.
110
+ '-ac', '1', '-ar', RATE, '-flush_packets', '1', '-y', out,
106
111
  ],
107
112
  },
108
113
  // Linux's own, present on most desktop installs.
@@ -206,6 +211,136 @@ function start() {
206
211
  };
207
212
  return { file, by: tool.name, stop };
208
213
  }
214
+ /** The helper built beside this file on a Mac, when there is one. */
215
+ function helper() {
216
+ if (process.platform !== 'darwin')
217
+ return null;
218
+ const p = path.join(__dirname, '..', 'native', 'dictate');
219
+ return fs.existsSync(p) ? p : null;
220
+ }
221
+ /**
222
+ * Words as they are spoken, the way the web app shows them.
223
+ *
224
+ * Nothing is recorded to a file and nothing leaves the machine: the Mac's own
225
+ * recogniser listens and reports what it hears, several times a second.
226
+ * Returns null where there is no helper, and the caller records and has the
227
+ * server write it down instead, as before.
228
+ */
229
+ function listenLive(language = 'en-US') {
230
+ const bin = helper();
231
+ if (!bin)
232
+ return null;
233
+ const child = (0, node_child_process_1.spawn)(bin, [language], { stdio: ['pipe', 'pipe', 'ignore'] });
234
+ let loud = 0;
235
+ let heard = '';
236
+ let final = null;
237
+ let why = '';
238
+ let rest = '';
239
+ const ended = new Promise((resolve) => child.once('close', () => resolve()));
240
+ child.stdout?.on('data', (chunk) => {
241
+ rest += chunk.toString('utf8');
242
+ const lines = rest.split('\n');
243
+ rest = lines.pop() ?? '';
244
+ for (const line of lines) {
245
+ const kind = line.slice(0, 1);
246
+ const text = line.slice(2);
247
+ if (kind === 'L')
248
+ loud = Number(text) || 0;
249
+ else if (kind === 'P')
250
+ heard = text;
251
+ else if (kind === 'F')
252
+ final = text;
253
+ else if (kind === 'E')
254
+ why = text;
255
+ }
256
+ });
257
+ // The helper never outlives the session.
258
+ const abandon = () => { try {
259
+ child.kill('SIGKILL');
260
+ }
261
+ catch { /* gone */ } };
262
+ process.once('exit', abandon);
263
+ return {
264
+ level: () => loud,
265
+ words: () => heard,
266
+ problem: () => why,
267
+ stop: async () => {
268
+ // Closing its input is "Enter was pressed": it says its final words and ends.
269
+ try {
270
+ child.stdin?.end();
271
+ }
272
+ catch { /* gone */ }
273
+ const timeout = new Promise((resolve) => setTimeout(resolve, 4000).unref());
274
+ await Promise.race([ended, timeout]);
275
+ abandon();
276
+ process.off('exit', abandon);
277
+ if (why && !final && !heard)
278
+ return null;
279
+ return (final ?? heard).trim();
280
+ },
281
+ };
282
+ }
283
+ /** Where the samples start in a WAV file this recorder wrote, or -1. */
284
+ function dataStart(buf) {
285
+ const at = buf.indexOf('data', 12, 'ascii');
286
+ return at < 0 ? -1 : at + 8;
287
+ }
288
+ /**
289
+ * How loud the last tenth of a second was, from 0 to 1.
290
+ *
291
+ * Read off the file as it is written, so somebody speaking sees the meter move
292
+ * and knows they are being heard, before a single word has come back. Sixteen
293
+ * bit mono at 16 kHz is 3,200 bytes a tenth of a second.
294
+ */
295
+ function level(file) {
296
+ try {
297
+ const size = fs.statSync(file).size;
298
+ const n = 3200;
299
+ if (size < 44 + n)
300
+ return 0;
301
+ const fd = fs.openSync(file, 'r');
302
+ const buf = Buffer.alloc(n);
303
+ fs.readSync(fd, buf, 0, n, size - n - (size - 44) % 2);
304
+ fs.closeSync(fd);
305
+ let sum = 0;
306
+ for (let i = 0; i + 1 < n; i += 2) {
307
+ const s = buf.readInt16LE(i) / 32768;
308
+ sum += s * s;
309
+ }
310
+ const rms = Math.sqrt(sum / (n / 2));
311
+ // Speech sits roughly between -50 dB (quiet room) and -10 dB (close and loud).
312
+ const db = 20 * Math.log10(rms || 1e-9);
313
+ return Math.max(0, Math.min(1, (db + 50) / 40));
314
+ }
315
+ catch {
316
+ return 0;
317
+ }
318
+ }
319
+ /**
320
+ * What has been said so far, as a WAV file a transcriber can read.
321
+ *
322
+ * The recorder writes the header's lengths only when it stops, so a copy taken
323
+ * mid recording claims no audio at all. The copy gets the lengths of what is
324
+ * actually there. Returns null until there is at least half a second to send.
325
+ */
326
+ function soFar(file) {
327
+ try {
328
+ const buf = fs.readFileSync(file);
329
+ const start = dataStart(buf);
330
+ if (start < 0 || buf.length - start < 16000)
331
+ return null;
332
+ const usable = buf.length - ((buf.length - start) % 2);
333
+ const copy = Buffer.from(buf.subarray(0, usable));
334
+ copy.writeUInt32LE(usable - 8, 4);
335
+ copy.writeUInt32LE(usable - start, start - 4);
336
+ const out = path.join(path.dirname(file), 'so-far.wav');
337
+ fs.writeFileSync(out, copy);
338
+ return out;
339
+ }
340
+ catch {
341
+ return null;
342
+ }
343
+ }
209
344
  /** Delete the recording and the folder holding it. Always, and immediately. */
210
345
  function discard(file) {
211
346
  try {
Binary file
package/out/thin.js CHANGED
@@ -94,7 +94,7 @@ async function attachStdin(piped) {
94
94
  const WORK_VALUES = ['--mode', '--model', '--max-spend', '--max-steps', '--effort', '--resume',
95
95
  '--cwd', '--allow', '--add-folder', '--timeout', '--issue', '--team', '--output', '--worktree',
96
96
  '--schema', '--config', '--max-minutes', '--queued'];
97
- const WORK_SWITCHES = ['--plan', '--approve-plan', '--pin', '--sandbox', '--yes', '--background',
97
+ const WORK_SWITCHES = ['--plan', '--approve-plan', '--pin', '--no-optimize', '--sandbox', '--yes', '--background',
98
98
  '--no-map', '--no-graph', '--quiet', '--no-personal', '--cloud'];
99
99
  /**
100
100
  * The value after a flag, or undefined when the flag was not given.
@@ -584,7 +584,7 @@ async function main() {
584
584
  plan: args.includes('--plan'),
585
585
  approvePlan: args.includes('--approve-plan'),
586
586
  model: flag(args, '--model'),
587
- pin: args.includes('--pin'),
587
+ pin: args.includes('--pin') || args.includes('--no-optimize'),
588
588
  ...(args.includes('--max-spend') ? { maxSpend: Number(spend) } : {}),
589
589
  ...(args.includes('--max-steps') ? { maxSteps: Number(flag(args, '--max-steps')) } : {}),
590
590
  ...(args.includes('--max-minutes') ? { maxMinutes: Number(flag(args, '--max-minutes')) } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illuminis/comprism",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "CompletionPrism Companion. Records what finishing a piece of work actually costs - every attempt, and the human time spent noticing and repairing the ones that failed.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "out/**/*.js",
14
14
  "out/**/*.d.ts",
15
+ "out/native/dictate",
15
16
  "!out/tests/**",
16
17
  "!out/internal/**",
17
18
  "scripts/read_python.py",
@@ -22,7 +23,7 @@
22
23
  "node": ">=22"
23
24
  },
24
25
  "scripts": {
25
- "build": "tsc -p ./",
26
+ "build": "tsc -p ./ && node scripts/build-native.mjs",
26
27
  "dev": "tsc -w -p ./",
27
28
  "test": "npm run build && node --test out/**/*.test.js",
28
29
  "gate": "npm run build && npm test && npm run shipcheck",