Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e310a51
feat(transcribe): per-channel acoustic speaker diarization
valentinweyer Jul 8, 2026
f9fcfae
feat(transcribe): acoustic diarization for mono audio
valentinweyer Jul 8, 2026
11dc570
perf(diarize): ~6.65x faster via Sortformer's offline chunk preset
valentinweyer Jul 14, 2026
7fcde29
feat(diarize): robust sidecar parsing, dominance gate, live progress
valentinweyer Jul 30, 2026
9ea3dcb
feat(processing): live per-stage progress for diarization
valentinweyer Jul 30, 2026
f31f2c9
fix(e2e): seed a real recording session in processing-stages.t1
valentinweyer Jul 30, 2026
f1e1b71
feat(speakers): human-in-the-loop speaker identification
valentinweyer Jul 14, 2026
0741c18
feat(diarize): restore voiceprint embedding extraction to the sidecar
valentinweyer Jul 30, 2026
842f9f5
fix(backend-cli): PYTHONUNBUFFERED:1 on every spawned backend process
valentinweyer Jul 30, 2026
e0621f2
feat(processing): restore embedding sub-progress + transcribe progress
valentinweyer Jul 30, 2026
e0bb5bf
feat(main): throttle PROGRESS:transcribe/embedding lines in the disk log
valentinweyer Jul 30, 2026
56ead44
feat(transcriber): restore identity-side diarization additions
valentinweyer Jul 30, 2026
1d87b6a
wip: voiceprint/diarization work carried over from local/mic-plus-voi…
valentinweyer Jul 19, 2026
9d89027
fix(speakers): channel-scope prototype matching, correction path, rel…
valentinweyer Jul 22, 2026
ea6ddad
feat(speakers): enroll-self-from-person CLI
valentinweyer Jul 23, 2026
166f83b
fix(e2e): seed a real recording session in processing-stages.t1
valentinweyer Jul 30, 2026
7e20f0f
fix(speakers): verified deletion + a setting to disable identity matc…
valentinweyer Jul 31, 2026
a659327
fix(e2e): make isSayAvailable() check real synthesis, fix impossible …
valentinweyer Aug 1, 2026
c4d4737
docs: update speaker-label docs for per-channel acoustic diarization
valentinweyer Aug 1, 2026
e3bc884
fix(backend-cli): attach stdout/stderr to non-zero-exit rejections
valentinweyer Aug 1, 2026
b54b429
Merge remote-tracking branch 'upstream/feat/speaker-diarization' into…
valentinweyer Aug 3, 2026
fab3bba
Merge branch 'feat/speaker-diarization' into speaker-identity
Optic00 Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions app/backend-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,42 @@
const path = require('path');
const { spawn: _spawnRaw } = require('child_process');

// Wrap spawn so every backend / ollama launch defaults to windowsHide:true.
// Wrap spawn so every backend / ollama launch defaults to windowsHide:true
// AND PYTHONUNBUFFERED:1.
// The PyInstaller backend (stenoai.exe) and bundled ollama.exe are console
// subsystem binaries; without this Electron pops a visible console window on
// Windows for every recording, live-transcribe, query, and the long-lived
// `ollama serve` keeps one open for the whole session. No-op on macOS/Linux.
// Callers can still override by passing an explicit windowsHide.
// subsystem binaries; without windowsHide, Electron pops a visible console
// window on Windows for every recording, live-transcribe, query, and the
// long-lived `ollama serve` keeps one open for the whole session. No-op on
// macOS/Linux.
// PYTHONUNBUFFERED matters because stdout/stderr are piped (not a TTY) here,
// so Python defaults to block-buffering them -- a logger.info() call can sit
// unflushed for many minutes on a long operation (a multi-hour recording's
// ffmpeg preprocessing/diarization/transcription), making the pipeline look
// hung even while it's genuinely working, and starving the inactivity
// watchdog (TRANSCRIBE_INACTIVITY_MS) of the HEARTBEAT:/log lines it needs to
// tell real silence from buffered-but-alive. Harmless for non-Python
// binaries (ollama/ffmpeg) -- just an unused env var.
// Callers can still override either by passing an explicit windowsHide/env.
function spawn(command, args, options) {
const unbufferedEnv = (existingEnv) => ({
...require('process').env,
PYTHONUNBUFFERED: '1',
...(existingEnv || {}),
});
if (Array.isArray(args) || args === undefined || args === null) {
return _spawnRaw(command, args, { windowsHide: true, ...(options || {}) });
const opts = options || {};
return _spawnRaw(command, args, {
windowsHide: true,
...opts,
env: unbufferedEnv(opts.env),
});
}
// 2-arg form: spawn(command, options)
return _spawnRaw(command, { windowsHide: true, ...args });
// 2-arg form: spawn(command, options) -- `args` IS the options object here.
return _spawnRaw(command, {
windowsHide: true,
...args,
env: unbufferedEnv(args.env),
});
}

