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.
Files changed (138) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/DEVELOPMENT.md +13 -0
  3. package/LICENSE +201 -674
  4. package/PLAN.md +28 -0
  5. package/README.md +65 -110
  6. package/docs/ARCHITECTURE.md +40 -0
  7. package/docs/CHOOSING-AN-ENGINE.md +120 -0
  8. package/docs/CONFIGURATION.md +149 -0
  9. package/docs/REVIEW.md +35 -0
  10. package/docs/VOICE-LIFECYCLE.md +59 -0
  11. package/lib/client.js +3652 -1576
  12. package/lib/server.js +641 -453
  13. package/package.json +17 -6
  14. package/scripts/build.ts +16 -4
  15. package/scripts/preview-ui.ts +1 -1
  16. package/src/{client/index.ts → app/client/apply.tsx} +152 -59
  17. package/src/app/client/i18n/DshLanguageBoundary.tsx +42 -0
  18. package/src/app/client/i18n/catalogs/base.ts +231 -0
  19. package/src/app/client/i18n/catalogs/en.ts +258 -0
  20. package/src/app/client/i18n/catalogs/es.ts +279 -0
  21. package/src/app/client/i18n/catalogs/fr.ts +281 -0
  22. package/src/app/client/i18n/catalogs/hi.ts +263 -0
  23. package/src/app/client/i18n/catalogs/index.ts +18 -0
  24. package/src/app/client/i18n/catalogs/pt-BR.ts +270 -0
  25. package/src/app/client/i18n/catalogs/zh.ts +248 -0
  26. package/src/app/client/i18n/index.ts +4 -0
  27. package/src/app/client/i18n/registerDshLocales.ts +43 -0
  28. package/src/app/client/i18n/runtime.tsx +58 -0
  29. package/src/app/client/index.ts +3 -0
  30. package/src/app/client/registerSlots.tsx +30 -0
  31. package/src/app/client/slotDefinitions.ts +32 -0
  32. package/src/app/server/apply.ts +479 -0
  33. package/src/app/server/index.ts +2 -0
  34. package/src/app/server/registerRoutes.ts +11 -0
  35. package/src/modules/conversation/components/ConversationControls.tsx +40 -0
  36. package/src/modules/conversation/components/ConversationStatusBar.tsx +155 -0
  37. package/src/modules/conversation/components/DeliveryModeButton.tsx +43 -0
  38. package/src/modules/conversation/components/MicrophoneButton.tsx +6 -0
  39. package/src/modules/conversation/components/PlaybackControls.tsx +41 -0
  40. package/src/modules/conversation/components/SpeakButton.tsx +30 -0
  41. package/src/modules/conversation/components/Waveform.tsx +68 -0
  42. package/src/modules/conversation/components/conversationStatus.ts +17 -0
  43. package/src/modules/conversation/components/createConversationComponents.tsx +505 -0
  44. package/src/modules/conversation/components/index.ts +8 -0
  45. package/src/modules/conversation/hooks/index.ts +2 -0
  46. package/src/modules/conversation/hooks/useConversationActions.ts +27 -0
  47. package/src/modules/conversation/hooks/useConversationController.ts +15 -0
  48. package/src/modules/conversation/index.ts +3 -0
  49. package/src/{client → modules/conversation/models}/chat.ts +2 -1
  50. package/src/modules/conversation/models/index.ts +1 -0
  51. package/src/{core → modules/core}/coordinator.ts +133 -7
  52. package/src/modules/core/index.ts +6 -0
  53. package/src/{engines/qwen-http-host.ts → modules/core/qwen/QwenHttpHost.ts} +2 -2
  54. package/src/modules/core/qwen/QwenSettings.tsx +148 -0
  55. package/src/{core → modules/core}/settings.ts +23 -0
  56. package/src/modules/recognition/components/RecognitionCapabilityStatus.tsx +19 -0
  57. package/src/{engines/recognition/qwen-http.ts → modules/recognition/engines/qwen/QwenRecognitionEngine.ts} +1 -1
  58. package/src/{engines/recognition/whisper-http.ts → modules/recognition/engines/whisper/WhisperRecognitionEngine.ts} +1 -1
  59. package/src/modules/recognition/engines/whisper/WhisperSettings.tsx +150 -0
  60. package/src/modules/recognition/index.ts +4 -0
  61. package/src/modules/recognition/qwen/QwenRecognitionSettings.tsx +4 -0
  62. package/src/modules/settings/components/LiveVoiceSettings.tsx +72 -0
  63. package/src/modules/settings/components/SettingsHeader.tsx +49 -0
  64. package/src/modules/settings/components/VersionBadges.tsx +39 -0
  65. package/src/modules/settings/components/createLiveVoiceSettings.tsx +8 -0
  66. package/src/modules/settings/components/index.ts +4 -0
  67. package/src/modules/settings/hooks/index.ts +3 -0
  68. package/src/modules/settings/hooks/useAudioDevices.ts +38 -0
  69. package/src/modules/settings/hooks/useLiveVoiceSettings.ts +4 -0
  70. package/src/modules/settings/hooks/useReleaseStatus.ts +17 -0
  71. package/src/modules/settings/index.ts +4 -0
  72. package/src/modules/settings/models/index.ts +1 -0
  73. package/src/modules/settings/models/settingsStorage.ts +14 -0
  74. package/src/modules/settings/sections/conversation/ConversationDelaySettings.tsx +24 -0
  75. package/src/modules/settings/sections/conversation/ConversationSettingsSection.tsx +23 -0
  76. package/src/modules/settings/sections/conversation/DeliverySettings.tsx +38 -0
  77. package/src/modules/settings/sections/conversation/HoldToTalkSettings.tsx +20 -0
  78. package/src/modules/settings/sections/conversation/VoiceModeSettings.tsx +31 -0
  79. package/src/modules/settings/sections/conversation/index.ts +5 -0
  80. package/src/modules/settings/sections/index.ts +3 -0
  81. package/src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx +109 -0
  82. package/src/modules/settings/sections/recognition/RecognitionFilterSettings.tsx +33 -0
  83. package/src/modules/settings/sections/recognition/RecognitionSettingsSection.tsx +20 -0
  84. package/src/modules/settings/sections/recognition/RecognitionStatus.tsx +19 -0
  85. package/src/modules/settings/sections/recognition/SilenceDetectionSettings.tsx +58 -0
  86. package/src/modules/settings/sections/recognition/VoiceCommandSettings.tsx +41 -0
  87. package/src/modules/settings/sections/recognition/index.ts +5 -0
  88. package/src/modules/settings/sections/speak/OutputFilterSettings.tsx +39 -0
  89. package/src/modules/settings/sections/speak/PlaybackPolicySettings.tsx +26 -0
  90. package/src/modules/settings/sections/speak/SpeakSettingsSection.tsx +16 -0
  91. package/src/modules/settings/sections/speak/SpeechAdvancedSettings.tsx +81 -0
  92. package/src/modules/settings/sections/speak/SpeechEngineSettings.tsx +80 -0
  93. package/src/modules/settings/sections/speak/index.ts +4 -0
  94. package/src/modules/settings/services/releases.ts +117 -0
  95. package/src/modules/speak/engines/audio/HostAudioEngine.ts +167 -0
  96. package/src/modules/speak/engines/audio/M4aAacTranscoder.ts +58 -0
  97. package/src/modules/speak/engines/qwen/QwenSpeakingEngine.ts +14 -0
  98. package/src/{engines/speaking/say.ts → modules/speak/engines/say/SaySpeakingEngine.ts} +26 -4
  99. package/src/modules/speak/index.ts +5 -0
  100. package/src/modules/speak/qwen/QwenSpeakingSettings.tsx +4 -0
  101. package/src/modules/speak/services/speechQueue.ts +2 -0
  102. package/src/server.ts +9 -365
  103. package/src/shared/design-system/buttons/IconButton.tsx +33 -0
  104. package/src/shared/design-system/buttons/PillButton.tsx +11 -0
  105. package/src/shared/design-system/buttons/ToggleButton.tsx +6 -0
  106. package/src/shared/design-system/buttons/index.ts +3 -0
  107. package/src/shared/design-system/feedback/ErrorMessage.tsx +27 -0
  108. package/src/shared/design-system/feedback/StatusBadge.tsx +9 -0
  109. package/src/shared/design-system/feedback/StatusMessage.tsx +4 -0
  110. package/src/shared/design-system/feedback/index.ts +3 -0
  111. package/src/shared/design-system/forms/CheckboxField.tsx +20 -0
  112. package/src/shared/design-system/forms/NumberField.tsx +12 -0
  113. package/src/shared/design-system/forms/SelectField.tsx +20 -0
  114. package/src/shared/design-system/forms/TextAreaField.tsx +12 -0
  115. package/src/shared/design-system/forms/TextField.tsx +12 -0
  116. package/src/shared/design-system/forms/index.ts +5 -0
  117. package/src/shared/design-system/icons/Icon.tsx +20 -0
  118. package/src/shared/design-system/icons/icons.ts +16 -0
  119. package/src/shared/design-system/icons/index.ts +2 -0
  120. package/src/shared/design-system/index.ts +5 -0
  121. package/src/shared/design-system/layout/SettingsCard.tsx +9 -0
  122. package/src/shared/design-system/layout/SettingsSection.tsx +9 -0
  123. package/src/shared/design-system/layout/SettingsSubcard.tsx +14 -0
  124. package/src/shared/design-system/layout/SettingsTabs.tsx +54 -0
  125. package/src/shared/design-system/layout/index.ts +4 -0
  126. package/src/{client/styles.ts → styles/index.ts} +3 -1
  127. package/src/client/components.ts +0 -1080
  128. package/src/client/qwen-settings.ts +0 -144
  129. package/src/client/whisper-settings.ts +0 -147
  130. package/src/engines/speaking/qwen-http.ts +0 -124
  131. /package/src/{core → modules/core}/filters.ts +0 -0
  132. /package/src/{core → modules/core}/microphone.ts +0 -0
  133. /package/src/{core → modules/core}/ownership.ts +0 -0
  134. /package/src/{core → modules/core}/transcript.ts +0 -0
  135. /package/src/{engines/recognition/browser.ts → modules/recognition/engines/browser/BrowserRecognitionEngine.ts} +0 -0
  136. /package/src/{engines/recognition/whisper-http-host.ts → modules/recognition/engines/whisper/whisperRecognitionHost.ts} +0 -0
  137. /package/src/{engines/speaking/browser.ts → modules/speak/engines/browser/BrowserSpeakingEngine.ts} +0 -0
  138. /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,4 @@
