dsh-live-voice 0.0.1-developing → 0.1.0
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/DEVELOPMENT.md +70 -0
- package/HISTORY.md +10 -85
- package/PLAN.md +244 -0
- package/README.md +88 -96
- package/cordis.patch.yml +3 -0
- package/lib/client.js +3015 -0
- package/lib/server.js +1006 -0
- package/package.json +69 -6
- package/scripts/build.ts +50 -0
- package/scripts/check-dist.mjs +38 -0
- package/scripts/preview-ui.ts +31 -0
- package/scripts/probe-browser.ts +46 -0
- package/src/client/chat.ts +40 -0
- package/src/client/components.ts +778 -0
- package/src/client/index.ts +430 -0
- package/src/client/qwen-settings.ts +144 -0
- package/src/client/styles.ts +36 -0
- package/src/client/whisper-settings.ts +147 -0
- package/src/core/coordinator.ts +564 -0
- package/src/core/microphone.ts +171 -0
- package/src/core/ownership.ts +31 -0
- package/src/core/settings.ts +113 -0
- package/src/core/transcript.ts +48 -0
- package/src/engines/qwen-http-host.ts +240 -0
- package/src/engines/recognition/browser.ts +314 -0
- package/src/engines/recognition/qwen-http.ts +36 -0
- package/src/engines/recognition/whisper-http-host.ts +210 -0
- package/src/engines/recognition/whisper-http.ts +191 -0
- package/src/engines/speaking/browser.ts +136 -0
- package/src/engines/speaking/qwen-http.ts +119 -0
- package/src/engines/speaking/say-client.ts +96 -0
- package/src/engines/speaking/say.ts +271 -0
- package/src/server.ts +365 -0
- package/AGENTS.md +0 -52
package/package.json
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-live-voice",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"typecheck": "tsc --noEmit",
|
|
7
|
+
"test": "npm run build && node --test .test-dist/*.test.js",
|
|
8
|
+
"build": "npm run typecheck && node --experimental-strip-types scripts/build.ts",
|
|
9
|
+
"check:dist": "node scripts/check-dist.mjs",
|
|
10
|
+
"dev": "node --experimental-strip-types scripts/build.ts --watch",
|
|
11
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.ts\"",
|
|
12
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.ts\"",
|
|
13
|
+
"setup:hooks": "git config core.hooksPath .githooks",
|
|
14
|
+
"prepack": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"description": "Local-first voice conversations for DSH, with local speech recognition and synthesis and optional external providers.",
|
|
5
17
|
"keywords": [
|
|
6
18
|
"dsh",
|
|
19
|
+
"dsh-plugin",
|
|
20
|
+
"deepseek-harness",
|
|
7
21
|
"local-first",
|
|
8
22
|
"local-voice",
|
|
23
|
+
"voice-assistant",
|
|
9
24
|
"voice-conversation",
|
|
25
|
+
"continuous-conversation",
|
|
26
|
+
"voice-dictation",
|
|
10
27
|
"speech-to-text",
|
|
11
28
|
"text-to-speech",
|
|
12
29
|
"speech-recognition",
|
|
13
30
|
"speech-synthesis",
|
|
14
31
|
"stt",
|
|
15
32
|
"tts",
|
|
33
|
+
"whisper",
|
|
34
|
+
"whisper-cpp",
|
|
35
|
+
"qwen3-asr",
|
|
36
|
+
"qwen3-tts",
|
|
37
|
+
"mlx",
|
|
38
|
+
"apple-silicon",
|
|
39
|
+
"web-speech-api",
|
|
40
|
+
"macos-say",
|
|
16
41
|
"turn-taking",
|
|
17
|
-
"voice-interruption"
|
|
42
|
+
"voice-interruption",
|
|
43
|
+
"silence-detection"
|
|
18
44
|
],
|
|
19
45
|
"author": {
|
|
20
46
|
"name": "wads.dev",
|
|
@@ -22,17 +48,54 @@
|
|
|
22
48
|
},
|
|
23
49
|
"license": "GPL-3.0-only",
|
|
24
50
|
"files": [
|
|
51
|
+
"src/",
|
|
52
|
+
"lib/",
|
|
53
|
+
"cordis.patch.yml",
|
|
25
54
|
"README.md",
|
|
26
|
-
"
|
|
55
|
+
"PLAN.md",
|
|
56
|
+
"DEVELOPMENT.md",
|
|
27
57
|
"HISTORY.md",
|
|
28
|
-
"
|
|
58
|
+
"scripts/",
|
|
59
|
+
"LICENSE"
|
|
29
60
|
],
|
|
30
61
|
"publishConfig": {
|
|
31
62
|
"access": "public",
|
|
32
|
-
"tag": "
|
|
63
|
+
"tag": "latest"
|
|
33
64
|
},
|
|
34
65
|
"repository": {
|
|
35
66
|
"type": "git",
|
|
36
67
|
"url": "git+https://github.com/victorwads/dsh-live-voice.git"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "^26.5.1",
|
|
71
|
+
"@types/react": "^19.3.0",
|
|
72
|
+
"@types/react-dom": "^19.3.0",
|
|
73
|
+
"esbuild": "^0.28.2",
|
|
74
|
+
"jsdom": "^30.0.1",
|
|
75
|
+
"playwright-core": "^1.63.0",
|
|
76
|
+
"prettier": "3.9.6",
|
|
77
|
+
"react": "^19.3.0",
|
|
78
|
+
"react-dom": "^19.3.0",
|
|
79
|
+
"typescript": "^7.0.2"
|
|
80
|
+
},
|
|
81
|
+
"main": "./lib/server.js",
|
|
82
|
+
"exports": {
|
|
83
|
+
".": "./lib/server.js",
|
|
84
|
+
"./client": "./lib/client.js",
|
|
85
|
+
"./package.json": "./package.json"
|
|
86
|
+
},
|
|
87
|
+
"dsh": {
|
|
88
|
+
"bundle": {
|
|
89
|
+
"patch": "./cordis.patch.yml"
|
|
90
|
+
},
|
|
91
|
+
"client": {
|
|
92
|
+
"platform": "web",
|
|
93
|
+
"inject": [
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-chat",
|
|
96
|
+
"@deepseek-ai/dsh-client-connection",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
98
|
+
]
|
|
99
|
+
}
|
|
37
100
|
}
|
|
38
101
|
}
|
package/scripts/build.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { build, context, type BuildOptions } from 'esbuild';
|
|
3
|
+
import { rm, mkdir } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
const client: BuildOptions = {
|
|
6
|
+
entryPoints: ['src/client/index.ts'],
|
|
7
|
+
outfile: 'lib/client.js',
|
|
8
|
+
bundle: true,
|
|
9
|
+
format: 'cjs',
|
|
10
|
+
platform: 'browser',
|
|
11
|
+
target: ['es2022'],
|
|
12
|
+
external: ['react'],
|
|
13
|
+
banner: {
|
|
14
|
+
js: 'window.__ModuleLoader__.load({id:"dsh-live-voice",factory:(require)=>{var module={exports:{}};var exports=module.exports;',
|
|
15
|
+
},
|
|
16
|
+
footer: { js: 'return module.exports;}});' },
|
|
17
|
+
logLevel: 'info',
|
|
18
|
+
};
|
|
19
|
+
const host: BuildOptions = {
|
|
20
|
+
entryPoints: ['src/server.ts'],
|
|
21
|
+
outfile: 'lib/server.js',
|
|
22
|
+
bundle: true,
|
|
23
|
+
format: 'esm',
|
|
24
|
+
platform: 'node',
|
|
25
|
+
target: ['node22'],
|
|
26
|
+
packages: 'external',
|
|
27
|
+
logLevel: 'info',
|
|
28
|
+
};
|
|
29
|
+
const tests: BuildOptions = {
|
|
30
|
+
entryPoints: ['test/*.test.ts'],
|
|
31
|
+
outdir: '.test-dist',
|
|
32
|
+
outbase: 'test',
|
|
33
|
+
bundle: true,
|
|
34
|
+
format: 'esm',
|
|
35
|
+
platform: 'node',
|
|
36
|
+
target: ['node22'],
|
|
37
|
+
packages: 'external',
|
|
38
|
+
logLevel: 'silent',
|
|
39
|
+
};
|
|
40
|
+
const watch = process.argv.includes('--watch');
|
|
41
|
+
if (watch) {
|
|
42
|
+
const clientContext = await context(client);
|
|
43
|
+
await clientContext.watch();
|
|
44
|
+
const hostContext = await context(host);
|
|
45
|
+
await hostContext.watch();
|
|
46
|
+
} else {
|
|
47
|
+
await rm('.test-dist', { recursive: true, force: true });
|
|
48
|
+
await mkdir('.test-dist', { recursive: true });
|
|
49
|
+
await Promise.all([build(client), build(host), build(tests)]);
|
|
50
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const runtimeEntries = ['lib/client.js', 'lib/server.js'];
|
|
4
|
+
|
|
5
|
+
const run = (command, args, options = {}) => {
|
|
6
|
+
const result = spawnSync(command, args, {
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
stdio: options.capture ? 'pipe' : 'inherit',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (result.error) throw result.error;
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
17
|
+
const build = run(npm, ['run', 'build']);
|
|
18
|
+
if (build.status !== 0) process.exit(build.status ?? 1);
|
|
19
|
+
|
|
20
|
+
const tracked = run('git', ['ls-files', '--error-unmatch', ...runtimeEntries], { capture: true });
|
|
21
|
+
if (tracked.status !== 0) {
|
|
22
|
+
console.error(
|
|
23
|
+
`\nThe dsh.pub runtime entries must be committed:\n${runtimeEntries
|
|
24
|
+
.map((entry) => ` - ${entry}`)
|
|
25
|
+
.join('\n')}\n`,
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const diff = run('git', ['diff', '--exit-code', '--', ...runtimeEntries]);
|
|
31
|
+
if (diff.status !== 0) {
|
|
32
|
+
console.error(
|
|
33
|
+
'\nGenerated runtime entries are stale. Review and stage the rebuilt lib files before pushing.\n',
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('Committed dsh.pub runtime entries match the current source.');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { build } from 'esbuild';
|
|
3
|
+
import { chromium } from 'playwright-core';
|
|
4
|
+
import { mkdir } from 'node:fs/promises';
|
|
5
|
+
const entry = `import React from 'react'; import {createRoot} from 'react-dom/client'; import {createComponents} from './src/client/components.ts'; import {styles} from './src/client/styles.ts'; const style=document.createElement('style');style.textContent=styles;document.head.appendChild(style); const state={conversation:true,listening:true,recognizing:true,speaking:false,paused:false,settings:{},capabilities:{}}; const c={subscribe:()=>()=>{},getSnapshot:()=>state,meter:{level:()=>0.4}};const {RecordingBar}=createComponents(React);createRoot(document.getElementById('root')).render(React.createElement(RecordingBar,{controller:c}));`;
|
|
6
|
+
const output = await build({
|
|
7
|
+
stdin: { contents: entry, resolveDir: process.cwd() },
|
|
8
|
+
bundle: true,
|
|
9
|
+
write: false,
|
|
10
|
+
format: 'iife',
|
|
11
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
12
|
+
});
|
|
13
|
+
const browser = await chromium.launch({
|
|
14
|
+
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
15
|
+
headless: true,
|
|
16
|
+
args: ['--disable-background-networking', '--disable-component-update'],
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
const page = await browser.newPage({ viewport: { width: 980, height: 220 } });
|
|
20
|
+
await page.route('**/*', (r) => r.abort());
|
|
21
|
+
await page.setContent(
|
|
22
|
+
'<style>:root{--dsw-alias-label-primary:#eee;--dsw-alias-label-secondary:#aaa;--dsw-alias-border-l1:#444;--dsw-alias-border-l2:#555;--dsw-alias-bg-layer-1:#292929}body{background:#171717;color:#eee;font:14px system-ui;padding:20px}textarea{margin-top:12px;box-sizing:border-box;width:100%;height:80px;background:#292929;color:#aaa;border:1px solid #444;border-radius:22px;padding:18px}</style><div id="root"></div><textarea placeholder="Isolated preview — not the authenticated DSH page"></textarea>',
|
|
23
|
+
);
|
|
24
|
+
await page.addScriptTag({ content: output.outputFiles[0].text });
|
|
25
|
+
await page.locator('[aria-label="Voice controls"]').waitFor();
|
|
26
|
+
await mkdir('artifacts', { recursive: true });
|
|
27
|
+
await page.screenshot({ path: 'artifacts/voice-ui-preview.png' });
|
|
28
|
+
console.log('Saved isolated UI preview; meter value is synthetic, no audio capture.');
|
|
29
|
+
} finally {
|
|
30
|
+
await browser.close();
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { chromium } from 'playwright-core';
|
|
3
|
+
const browser = await chromium.launch({
|
|
4
|
+
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
5
|
+
headless: true,
|
|
6
|
+
args: ['--disable-background-networking', '--disable-component-update'],
|
|
7
|
+
});
|
|
8
|
+
try {
|
|
9
|
+
const context = await browser.newContext();
|
|
10
|
+
await context.route('**/*', (route) => {
|
|
11
|
+
if (route.request().url() === 'http://127.0.0.1:3080/')
|
|
12
|
+
return route.fulfill({
|
|
13
|
+
contentType: 'text/html',
|
|
14
|
+
body: '<html><body>Isolated capability probe, not the DSH application.</body></html>',
|
|
15
|
+
});
|
|
16
|
+
return route.abort();
|
|
17
|
+
});
|
|
18
|
+
const page = await context.newPage();
|
|
19
|
+
await page.goto('http://127.0.0.1:3080/');
|
|
20
|
+
const result = await page.evaluate(async () => {
|
|
21
|
+
const C = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
22
|
+
const result = {
|
|
23
|
+
userAgent: navigator.userAgent,
|
|
24
|
+
secureContext: isSecureContext,
|
|
25
|
+
recognitionConstructor: !!C,
|
|
26
|
+
processLocally: C ? 'processLocally' in new C() : false,
|
|
27
|
+
availabilityAPI: typeof C?.available,
|
|
28
|
+
localVoices: speechSynthesis.getVoices().filter((v) => v.localService).length,
|
|
29
|
+
};
|
|
30
|
+
if (typeof C?.available === 'function')
|
|
31
|
+
for (const lang of ['pt-BR', 'en-US']) {
|
|
32
|
+
try {
|
|
33
|
+
result[lang] = await Promise.race([
|
|
34
|
+
C.available({ langs: [lang], processLocally: true }),
|
|
35
|
+
new Promise((r) => setTimeout(() => r('timeout'), 3000)),
|
|
36
|
+
]);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
result[lang] = error.name + ': ' + error.message;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
});
|
|
43
|
+
console.log(JSON.stringify(result, null, 2));
|
|
44
|
+
} finally {
|
|
45
|
+
await browser.close();
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/** Read only visible assistant prose from the supported Chat target snapshot. */
|
|
3
|
+
export function assistantMessages(snapshot) {
|
|
4
|
+
const nodes = snapshot?.nodes?.values?.() || [];
|
|
5
|
+
return [...nodes]
|
|
6
|
+
.filter(
|
|
7
|
+
(node) =>
|
|
8
|
+
(node.kind === 'assistant-step' || node.kind === 'assistant') &&
|
|
9
|
+
node.visibility !== 'hidden',
|
|
10
|
+
)
|
|
11
|
+
.map((node) => {
|
|
12
|
+
const data = node.data;
|
|
13
|
+
return {
|
|
14
|
+
id: String(data.turn) + ':' + String(data.step),
|
|
15
|
+
messageId: data.finalNode?.messageId,
|
|
16
|
+
turn: data.turn,
|
|
17
|
+
step: data.step,
|
|
18
|
+
complete: data.status !== 'running',
|
|
19
|
+
interrupted: data.status === 'interrupted',
|
|
20
|
+
text: (data.blocks || [])
|
|
21
|
+
.filter((block) => block.kind === 'text')
|
|
22
|
+
.map((block) => block.text || '')
|
|
23
|
+
.join(''),
|
|
24
|
+
};
|
|
25
|
+
})
|
|
26
|
+
.sort((a, b) => a.turn - b.turn || a.step - b.step);
|
|
27
|
+
}
|
|
28
|
+
export function latestUserSequence(snapshot) {
|
|
29
|
+
return Math.max(
|
|
30
|
+
-1,
|
|
31
|
+
...[...(snapshot?.nodes?.values?.() || [])]
|
|
32
|
+
.filter((node) => node.kind === 'user' || node.kind === 'steering')
|
|
33
|
+
.map((node) => Number(node.anchorSeq ?? node.data?.seq ?? -1)),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
export function addressedTurn(messages, messageId) {
|
|
37
|
+
const addressed = messages.find((message) => String(message.messageId) === String(messageId));
|
|
38
|
+
if (!addressed) return { text: '', ids: [] };
|
|
39
|
+
return { text: addressed.text, id: addressed.id };
|
|
40
|
+
}
|