// Terminate a process AND its child processes. On Windows `process.kill(pid)`
Expand Down Expand Up @@ -152,7 +176,14 @@ function createBackendCli({
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(`Python script failed with code ${code}: ${stderr}`));
const err = new Error(`Python script failed with code ${code}: ${stderr}`);
// Callers (see parsePythonFailureJson in main.js) recover a graceful
// {"success": false, "error": ...} a CLI command printed to stdout
// right before exiting non-zero -- without these, that message is
// unreachable and every failure looks like a generic crash.
err.stdout = stdout;
err.stderr = stderr;
reject(err);
}
});

Expand Down
32 changes: 23 additions & 9 deletions app/backend-cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,42 @@ function recordingDeps(extra = {}) {

// ---- spawn wrapper -------------------------------------------------------

test('spawn defaults windowsHide:true for the (command, args[]) form', () => {
test('spawn defaults windowsHide:true and PYTHONUNBUFFERED:1 for the (command, args[]) form', () => {
spawn('backend', ['a', 'b']);
assert.deepStrictEqual(spawnCalls[0][0], 'backend');
assert.deepStrictEqual(spawnCalls[0][1], ['a', 'b']);
assert.deepStrictEqual(spawnCalls[0][2], { windowsHide: true });
assert.strictEqual(spawnCalls[0][2].windowsHide, true);
assert.strictEqual(spawnCalls[0][2].env.PYTHONUNBUFFERED, '1');
});

test('spawn lets a caller override windowsHide', () => {
spawn('backend', ['a'], { windowsHide: false, cwd: '/x' });
assert.deepStrictEqual(spawnCalls[0][2], { windowsHide: false, cwd: '/x' });
test('spawn lets a caller override windowsHide, and merges (not replaces) env', () => {
spawn('backend', ['a'], { windowsHide: false, cwd: '/x', env: { FOO: 'bar' } });
const opts = spawnCalls[0][2];
assert.strictEqual(opts.windowsHide, false);
assert.strictEqual(opts.cwd, '/x');
assert.strictEqual(opts.env.FOO, 'bar');
assert.strictEqual(opts.env.PYTHONUNBUFFERED, '1');
});

test('spawn lets a caller override PYTHONUNBUFFERED itself', () => {
spawn('backend', ['a'], { env: { PYTHONUNBUFFERED: '0' } });
assert.strictEqual(spawnCalls[0][2].env.PYTHONUNBUFFERED, '0');
});

test('spawn handles the 2-arg (command, options) form', () => {
spawn('backend', { cwd: '/y' });
// Collapsed to the options-object overload; windowsHide defaulted in.
assert.deepStrictEqual(spawnCalls[0][1], { windowsHide: true, cwd: '/y' });
const opts = spawnCalls[0][1];
assert.strictEqual(opts.windowsHide, true);
assert.strictEqual(opts.cwd, '/y');
assert.strictEqual(opts.env.PYTHONUNBUFFERED, '1');
});

test('spawn defaults options when args is null/undefined', () => {
spawn('backend');
assert.deepStrictEqual(spawnCalls[0][1], undefined);
assert.deepStrictEqual(spawnCalls[0][2], { windowsHide: true });
assert.strictEqual(spawnCalls[0][2].windowsHide, true);
assert.strictEqual(spawnCalls[0][2].env.PYTHONUNBUFFERED, '1');
});

// ---- killProcessTree -----------------------------------------------------
Expand Down Expand Up @@ -147,8 +161,8 @@ test('runPythonScript (non-silent) sanitizes the echoed argv and streams output'
// The spawned argv is untouched; only the LOGGED echo is sanitized.
assert.deepStrictEqual(spawnCalls[0][1], ['create-folder', 'secret']);
assert.ok(rec.debug.includes('$ stenoai SANITIZED'));
// No extraEnv -> env is left undefined (inherit parent).
assert.strictEqual(spawnCalls[0][2].env, undefined);
// No extraEnv -> spawn()'s own PYTHONUNBUFFERED default is all that's set.
assert.strictEqual(spawnCalls[0][2].env.PYTHONUNBUFFERED, '1');

stubChild.emit('stdout', 'data', Buffer.from('one\ntwo'));
assert.deepStrictEqual(rec.forwarded, [
Expand Down
Loading
Loading