1
+ export * from './createLiveVoiceSettings.js';
2
+ export * from './LiveVoiceSettings.js';
3
+ export * from './SettingsHeader.js';
4
+ export * from './VersionBadges.js';
@@ -0,0 +1,3 @@
1
+ export * from './useAudioDevices.js';
2
+ export * from './useLiveVoiceSettings.js';
3
+ export * from './useReleaseStatus.js';
@@ -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,4 @@
1
+ export {
2
+ useConversationActions as useLiveVoiceSettingsActions,
3
+ useConversationController as useLiveVoiceSettings,
4
+ } from '../../conversation/hooks/index.js';
@@ -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,4 @@
1
+ export * from './components/index.js';
2
+ export * from './hooks/index.js';
3
+ export * from './models/index.js';
4
+ export * from './sections/index.js';
@@ -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,5 @@
1
+ export * from './ConversationSettingsSection.js';
2
+ export * from './ConversationDelaySettings.js';
3
+ export * from './DeliverySettings.js';
4
+ export * from './HoldToTalkSettings.js';
5
+ export * from './VoiceModeSettings.js';
@@ -0,0 +1,3 @@
1
+ export * from './conversation/index.js';
2
+ export * from './recognition/index.js';
3
+ export * from './speak/index.js';
@@ -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
+ }