dsh-live-voice 0.2.2 → 0.3.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/CHANGELOG.md +39 -1
- package/DEVELOPMENT.md +13 -0
- package/LICENSE +201 -674
- package/PLAN.md +28 -0
- package/README.md +65 -110
- package/docs/ARCHITECTURE.md +40 -0
- package/docs/CHOOSING-AN-ENGINE.md +120 -0
- package/docs/CONFIGURATION.md +149 -0
- package/docs/REVIEW.md +35 -0
- package/docs/VOICE-LIFECYCLE.md +59 -0
- package/lib/client.js +3652 -1576
- package/lib/server.js +641 -453
- package/package.json +17 -6
- package/scripts/build.ts +16 -4
- package/scripts/preview-ui.ts +1 -1
- package/src/{client/index.ts → app/client/apply.tsx} +152 -59
- package/src/app/client/i18n/DshLanguageBoundary.tsx +42 -0
- package/src/app/client/i18n/catalogs/base.ts +231 -0
- package/src/app/client/i18n/catalogs/en.ts +258 -0
- package/src/app/client/i18n/catalogs/es.ts +279 -0
- package/src/app/client/i18n/catalogs/fr.ts +281 -0
- package/src/app/client/i18n/catalogs/hi.ts +263 -0
- package/src/app/client/i18n/catalogs/index.ts +18 -0
- package/src/app/client/i18n/catalogs/pt-BR.ts +270 -0
- package/src/app/client/i18n/catalogs/zh.ts +248 -0
- package/src/app/client/i18n/index.ts +4 -0
- package/src/app/client/i18n/registerDshLocales.ts +43 -0
- package/src/app/client/i18n/runtime.tsx +58 -0
- package/src/app/client/index.ts +3 -0
- package/src/app/client/registerSlots.tsx +30 -0
- package/src/app/client/slotDefinitions.ts +32 -0
- package/src/app/server/apply.ts +479 -0
- package/src/app/server/index.ts +2 -0
- package/src/app/server/registerRoutes.ts +11 -0
- package/src/modules/conversation/components/ConversationControls.tsx +40 -0
- package/src/modules/conversation/components/ConversationStatusBar.tsx +155 -0
- package/src/modules/conversation/components/DeliveryModeButton.tsx +43 -0
- package/src/modules/conversation/components/MicrophoneButton.tsx +6 -0
- package/src/modules/conversation/components/PlaybackControls.tsx +41 -0
- package/src/modules/conversation/components/SpeakButton.tsx +30 -0
- package/src/modules/conversation/components/Waveform.tsx +68 -0
- package/src/modules/conversation/components/conversationStatus.ts +17 -0
- package/src/modules/conversation/components/createConversationComponents.tsx +505 -0
- package/src/modules/conversation/components/index.ts +8 -0
- package/src/modules/conversation/hooks/index.ts +2 -0
- package/src/modules/conversation/hooks/useConversationActions.ts +27 -0
- package/src/modules/conversation/hooks/useConversationController.ts +15 -0
- package/src/modules/conversation/index.ts +3 -0
- package/src/{client → modules/conversation/models}/chat.ts +2 -1
- package/src/modules/conversation/models/index.ts +1 -0
- package/src/{core → modules/core}/coordinator.ts +133 -7
- package/src/modules/core/index.ts +6 -0
- package/src/{engines/qwen-http-host.ts → modules/core/qwen/QwenHttpHost.ts} +2 -2
- package/src/modules/core/qwen/QwenSettings.tsx +148 -0
- package/src/{core → modules/core}/settings.ts +23 -0
- package/src/modules/recognition/components/RecognitionCapabilityStatus.tsx +19 -0
- package/src/{engines/recognition/qwen-http.ts → modules/recognition/engines/qwen/QwenRecognitionEngine.ts} +1 -1
- package/src/{engines/recognition/whisper-http.ts → modules/recognition/engines/whisper/WhisperRecognitionEngine.ts} +1 -1
- package/src/modules/recognition/engines/whisper/WhisperSettings.tsx +150 -0
- package/src/modules/recognition/index.ts +4 -0
- package/src/modules/recognition/qwen/QwenRecognitionSettings.tsx +4 -0
- package/src/modules/settings/components/LiveVoiceSettings.tsx +72 -0
- package/src/modules/settings/components/SettingsHeader.tsx +49 -0
- package/src/modules/settings/components/VersionBadges.tsx +39 -0
- package/src/modules/settings/components/createLiveVoiceSettings.tsx +8 -0
- package/src/modules/settings/components/index.ts +4 -0
- package/src/modules/settings/hooks/index.ts +3 -0
- package/src/modules/settings/hooks/useAudioDevices.ts +38 -0
- package/src/modules/settings/hooks/useLiveVoiceSettings.ts +4 -0
- package/src/modules/settings/hooks/useReleaseStatus.ts +17 -0
- package/src/modules/settings/index.ts +4 -0
- package/src/modules/settings/models/index.ts +1 -0
- package/src/modules/settings/models/settingsStorage.ts +14 -0
- package/src/modules/settings/sections/conversation/ConversationDelaySettings.tsx +24 -0
- package/src/modules/settings/sections/conversation/ConversationSettingsSection.tsx +23 -0
- package/src/modules/settings/sections/conversation/DeliverySettings.tsx +38 -0
- package/src/modules/settings/sections/conversation/HoldToTalkSettings.tsx +20 -0
- package/src/modules/settings/sections/conversation/VoiceModeSettings.tsx +31 -0
- package/src/modules/settings/sections/conversation/index.ts +5 -0
- package/src/modules/settings/sections/index.ts +3 -0
- package/src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx +109 -0
- package/src/modules/settings/sections/recognition/RecognitionFilterSettings.tsx +33 -0
- package/src/modules/settings/sections/recognition/RecognitionSettingsSection.tsx +20 -0
- package/src/modules/settings/sections/recognition/RecognitionStatus.tsx +19 -0
- package/src/modules/settings/sections/recognition/SilenceDetectionSettings.tsx +58 -0
- package/src/modules/settings/sections/recognition/VoiceCommandSettings.tsx +41 -0
- package/src/modules/settings/sections/recognition/index.ts +5 -0
- package/src/modules/settings/sections/speak/OutputFilterSettings.tsx +39 -0
- package/src/modules/settings/sections/speak/PlaybackPolicySettings.tsx +26 -0
- package/src/modules/settings/sections/speak/SpeakSettingsSection.tsx +16 -0
- package/src/modules/settings/sections/speak/SpeechAdvancedSettings.tsx +81 -0
- package/src/modules/settings/sections/speak/SpeechEngineSettings.tsx +80 -0
- package/src/modules/settings/sections/speak/index.ts +4 -0
- package/src/modules/settings/services/releases.ts +117 -0
- package/src/modules/speak/engines/audio/HostAudioEngine.ts +167 -0
- package/src/modules/speak/engines/audio/M4aAacTranscoder.ts +58 -0
- package/src/modules/speak/engines/qwen/QwenSpeakingEngine.ts +14 -0
- package/src/{engines/speaking/say.ts → modules/speak/engines/say/SaySpeakingEngine.ts} +26 -4
- package/src/modules/speak/index.ts +5 -0
- package/src/modules/speak/qwen/QwenSpeakingSettings.tsx +4 -0
- package/src/modules/speak/services/speechQueue.ts +2 -0
- package/src/server.ts +9 -365
- package/src/shared/design-system/buttons/IconButton.tsx +33 -0
- package/src/shared/design-system/buttons/PillButton.tsx +11 -0
- package/src/shared/design-system/buttons/ToggleButton.tsx +6 -0
- package/src/shared/design-system/buttons/index.ts +3 -0
- package/src/shared/design-system/feedback/ErrorMessage.tsx +27 -0
- package/src/shared/design-system/feedback/StatusBadge.tsx +9 -0
- package/src/shared/design-system/feedback/StatusMessage.tsx +4 -0
- package/src/shared/design-system/feedback/index.ts +3 -0
- package/src/shared/design-system/forms/CheckboxField.tsx +20 -0
- package/src/shared/design-system/forms/NumberField.tsx +12 -0
- package/src/shared/design-system/forms/SelectField.tsx +20 -0
- package/src/shared/design-system/forms/TextAreaField.tsx +12 -0
- package/src/shared/design-system/forms/TextField.tsx +12 -0
- package/src/shared/design-system/forms/index.ts +5 -0
- package/src/shared/design-system/icons/Icon.tsx +20 -0
- package/src/shared/design-system/icons/icons.ts +16 -0
- package/src/shared/design-system/icons/index.ts +2 -0
- package/src/shared/design-system/index.ts +5 -0
- package/src/shared/design-system/layout/SettingsCard.tsx +9 -0
- package/src/shared/design-system/layout/SettingsSection.tsx +9 -0
- package/src/shared/design-system/layout/SettingsSubcard.tsx +14 -0
- package/src/shared/design-system/layout/SettingsTabs.tsx +54 -0
- package/src/shared/design-system/layout/index.ts +4 -0
- package/src/{client/styles.ts → styles/index.ts} +3 -1
- package/src/client/components.ts +0 -1080
- package/src/client/qwen-settings.ts +0 -144
- package/src/client/whisper-settings.ts +0 -147
- package/src/engines/speaking/qwen-http.ts +0 -124
- /package/src/{core → modules/core}/filters.ts +0 -0
- /package/src/{core → modules/core}/microphone.ts +0 -0
- /package/src/{core → modules/core}/ownership.ts +0 -0
- /package/src/{core → modules/core}/transcript.ts +0 -0
- /package/src/{engines/recognition/browser.ts → modules/recognition/engines/browser/BrowserRecognitionEngine.ts} +0 -0
- /package/src/{engines/recognition/whisper-http-host.ts → modules/recognition/engines/whisper/whisperRecognitionHost.ts} +0 -0
- /package/src/{engines/speaking/browser.ts → modules/speak/engines/browser/BrowserSpeakingEngine.ts} +0 -0
- /package/src/{engines/speaking/say-client.ts → modules/speak/engines/say/sayClient.ts} +0 -0
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
const BASE = '/api/dsh-live-voice/qwen';
|
|
3
|
-
const UNLOADED =
|
|
4
|
-
'Qwen settings routes are not loaded. A normal DSH server restart is required to load updated plugin routes; refreshing this page alone is not enough.';
|
|
5
|
-
export async function qwenSettingsRequest(
|
|
6
|
-
path,
|
|
7
|
-
{ method = 'GET', config, signal } = {},
|
|
8
|
-
fetchImpl = globalThis.fetch,
|
|
9
|
-
) {
|
|
10
|
-
const response = await fetchImpl(BASE + path, {
|
|
11
|
-
method,
|
|
12
|
-
credentials: 'same-origin',
|
|
13
|
-
signal,
|
|
14
|
-
headers: config ? { 'content-type': 'application/json' } : undefined,
|
|
15
|
-
body: config ? JSON.stringify(config) : undefined,
|
|
16
|
-
});
|
|
17
|
-
if (response.status === 401 || response.status === 403)
|
|
18
|
-
throw new Error('Sign in to DSH to manage Qwen settings.');
|
|
19
|
-
if (response.status === 404 || response.status === 405) throw new Error(UNLOADED);
|
|
20
|
-
let body;
|
|
21
|
-
try {
|
|
22
|
-
body = await response.json();
|
|
23
|
-
} catch {
|
|
24
|
-
throw new Error(UNLOADED);
|
|
25
|
-
}
|
|
26
|
-
if (typeof body?.ok !== 'boolean') throw new Error(UNLOADED);
|
|
27
|
-
if (!response.ok || !body.ok)
|
|
28
|
-
throw new Error(body.error?.message || 'Qwen settings request failed.');
|
|
29
|
-
return body.value;
|
|
30
|
-
}
|
|
31
|
-
export function createQwenSettings(React) {
|
|
32
|
-
const h = React.createElement;
|
|
33
|
-
return function QwenSettings({ controller }) {
|
|
34
|
-
const [draft, setDraft] = React.useState({
|
|
35
|
-
baseUrl: 'http://127.0.0.1:8080/',
|
|
36
|
-
timeoutMs: 300000,
|
|
37
|
-
});
|
|
38
|
-
const [busy, setBusy] = React.useState(true),
|
|
39
|
-
[loaded, setLoaded] = React.useState(false),
|
|
40
|
-
[error, setError] = React.useState(''),
|
|
41
|
-
[message, setMessage] = React.useState('');
|
|
42
|
-
const active = React.useRef(null);
|
|
43
|
-
async function run(action) {
|
|
44
|
-
active.current?.abort();
|
|
45
|
-
const abort = new AbortController();
|
|
46
|
-
active.current = abort;
|
|
47
|
-
setBusy(true);
|
|
48
|
-
setError('');
|
|
49
|
-
setMessage('');
|
|
50
|
-
try {
|
|
51
|
-
if (action === 'load') {
|
|
52
|
-
const value = await qwenSettingsRequest('/config', { signal: abort.signal });
|
|
53
|
-
if (!abort.signal.aborted) {
|
|
54
|
-
setDraft(value);
|
|
55
|
-
setLoaded(true);
|
|
56
|
-
}
|
|
57
|
-
} else if (action === 'save') {
|
|
58
|
-
await controller.endConversation?.();
|
|
59
|
-
const value = await qwenSettingsRequest('/config', {
|
|
60
|
-
method: 'PUT',
|
|
61
|
-
config: { ...draft, timeoutMs: Number(draft.timeoutMs) },
|
|
62
|
-
signal: abort.signal,
|
|
63
|
-
});
|
|
64
|
-
if (!abort.signal.aborted) {
|
|
65
|
-
setDraft(value);
|
|
66
|
-
setMessage('Saved on the DSH host. Active Qwen requests were cancelled.');
|
|
67
|
-
await controller.refreshCapabilities?.();
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
const value = await qwenSettingsRequest('/test', {
|
|
71
|
-
method: 'POST',
|
|
72
|
-
config: { ...draft, timeoutMs: Number(draft.timeoutMs) },
|
|
73
|
-
signal: abort.signal,
|
|
74
|
-
});
|
|
75
|
-
if (!abort.signal.aborted) {
|
|
76
|
-
if (!value.supported) throw new Error(value.reason || 'Qwen health check failed.');
|
|
77
|
-
setMessage(
|
|
78
|
-
'Connection successful. Both Qwen ASR and TTS are loaded. Unsaved edits have not been applied.',
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch (reason) {
|
|
83
|
-
if (!abort.signal.aborted) setError(reason.message || String(reason));
|
|
84
|
-
} finally {
|
|
85
|
-
if (!abort.signal.aborted) setBusy(false);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
React.useEffect(() => {
|
|
89
|
-
void run('load');
|
|
90
|
-
return () => active.current?.abort();
|
|
91
|
-
}, []);
|
|
92
|
-
const field = (label, key, type = 'text') =>
|
|
93
|
-
h(
|
|
94
|
-
'label',
|
|
95
|
-
null,
|
|
96
|
-
label,
|
|
97
|
-
h('input', {
|
|
98
|
-
type,
|
|
99
|
-
value: draft[key],
|
|
100
|
-
disabled: busy || !loaded,
|
|
101
|
-
autoComplete: 'off',
|
|
102
|
-
...(type === 'number' ? { min: 1000, max: 600000, step: 1 } : {}),
|
|
103
|
-
onChange: (event) => {
|
|
104
|
-
setDraft({ ...draft, [key]: event.target.value });
|
|
105
|
-
setMessage('Unsaved changes');
|
|
106
|
-
setError('');
|
|
107
|
-
},
|
|
108
|
-
}),
|
|
109
|
-
);
|
|
110
|
-
return h(
|
|
111
|
-
React.Fragment,
|
|
112
|
-
null,
|
|
113
|
-
h(
|
|
114
|
-
'p',
|
|
115
|
-
null,
|
|
116
|
-
'Host-wide settings for the Qwen3 ASR + TTS server. Enter any HTTP or HTTPS base URL reachable from the DSH host. The browser accesses it through authenticated DSH routes.',
|
|
117
|
-
),
|
|
118
|
-
field('Qwen API base URL', 'baseUrl'),
|
|
119
|
-
field('Request timeout (ms)', 'timeoutMs', 'number'),
|
|
120
|
-
h(
|
|
121
|
-
'div',
|
|
122
|
-
{ className: 'dlv-settings-actions' },
|
|
123
|
-
h(
|
|
124
|
-
'button',
|
|
125
|
-
{ type: 'button', disabled: busy || !loaded, onClick: () => run('save') },
|
|
126
|
-
'Save Qwen settings',
|
|
127
|
-
),
|
|
128
|
-
h(
|
|
129
|
-
'button',
|
|
130
|
-
{ type: 'button', disabled: busy || !loaded, onClick: () => run('test') },
|
|
131
|
-
'Test Qwen server',
|
|
132
|
-
),
|
|
133
|
-
h(
|
|
134
|
-
'button',
|
|
135
|
-
{ type: 'button', disabled: busy, onClick: () => run('load') },
|
|
136
|
-
'Reload saved settings',
|
|
137
|
-
),
|
|
138
|
-
),
|
|
139
|
-
busy ? h('p', { role: 'status' }, 'Contacting DSH host…') : null,
|
|
140
|
-
message ? h('p', { role: 'status' }, message) : null,
|
|
141
|
-
error ? h('p', { role: 'alert' }, error) : null,
|
|
142
|
-
);
|
|
143
|
-
};
|
|
144
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
const BASE = '/api/dsh-live-voice/whisper';
|
|
3
|
-
const UNLOADED =
|
|
4
|
-
'Whisper settings routes are not loaded. A normal DSH server restart is required to load updated plugin routes; refreshing this page alone is not enough.';
|
|
5
|
-
export async function whisperSettingsRequest(
|
|
6
|
-
path,
|
|
7
|
-
{ method = 'GET', config, signal } = {},
|
|
8
|
-
fetchImpl = globalThis.fetch,
|
|
9
|
-
) {
|
|
10
|
-
const response = await fetchImpl(BASE + path, {
|
|
11
|
-
method,
|
|
12
|
-
credentials: 'same-origin',
|
|
13
|
-
signal,
|
|
14
|
-
headers: config ? { 'content-type': 'application/json' } : undefined,
|
|
15
|
-
body: config ? JSON.stringify(config) : undefined,
|
|
16
|
-
});
|
|
17
|
-
if (response.status === 401 || response.status === 403)
|
|
18
|
-
throw new Error('Sign in to DSH to manage Whisper settings.');
|
|
19
|
-
if (response.status === 404 || response.status === 405) throw new Error(UNLOADED);
|
|
20
|
-
let body;
|
|
21
|
-
try {
|
|
22
|
-
body = await response.json();
|
|
23
|
-
} catch {
|
|
24
|
-
throw new Error(UNLOADED);
|
|
25
|
-
}
|
|
26
|
-
if (typeof body?.ok !== 'boolean') throw new Error(UNLOADED);
|
|
27
|
-
if (!response.ok || !body.ok)
|
|
28
|
-
throw new Error(body.error?.message || 'Whisper settings request failed.');
|
|
29
|
-
return body.value;
|
|
30
|
-
}
|
|
31
|
-
export function createWhisperSettings(React) {
|
|
32
|
-
const h = React.createElement;
|
|
33
|
-
return function WhisperSettings({ controller }) {
|
|
34
|
-
const [draft, setDraft] = React.useState({
|
|
35
|
-
url: 'http://127.0.0.1:8080/inference',
|
|
36
|
-
healthUrl: '/health',
|
|
37
|
-
timeoutMs: 30000,
|
|
38
|
-
});
|
|
39
|
-
const [busy, setBusy] = React.useState(true),
|
|
40
|
-
[loaded, setLoaded] = React.useState(false),
|
|
41
|
-
[error, setError] = React.useState(''),
|
|
42
|
-
[message, setMessage] = React.useState('');
|
|
43
|
-
const active = React.useRef(null);
|
|
44
|
-
async function run(action) {
|
|
45
|
-
active.current?.abort();
|
|
46
|
-
const abort = new AbortController();
|
|
47
|
-
active.current = abort;
|
|
48
|
-
setBusy(true);
|
|
49
|
-
setError('');
|
|
50
|
-
setMessage('');
|
|
51
|
-
try {
|
|
52
|
-
if (action === 'load') {
|
|
53
|
-
const value = await whisperSettingsRequest('/config', { signal: abort.signal });
|
|
54
|
-
if (!abort.signal.aborted) {
|
|
55
|
-
setDraft(value);
|
|
56
|
-
setLoaded(true);
|
|
57
|
-
}
|
|
58
|
-
} else if (action === 'save') {
|
|
59
|
-
await controller.endConversation?.();
|
|
60
|
-
const value = await whisperSettingsRequest('/config', {
|
|
61
|
-
method: 'PUT',
|
|
62
|
-
config: { ...draft, timeoutMs: Number(draft.timeoutMs) },
|
|
63
|
-
signal: abort.signal,
|
|
64
|
-
});
|
|
65
|
-
if (!abort.signal.aborted) {
|
|
66
|
-
setDraft(value);
|
|
67
|
-
setMessage('Saved on the DSH host. Active host transcription requests were cancelled.');
|
|
68
|
-
await controller.refreshCapabilities?.();
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
const value = await whisperSettingsRequest('/test', {
|
|
72
|
-
method: 'POST',
|
|
73
|
-
config: { ...draft, timeoutMs: Number(draft.timeoutMs) },
|
|
74
|
-
signal: abort.signal,
|
|
75
|
-
});
|
|
76
|
-
if (!abort.signal.aborted) {
|
|
77
|
-
if (!value.supported) throw new Error(value.reason || 'Whisper health check failed.');
|
|
78
|
-
setMessage(
|
|
79
|
-
'Connection successful. Health endpoint responded; transcription was not tested. Unsaved edits have not been applied.',
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
} catch (reason) {
|
|
84
|
-
if (!abort.signal.aborted) setError(reason.message || String(reason));
|
|
85
|
-
} finally {
|
|
86
|
-
if (!abort.signal.aborted) setBusy(false);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
React.useEffect(() => {
|
|
90
|
-
void run('load');
|
|
91
|
-
return () => active.current?.abort();
|
|
92
|
-
}, []);
|
|
93
|
-
function field(label, key, type = 'text') {
|
|
94
|
-
return h(
|
|
95
|
-
'label',
|
|
96
|
-
null,
|
|
97
|
-
label,
|
|
98
|
-
h('input', {
|
|
99
|
-
type,
|
|
100
|
-
value: draft[key],
|
|
101
|
-
disabled: busy || !loaded,
|
|
102
|
-
autoComplete: 'off',
|
|
103
|
-
...(type === 'number' ? { min: 100, max: 300000, step: 1 } : {}),
|
|
104
|
-
onChange: (event) => {
|
|
105
|
-
setDraft({ ...draft, [key]: event.target.value });
|
|
106
|
-
setMessage('Unsaved changes');
|
|
107
|
-
setError('');
|
|
108
|
-
},
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
return h(
|
|
113
|
-
React.Fragment,
|
|
114
|
-
null,
|
|
115
|
-
h(
|
|
116
|
-
'p',
|
|
117
|
-
null,
|
|
118
|
-
'Host-wide settings. Only unauthenticated loopback HTTP URLs (localhost, 127.0.0.1, [::1]) are allowed. Loopback means the DSH host, not this browser. All health checks and audio requests run through the authenticated backend.',
|
|
119
|
-
),
|
|
120
|
-
field('Endpoint URL', 'url'),
|
|
121
|
-
field('Health URL or path', 'healthUrl'),
|
|
122
|
-
field('Request timeout (ms)', 'timeoutMs', 'number'),
|
|
123
|
-
h(
|
|
124
|
-
'div',
|
|
125
|
-
{ className: 'dlv-settings-actions' },
|
|
126
|
-
h(
|
|
127
|
-
'button',
|
|
128
|
-
{ type: 'button', disabled: busy || !loaded, onClick: () => run('save') },
|
|
129
|
-
'Save Whisper settings',
|
|
130
|
-
),
|
|
131
|
-
h(
|
|
132
|
-
'button',
|
|
133
|
-
{ type: 'button', disabled: busy || !loaded, onClick: () => run('test') },
|
|
134
|
-
'Test connection',
|
|
135
|
-
),
|
|
136
|
-
h(
|
|
137
|
-
'button',
|
|
138
|
-
{ type: 'button', disabled: busy, onClick: () => run('load') },
|
|
139
|
-
'Reload saved settings',
|
|
140
|
-
),
|
|
141
|
-
),
|
|
142
|
-
busy ? h('p', { role: 'status' }, 'Contacting DSH host…') : null,
|
|
143
|
-
message ? h('p', { role: 'status' }, message) : null,
|
|
144
|
-
error ? h('p', { role: 'alert' }, error) : null,
|
|
145
|
-
);
|
|
146
|
-
};
|
|
147
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
const BASE = '/api/dsh-live-voice/qwen';
|
|
3
|
-
const cancelled = () => Object.assign(new Error('Speech was cancelled.'), { name: 'AbortError' });
|
|
4
|
-
|
|
5
|
-
/** Fetches host-local Qwen WAV audio through authenticated DSH and plays it in
|
|
6
|
-
* the user's browser. The resident server owns model lifetime. */
|
|
7
|
-
export class QwenHttpSpeakingEngine {
|
|
8
|
-
constructor({ globals = globalThis, lang = 'pt-BR' } = {}) {
|
|
9
|
-
this.g = globals;
|
|
10
|
-
this.lang = lang;
|
|
11
|
-
this.current = null;
|
|
12
|
-
}
|
|
13
|
-
async capability() {
|
|
14
|
-
try {
|
|
15
|
-
const response = await this.g.fetch(BASE + '/capabilities?kind=tts', {
|
|
16
|
-
credentials: 'same-origin',
|
|
17
|
-
}),
|
|
18
|
-
json = await response.json();
|
|
19
|
-
if (!response.ok || !json?.ok)
|
|
20
|
-
throw new Error(json?.error?.message || 'Qwen capability check failed.');
|
|
21
|
-
return { ...json.value, pause: true, resume: true };
|
|
22
|
-
} catch (error) {
|
|
23
|
-
return {
|
|
24
|
-
supported: false,
|
|
25
|
-
local: true,
|
|
26
|
-
location: 'host',
|
|
27
|
-
pause: true,
|
|
28
|
-
resume: true,
|
|
29
|
-
reason: 'Qwen speech server check failed: ' + (error?.message || error),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
async speak(
|
|
34
|
-
text,
|
|
35
|
-
{ rate = 1, signal, lang = this.lang, voice = 'aiden', outputDeviceId = '' } = {},
|
|
36
|
-
) {
|
|
37
|
-
if (typeof text !== 'string') throw new TypeError('Speech text must be a string.');
|
|
38
|
-
if (!Number.isFinite(rate) || rate < 0.1 || rate > 3)
|
|
39
|
-
throw new RangeError('Speech rate must be between 0.1 and 3.');
|
|
40
|
-
if (signal?.aborted) throw cancelled();
|
|
41
|
-
await this.stop();
|
|
42
|
-
if (!text.trim()) return;
|
|
43
|
-
const operation = { abort: new AbortController(), audio: null, url: null };
|
|
44
|
-
this.current = operation;
|
|
45
|
-
const cancel = () => operation.abort.abort();
|
|
46
|
-
signal?.addEventListener('abort', cancel, { once: true });
|
|
47
|
-
if (signal?.aborted) cancel();
|
|
48
|
-
try {
|
|
49
|
-
const response = await this.g.fetch(BASE + '/speech', {
|
|
50
|
-
method: 'POST',
|
|
51
|
-
credentials: 'same-origin',
|
|
52
|
-
headers: { 'content-type': 'application/json' },
|
|
53
|
-
body: JSON.stringify({ text, lang, voice }),
|
|
54
|
-
signal: operation.abort.signal,
|
|
55
|
-
});
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
let body;
|
|
58
|
-
try {
|
|
59
|
-
body = await response.json();
|
|
60
|
-
} catch {}
|
|
61
|
-
throw new Error(body?.error?.message || `Qwen synthesis failed (${response.status}).`);
|
|
62
|
-
}
|
|
63
|
-
if (operation.abort.signal.aborted) throw cancelled();
|
|
64
|
-
const blob = await response.blob();
|
|
65
|
-
operation.url = this.g.URL.createObjectURL(blob);
|
|
66
|
-
const audio = (operation.audio = new this.g.Audio(operation.url));
|
|
67
|
-
audio.playbackRate = rate;
|
|
68
|
-
if (outputDeviceId && typeof audio.setSinkId === 'function')
|
|
69
|
-
await audio.setSinkId(outputDeviceId);
|
|
70
|
-
await new Promise((resolve, reject) => {
|
|
71
|
-
const done = () => {
|
|
72
|
-
cleanup();
|
|
73
|
-
resolve();
|
|
74
|
-
},
|
|
75
|
-
failed = () => {
|
|
76
|
-
cleanup();
|
|
77
|
-
reject(new Error('The browser could not play Qwen speech audio.'));
|
|
78
|
-
},
|
|
79
|
-
aborted = () => {
|
|
80
|
-
cleanup();
|
|
81
|
-
audio.pause();
|
|
82
|
-
reject(cancelled());
|
|
83
|
-
},
|
|
84
|
-
cleanup = () => {
|
|
85
|
-
audio.removeEventListener('ended', done);
|
|
86
|
-
audio.removeEventListener('error', failed);
|
|
87
|
-
operation.abort.signal.removeEventListener('abort', aborted);
|
|
88
|
-
};
|
|
89
|
-
audio.addEventListener('ended', done, { once: true });
|
|
90
|
-
audio.addEventListener('error', failed, { once: true });
|
|
91
|
-
operation.abort.signal.addEventListener('abort', aborted, { once: true });
|
|
92
|
-
Promise.resolve(audio.play()).catch(failed);
|
|
93
|
-
});
|
|
94
|
-
} catch (error) {
|
|
95
|
-
if (operation.abort.signal.aborted && error?.name !== 'AbortError') throw cancelled();
|
|
96
|
-
throw error;
|
|
97
|
-
} finally {
|
|
98
|
-
signal?.removeEventListener('abort', cancel);
|
|
99
|
-
if (operation.url) this.g.URL.revokeObjectURL(operation.url);
|
|
100
|
-
if (this.current === operation) this.current = null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
async stop() {
|
|
104
|
-
const operation = this.current;
|
|
105
|
-
if (!operation) return;
|
|
106
|
-
this.current = null;
|
|
107
|
-
operation.abort.abort();
|
|
108
|
-
operation.audio?.pause();
|
|
109
|
-
if (operation.url) {
|
|
110
|
-
this.g.URL.revokeObjectURL(operation.url);
|
|
111
|
-
operation.url = null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
pause() {
|
|
115
|
-
if (!this.current?.audio || this.current.audio.paused) return false;
|
|
116
|
-
this.current.audio.pause();
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
async resume() {
|
|
120
|
-
if (!this.current?.audio || !this.current.audio.paused) return false;
|
|
121
|
-
await this.current.audio.play();
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/{engines/speaking/browser.ts → modules/speak/engines/browser/BrowserSpeakingEngine.ts}
RENAMED
|
File without changes
|
|
File without changes
|