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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../app/client/i18n/index.js';
|
|
3
|
+
import { ErrorMessage, SettingsTabs } from '../../../shared/design-system/index.js';
|
|
4
|
+
import { useLiveVoiceSettings, useLiveVoiceSettingsActions } from '../hooks/index.js';
|
|
5
|
+
import {
|
|
6
|
+
ConversationSettingsSection,
|
|
7
|
+
RecognitionSettingsSection,
|
|
8
|
+
SpeakSettingsSection,
|
|
9
|
+
} from '../sections/index.js';
|
|
10
|
+
import { SettingsHeader } from './SettingsHeader.js';
|
|
11
|
+
|
|
12
|
+
export function LiveVoiceSettings({ controller, onClose }: { controller: any; onClose?(): void }) {
|
|
13
|
+
const { scoped: commons } = useLanguage((ctx) => ctx.commons);
|
|
14
|
+
const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
|
|
15
|
+
const state = useLiveVoiceSettings<any>(controller);
|
|
16
|
+
const { invoke, error, clearError } = useLiveVoiceSettingsActions(controller);
|
|
17
|
+
const [activeTab, setActiveTab] = React.useState('conversation');
|
|
18
|
+
const tabsId = React.useId();
|
|
19
|
+
const tabs = [
|
|
20
|
+
{ id: 'speech', label: (settingsLanguage as any).tabs.speak() },
|
|
21
|
+
{ id: 'recognition', label: (settingsLanguage as any).tabs.recognition() },
|
|
22
|
+
{ id: 'conversation', label: (settingsLanguage as any).tabs.conversation() },
|
|
23
|
+
];
|
|
24
|
+
const sectionProps = {
|
|
25
|
+
controller,
|
|
26
|
+
settings: state.settings,
|
|
27
|
+
capabilities: state.capabilities,
|
|
28
|
+
speaking: state.speaking,
|
|
29
|
+
invoke,
|
|
30
|
+
|
|
31
|
+
updateSettings: (next: any) => invoke('updateSettings', next),
|
|
32
|
+
};
|
|
33
|
+
return (
|
|
34
|
+
<section className="dlv-settings" aria-label={(settingsLanguage as any).title()}>
|
|
35
|
+
<SettingsHeader onClose={onClose} />
|
|
36
|
+
<SettingsTabs
|
|
37
|
+
label={(settingsLanguage as any).title()}
|
|
38
|
+
tabs={tabs}
|
|
39
|
+
active={activeTab}
|
|
40
|
+
idPrefix={tabsId}
|
|
41
|
+
onChange={setActiveTab}
|
|
42
|
+
/>
|
|
43
|
+
{tabs.map((tab) => (
|
|
44
|
+
<div
|
|
45
|
+
key={tab.id}
|
|
46
|
+
id={`${tabsId}-panel-${tab.id}`}
|
|
47
|
+
className="dlv-settings-panel"
|
|
48
|
+
role="tabpanel"
|
|
49
|
+
aria-labelledby={`${tabsId}-tab-${tab.id}`}
|
|
50
|
+
hidden={activeTab !== tab.id}
|
|
51
|
+
>
|
|
52
|
+
{tab.id === 'speech' ? (
|
|
53
|
+
<SpeakSettingsSection {...sectionProps} />
|
|
54
|
+
) : tab.id === 'recognition' ? (
|
|
55
|
+
<RecognitionSettingsSection {...sectionProps} />
|
|
56
|
+
) : (
|
|
57
|
+
<ConversationSettingsSection {...sectionProps} />
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
<ErrorMessage
|
|
62
|
+
error={error || state.error}
|
|
63
|
+
dismissLabel={(commons as any).dismissError()}
|
|
64
|
+
dismissText={(commons as any).dismiss()}
|
|
65
|
+
onDismiss={() => {
|
|
66
|
+
clearError();
|
|
67
|
+
controller.clearError();
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
</section>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../app/client/i18n/index.js';
|
|
3
|
+
import { REPOSITORY_URL } from '../services/releases.js';
|
|
4
|
+
import { IconButton } from '../../../shared/design-system/index.js';
|
|
5
|
+
import { useReleaseStatus } from '../hooks/index.js';
|
|
6
|
+
import { VersionBadges } from './VersionBadges.js';
|
|
7
|
+
export function SettingsHeader({ onClose }: { onClose?(): void }) {
|
|
8
|
+
const { scoped: commons } = useLanguage((ctx) => ctx.commons);
|
|
9
|
+
const { scoped: settings } = useLanguage((ctx) => ctx.settings);
|
|
10
|
+
const { latestRelease, updateAvailable } = useReleaseStatus();
|
|
11
|
+
return (
|
|
12
|
+
<div className="dlv-settings-heading">
|
|
13
|
+
<h3>{(commons as any).pluginName()}</h3>
|
|
14
|
+
<VersionBadges />
|
|
15
|
+
<span className="dlv-heading-divider" aria-hidden />
|
|
16
|
+
{updateAvailable && latestRelease ? (
|
|
17
|
+
<>
|
|
18
|
+
<a
|
|
19
|
+
className="dlv-version-badge dlv-update-badge"
|
|
20
|
+
href={latestRelease.url}
|
|
21
|
+
target="_blank"
|
|
22
|
+
rel="noreferrer"
|
|
23
|
+
aria-label={(commons as any).update.link({ version: latestRelease.tag })}
|
|
24
|
+
title={(commons as any).update.version({ version: latestRelease.tag })}
|
|
25
|
+
>
|
|
26
|
+
<span className="dlv-update-icon" aria-hidden>
|
|
27
|
+
↑
|
|
28
|
+
</span>
|
|
29
|
+
{(commons as any).update.label()}
|
|
30
|
+
</a>
|
|
31
|
+
<span className="dlv-heading-divider" aria-hidden />
|
|
32
|
+
</>
|
|
33
|
+
) : null}
|
|
34
|
+
<a
|
|
35
|
+
className="dlv-version-badge dlv-star-badge"
|
|
36
|
+
href={REPOSITORY_URL}
|
|
37
|
+
target="_blank"
|
|
38
|
+
rel="noreferrer"
|
|
39
|
+
aria-label={`Star DSH Live Voice on GitHub`}
|
|
40
|
+
title={(commons as any).repository.starLabel()}
|
|
41
|
+
>
|
|
42
|
+
★ {(commons as any).repository.starLabel()}
|
|
43
|
+
</a>
|
|
44
|
+
{onClose ? (
|
|
45
|
+
<IconButton icon="close" label={(settings as any).close()} onClick={onClose} />
|
|
46
|
+
) : null}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../app/client/i18n/index.js';
|
|
3
|
+
import {
|
|
4
|
+
CURRENT_VERSION,
|
|
5
|
+
DSH_BADGE_URL,
|
|
6
|
+
LIVE_VOICE_BADGE_URL,
|
|
7
|
+
RELEASES_URL,
|
|
8
|
+
TESTED_DSH_RELEASE_URL,
|
|
9
|
+
TESTED_DSH_VERSION,
|
|
10
|
+
} from '../services/releases.js';
|
|
11
|
+
export function VersionBadges() {
|
|
12
|
+
const { scoped: commons } = useLanguage((ctx) => ctx.commons);
|
|
13
|
+
return (
|
|
14
|
+
<div className="dlv-version-badges" aria-label={(commons as any).version.title()}>
|
|
15
|
+
<a
|
|
16
|
+
className="dlv-shields-badge"
|
|
17
|
+
href={RELEASES_URL}
|
|
18
|
+
target="_blank"
|
|
19
|
+
rel="noreferrer"
|
|
20
|
+
aria-label={(commons as any).version.link({ version: CURRENT_VERSION })}
|
|
21
|
+
title={(commons as any).version.label({ version: CURRENT_VERSION })}
|
|
22
|
+
>
|
|
23
|
+
<img src={LIVE_VOICE_BADGE_URL} alt="" />
|
|
24
|
+
<span>{`v${CURRENT_VERSION}`}</span>
|
|
25
|
+
</a>
|
|
26
|
+
<a
|
|
27
|
+
className="dlv-shields-badge"
|
|
28
|
+
href={TESTED_DSH_RELEASE_URL}
|
|
29
|
+
target="_blank"
|
|
30
|
+
rel="noreferrer"
|
|
31
|
+
aria-label={(commons as any).version.compatibilityLink({ version: TESTED_DSH_VERSION })}
|
|
32
|
+
title={(commons as any).version.compatibility({ version: TESTED_DSH_VERSION })}
|
|
33
|
+
>
|
|
34
|
+
<img src={DSH_BADGE_URL} alt="" />
|
|
35
|
+
<span>{`v${TESTED_DSH_VERSION}`}</span>
|
|
36
|
+
</a>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LiveVoiceSettings } from './LiveVoiceSettings.js';
|
|
3
|
+
|
|
4
|
+
export function createLiveVoiceSettings(..._legacyArguments: unknown[]) {
|
|
5
|
+
return function SettingsPanel({ controller, onClose }: { controller: any; onClose?(): void }) {
|
|
6
|
+
return <LiveVoiceSettings controller={controller} onClose={onClose} />;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export function useAudioDevices() {
|
|
3
|
+
const [audioDevices, setAudioDevices] = React.useState<MediaDeviceInfo[]>([]);
|
|
4
|
+
React.useEffect(() => {
|
|
5
|
+
let active = true;
|
|
6
|
+
const refresh = async () => {
|
|
7
|
+
try {
|
|
8
|
+
const devices = await globalThis.navigator?.mediaDevices?.enumerateDevices?.();
|
|
9
|
+
if (active) setAudioDevices(Array.from(devices || []));
|
|
10
|
+
} catch {
|
|
11
|
+
if (active) setAudioDevices([]);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
void refresh();
|
|
15
|
+
globalThis.navigator?.mediaDevices?.addEventListener?.('devicechange', refresh);
|
|
16
|
+
return () => {
|
|
17
|
+
active = false;
|
|
18
|
+
globalThis.navigator?.mediaDevices?.removeEventListener?.('devicechange', refresh);
|
|
19
|
+
};
|
|
20
|
+
}, []);
|
|
21
|
+
return audioDevices;
|
|
22
|
+
}
|
|
23
|
+
export function createDeviceOptions(
|
|
24
|
+
devices: readonly MediaDeviceInfo[],
|
|
25
|
+
kind: MediaDeviceKind,
|
|
26
|
+
systemDefault: string,
|
|
27
|
+
fallback: string,
|
|
28
|
+
) {
|
|
29
|
+
return [
|
|
30
|
+
{ value: '', label: systemDefault },
|
|
31
|
+
...devices
|
|
32
|
+
.filter((device) => device.kind === kind && device.deviceId && device.deviceId !== 'default')
|
|
33
|
+
.map((device, index) => ({
|
|
34
|
+
value: device.deviceId,
|
|
35
|
+
label: device.label || `${fallback} ${index + 1}`,
|
|
36
|
+
})),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { checkLatestRelease, hasNewerRelease } from '../services/releases.js';
|
|
3
|
+
export function useReleaseStatus() {
|
|
4
|
+
const [latestRelease, setLatestRelease] = React.useState<{ tag: string; url: string } | null>(
|
|
5
|
+
null,
|
|
6
|
+
);
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
let active = true;
|
|
9
|
+
void checkLatestRelease().then((result) => {
|
|
10
|
+
if (active) setLatestRelease(result.release);
|
|
11
|
+
});
|
|
12
|
+
return () => {
|
|
13
|
+
active = false;
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
return { latestRelease, updateAvailable: hasNewerRelease(latestRelease) };
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './settingsStorage.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { normalizeSettings } from '../../core/settings.js';
|
|
2
|
+
export const SETTINGS_STORAGE_KEY = 'dsh-live-voice.settings';
|
|
3
|
+
export function readSettings(storage: Pick<Storage, 'getItem'> = localStorage) {
|
|
4
|
+
try {
|
|
5
|
+
return normalizeSettings(JSON.parse(storage.getItem(SETTINGS_STORAGE_KEY) || '{}'));
|
|
6
|
+
} catch {
|
|
7
|
+
return normalizeSettings({});
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function writeSettings(settings: unknown, storage: Pick<Storage, 'setItem'> = localStorage) {
|
|
11
|
+
const normalized = normalizeSettings(settings);
|
|
12
|
+
storage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(normalized));
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../../app/client/i18n/index.js';
|
|
3
|
+
import { SelectField } from '../../../../shared/design-system/index.js';
|
|
4
|
+
const DELAYS = [1, 2, 3, 4, 5, 6, 8, 10];
|
|
5
|
+
export function ConversationDelaySettings({ settings, updateSettings }: any) {
|
|
6
|
+
const { scoped: commons } = useLanguage((ctx) => ctx.commons);
|
|
7
|
+
const { scoped: speak } = useLanguage((ctx) => ctx.speak);
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<SelectField
|
|
11
|
+
label={(speak as any).responseDelay.label()}
|
|
12
|
+
value={String(settings.assistantSpeechDelaySeconds ?? 3)}
|
|
13
|
+
options={DELAYS.map((seconds) => ({
|
|
14
|
+
value: String(seconds),
|
|
15
|
+
label: (commons as any).seconds({ seconds }),
|
|
16
|
+
}))}
|
|
17
|
+
onChange={(event) =>
|
|
18
|
+
updateSettings({ assistantSpeechDelaySeconds: Number(event.target.value) })
|
|
19
|
+
}
|
|
20
|
+
/>
|
|
21
|
+
<p className="dlv-setting-description">{(speak as any).responseDelay.help()}</p>
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SettingsCard } from '../../../../shared/design-system/index.js';
|
|
3
|
+
import { DeliverySettings } from './DeliverySettings.js';
|
|
4
|
+
import { HoldToTalkSettings } from './HoldToTalkSettings.js';
|
|
5
|
+
import { VoiceModeSettings } from './VoiceModeSettings.js';
|
|
6
|
+
import { ConversationDelaySettings } from './ConversationDelaySettings.js';
|
|
7
|
+
import { PlaybackPolicySettings } from '../speak/PlaybackPolicySettings.js';
|
|
8
|
+
export function ConversationSettingsSection(props: {
|
|
9
|
+
settings: any;
|
|
10
|
+
updateSettings(value: any): void;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<SettingsCard>
|
|
14
|
+
<div className="dlv-settings-card-body">
|
|
15
|
+
<DeliverySettings {...props} />
|
|
16
|
+
<HoldToTalkSettings {...props} />
|
|
17
|
+
<VoiceModeSettings {...props} />
|
|
18
|
+
<PlaybackPolicySettings {...props} />
|
|
19
|
+
<ConversationDelaySettings {...props} />
|
|
20
|
+
</div>
|
|
21
|
+
</SettingsCard>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../../app/client/i18n/index.js';
|
|
3
|
+
import { SelectField } from '../../../../shared/design-system/index.js';
|
|
4
|
+
export function DeliverySettings({
|
|
5
|
+
settings,
|
|
6
|
+
updateSettings,
|
|
7
|
+
}: {
|
|
8
|
+
settings: any;
|
|
9
|
+
updateSettings(value: any): void;
|
|
10
|
+
}) {
|
|
11
|
+
const { scoped } = useLanguage((ctx) => ctx.settings);
|
|
12
|
+
const mode = settings.sendingMode || 'manual';
|
|
13
|
+
const seconds = [2, 3, 4, 5, 6, 8, 10];
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<SelectField
|
|
17
|
+
label={(scoped as any).delivery.label()}
|
|
18
|
+
value={mode}
|
|
19
|
+
options={[
|
|
20
|
+
{ value: 'manual', label: (scoped as any).delivery.manualLabel() },
|
|
21
|
+
{ value: 'queue', label: (scoped as any).delivery.queueLabel() },
|
|
22
|
+
{ value: 'steer', label: (scoped as any).delivery.steerLabel() },
|
|
23
|
+
]}
|
|
24
|
+
onChange={(event) => updateSettings({ sendingMode: event.target.value })}
|
|
25
|
+
/>
|
|
26
|
+
{mode !== 'manual' ? (
|
|
27
|
+
<SelectField
|
|
28
|
+
label={(scoped as any).autoSend.delay()}
|
|
29
|
+
value={String(settings.autoSendDelaySeconds || 4)}
|
|
30
|
+
options={seconds.map((value) => ({ value: String(value), label: String(value) }))}
|
|
31
|
+
onChange={(event) => updateSettings({ autoSendDelaySeconds: Number(event.target.value) })}
|
|
32
|
+
/>
|
|
33
|
+
) : (
|
|
34
|
+
<p className="dlv-setting-description">{(scoped as any).delivery.manualLabel()}</p>
|
|
35
|
+
)}
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../../app/client/i18n/index.js';
|
|
3
|
+
import { CheckboxField } from '../../../../shared/design-system/index.js';
|
|
4
|
+
export function HoldToTalkSettings({
|
|
5
|
+
settings,
|
|
6
|
+
updateSettings,
|
|
7
|
+
}: {
|
|
8
|
+
settings: any;
|
|
9
|
+
updateSettings(value: any): void;
|
|
10
|
+
}) {
|
|
11
|
+
const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
|
|
12
|
+
return (
|
|
13
|
+
<CheckboxField
|
|
14
|
+
label={(recognition as any).holdToTalk.enabled()}
|
|
15
|
+
description={(recognition as any).holdToTalk.help()}
|
|
16
|
+
checked={settings.holdToTalkEnabled !== false}
|
|
17
|
+
onChange={(event) => updateSettings({ holdToTalkEnabled: event.target.checked })}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../../app/client/i18n/index.js';
|
|
3
|
+
import { SelectField } from '../../../../shared/design-system/index.js';
|
|
4
|
+
export function VoiceModeSettings({
|
|
5
|
+
settings,
|
|
6
|
+
updateSettings,
|
|
7
|
+
}: {
|
|
8
|
+
settings: any;
|
|
9
|
+
updateSettings(value: any): void;
|
|
10
|
+
}) {
|
|
11
|
+
const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
|
|
12
|
+
const mode = settings.mode || 'speaker';
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<SelectField
|
|
16
|
+
label={(recognition as any).mode.label()}
|
|
17
|
+
value={mode}
|
|
18
|
+
options={[
|
|
19
|
+
{ value: 'speaker', label: (recognition as any).speakerMode.label() },
|
|
20
|
+
{ value: 'headphones', label: (recognition as any).headphoneMode.label() },
|
|
21
|
+
]}
|
|
22
|
+
onChange={(event) => updateSettings({ mode: event.target.value })}
|
|
23
|
+
/>
|
|
24
|
+
<p className="dlv-setting-description">
|
|
25
|
+
{mode === 'headphones'
|
|
26
|
+
? (recognition as any).headphoneMode.help()
|
|
27
|
+
: (recognition as any).speakerMode.help()}
|
|
28
|
+
</p>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../../app/client/i18n/index.js';
|
|
3
|
+
import { QwenSettings } from '../../../core/qwen/QwenSettings.js';
|
|
4
|
+
import { WhisperSettings } from '../../../recognition/engines/whisper/WhisperSettings.js';
|
|
5
|
+
import {
|
|
6
|
+
CheckboxField,
|
|
7
|
+
SelectField,
|
|
8
|
+
SettingsSubcard,
|
|
9
|
+
} from '../../../../shared/design-system/index.js';
|
|
10
|
+
import { createDeviceOptions, useAudioDevices } from '../../hooks/useAudioDevices.js';
|
|
11
|
+
export function RecognitionEngineSettings({
|
|
12
|
+
controller,
|
|
13
|
+
settings,
|
|
14
|
+
capabilities = {},
|
|
15
|
+
updateSettings,
|
|
16
|
+
}: any) {
|
|
17
|
+
const { scoped: commons } = useLanguage((ctx) => ctx.commons);
|
|
18
|
+
const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
|
|
19
|
+
const devices = useAudioDevices();
|
|
20
|
+
const engine = settings.recognitionEngine || 'browser';
|
|
21
|
+
const setEngine = (next: string) =>
|
|
22
|
+
updateSettings(
|
|
23
|
+
next === 'browser' && settings.recognitionLang === 'auto'
|
|
24
|
+
? { recognitionEngine: next, recognitionLang: 'pt-BR' }
|
|
25
|
+
: { recognitionEngine: next },
|
|
26
|
+
);
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<SelectField
|
|
30
|
+
label={(recognition as any).engine.label()}
|
|
31
|
+
value={engine}
|
|
32
|
+
options={[
|
|
33
|
+
{
|
|
34
|
+
value: 'browser',
|
|
35
|
+
label: (recognition as any).browser.label(),
|
|
36
|
+
disabled: capabilities.recognition?.supported === false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: 'qwen-http',
|
|
40
|
+
label: (recognition as any).qwen.label(),
|
|
41
|
+
disabled: capabilities['qwen-http-recognition']?.supported === false,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
value: 'whisper-http',
|
|
45
|
+
label: (recognition as any).whisper.label(),
|
|
46
|
+
disabled: capabilities['whisper-http']?.supported === false,
|
|
47
|
+
},
|
|
48
|
+
]}
|
|
49
|
+
onChange={(event) => setEngine(event.target.value)}
|
|
50
|
+
/>
|
|
51
|
+
<SelectField
|
|
52
|
+
label={(recognition as any).microphone.device()}
|
|
53
|
+
value={settings.inputDeviceId || ''}
|
|
54
|
+
options={createDeviceOptions(
|
|
55
|
+
devices,
|
|
56
|
+
'audioinput',
|
|
57
|
+
(commons as any).systemDefault(),
|
|
58
|
+
(commons as any).device.numberedLabel({ number: '' }),
|
|
59
|
+
)}
|
|
60
|
+
onChange={(event) => updateSettings({ inputDeviceId: event.target.value })}
|
|
61
|
+
/>
|
|
62
|
+
<SelectField
|
|
63
|
+
label={(recognition as any).language.label()}
|
|
64
|
+
value={settings.recognitionLang || (engine === 'browser' ? 'pt-BR' : 'auto')}
|
|
65
|
+
options={[
|
|
66
|
+
...(engine === 'browser'
|
|
67
|
+
? []
|
|
68
|
+
: [{ value: 'auto', label: (recognition as any).language.automatic() }]),
|
|
69
|
+
{ value: 'pt-BR', label: 'Português (Brasil)' },
|
|
70
|
+
{ value: 'en-US', label: 'English (US)' },
|
|
71
|
+
]}
|
|
72
|
+
onChange={(event) => updateSettings({ recognitionLang: event.target.value })}
|
|
73
|
+
/>
|
|
74
|
+
{engine === 'browser' && (
|
|
75
|
+
<>
|
|
76
|
+
<p>{(recognition as any).browser.help()}</p>
|
|
77
|
+
<CheckboxField
|
|
78
|
+
label={(recognition as any).browser.localProcessing()}
|
|
79
|
+
checked={settings.recognitionProcessLocally !== false}
|
|
80
|
+
onChange={(event) =>
|
|
81
|
+
updateSettings({ recognitionProcessLocally: event.target.checked })
|
|
82
|
+
}
|
|
83
|
+
/>
|
|
84
|
+
{settings.recognitionProcessLocally !== false ? (
|
|
85
|
+
<CheckboxField
|
|
86
|
+
label={(recognition as any).browser.autoInstallPack()}
|
|
87
|
+
checked={settings.recognitionAutoInstall !== false}
|
|
88
|
+
onChange={(event) => updateSettings({ recognitionAutoInstall: event.target.checked })}
|
|
89
|
+
/>
|
|
90
|
+
) : (
|
|
91
|
+
<p role="status">{(recognition as any).browser.remoteServiceWarning()}</p>
|
|
92
|
+
)}
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
{engine === 'whisper-http' && (
|
|
96
|
+
<SettingsSubcard title={(commons as any).connection.title()}>
|
|
97
|
+
<p>{(recognition as any).whisper.captureHelp()}</p>
|
|
98
|
+
<WhisperSettings controller={controller} />
|
|
99
|
+
</SettingsSubcard>
|
|
100
|
+
)}
|
|
101
|
+
{engine === 'qwen-http' && (
|
|
102
|
+
<SettingsSubcard title={(commons as any).connection.title()}>
|
|
103
|
+
<p>{(recognition as any).qwen.captureHelp()}</p>
|
|
104
|
+
{settings.engine !== 'qwen-http' && <QwenSettings controller={controller} />}
|
|
105
|
+
</SettingsSubcard>
|
|
106
|
+
)}
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../../app/client/i18n/index.js';
|
|
3
|
+
import {
|
|
4
|
+
CheckboxField,
|
|
5
|
+
NumberField,
|
|
6
|
+
SettingsSubcard,
|
|
7
|
+
} from '../../../../shared/design-system/index.js';
|
|
8
|
+
export function RecognitionFilterSettings({ settings, updateSettings }: any) {
|
|
9
|
+
const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
|
|
10
|
+
const enabled = settings.recognitionFilterEnabled !== false;
|
|
11
|
+
return (
|
|
12
|
+
<SettingsSubcard title={(recognition as any).minimumWords.label()}>
|
|
13
|
+
<CheckboxField
|
|
14
|
+
label={(recognition as any).minimumWords.enabled()}
|
|
15
|
+
description={(recognition as any).minimumWords.help()}
|
|
16
|
+
checked={enabled}
|
|
17
|
+
onChange={(event) => updateSettings({ recognitionFilterEnabled: event.target.checked })}
|
|
18
|
+
/>
|
|
19
|
+
<NumberField
|
|
20
|
+
label={(recognition as any).minimumWords.label()}
|
|
21
|
+
min={1}
|
|
22
|
+
max={20}
|
|
23
|
+
value={settings.recognitionMinimumWords ?? 2}
|
|
24
|
+
disabled={!enabled}
|
|
25
|
+
onChange={(event) => {
|
|
26
|
+
const value = Number(event.target.value);
|
|
27
|
+
if (Number.isInteger(value) && value >= 1 && value <= 20)
|
|
28
|
+
updateSettings({ recognitionMinimumWords: value });
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
</SettingsSubcard>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SettingsCard } from '../../../../shared/design-system/index.js';
|
|
3
|
+
import { RecognitionEngineSettings } from './RecognitionEngineSettings.js';
|
|
4
|
+
import { RecognitionFilterSettings } from './RecognitionFilterSettings.js';
|
|
5
|
+
import { SilenceDetectionSettings } from './SilenceDetectionSettings.js';
|
|
6
|
+
import { VoiceCommandSettings } from './VoiceCommandSettings.js';
|
|
7
|
+
import { RecognitionStatus } from './RecognitionStatus.js';
|
|
8
|
+
export function RecognitionSettingsSection(props: any) {
|
|
9
|
+
return (
|
|
10
|
+
<SettingsCard>
|
|
11
|
+
<div className="dlv-settings-card-body">
|
|
12
|
+
<RecognitionEngineSettings {...props} />
|
|
13
|
+
<VoiceCommandSettings {...props} />
|
|
14
|
+
<RecognitionFilterSettings {...props} />
|
|
15
|
+
<SilenceDetectionSettings {...props} />
|
|
16
|
+
<RecognitionStatus {...props} />
|
|
17
|
+
</div>
|
|
18
|
+
</SettingsCard>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLanguage } from '../../../../app/client/i18n/index.js';
|
|
3
|
+
export function RecognitionStatus({ capabilities = {} }: any) {
|
|
4
|
+
const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
{capabilities.capture?.supported === false ? (
|
|
8
|
+
<p role="status">
|
|
9
|
+
{(recognition as any).microphone.failure({ reason: capabilities.capture.reason || '' })}
|
|
10
|
+
</p>
|
|
11
|
+
) : capabilities.capture?.permission === 'prompt' ? (
|
|
12
|
+
<p role="status">{(recognition as any).microphone.permissionHelp()}</p>
|
|
13
|
+
) : null}
|
|
14
|
+
{capabilities.recognition?.supported === false && (
|
|
15
|
+
<p role="status">{capabilities.recognition.reason}</p>
|
|
16
|
+
)}
|
|
17
|
+
</>
|
|
18
|
+
);
|
|
19
|
+
}
|