@goodandready/dsh-voice 0.9.16 → 0.9.18
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/lib/client.js +547 -394
- package/lib/host-routes.js +186 -0
- package/lib/index.js +75 -457
- package/lib/local-daemon.js +272 -0
- package/lib/schema.js +1 -1
- package/lib/sensevoice-installer.js +4 -2
- package/lib/updater.js +17 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -568,6 +568,7 @@ window.__ModuleLoader__.load({
|
|
|
568
568
|
window.addEventListener('beforeunload', onPageUnload)
|
|
569
569
|
|
|
570
570
|
return () => {
|
|
571
|
+
voice.disposed = true
|
|
571
572
|
window.removeEventListener('dsh:tts:start', onTtsStart)
|
|
572
573
|
window.removeEventListener('dsh:tts:stop', onTtsStop)
|
|
573
574
|
window.removeEventListener('dsh-voice:settings-saved', onSettingsSaved)
|
|
@@ -734,51 +735,93 @@ window.__ModuleLoader__.load({
|
|
|
734
735
|
|
|
735
736
|
async function sendAudio(blob, mime, mode) {
|
|
736
737
|
const activeChain = (voice._activeChain || [])
|
|
737
|
-
|
|
738
|
-
|
|
738
|
+
const modelMap = voice._activeModelMap || {}
|
|
739
|
+
const chosenModel = modelMap['browser-webgpu'] || (voice.settings && voice.settings.webgpuModel)
|
|
740
|
+
|
|
741
|
+
const tryHost = async (chainOverride) => {
|
|
742
|
+
const dataBase64 = await blobToBase64(blob)
|
|
743
|
+
const payload = { dataBase64, mimeType: mime, mode }
|
|
744
|
+
if (Array.isArray(chainOverride)) {
|
|
745
|
+
payload.chain = chainOverride.map((p) => (typeof p === 'string' ? { provider: p, model: modelMap[p] } : p))
|
|
746
|
+
}
|
|
747
|
+
const contextWords = extractContextKeywords()
|
|
748
|
+
if (contextWords && contextWords.length > 0) payload.contextWords = contextWords
|
|
749
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null
|
|
750
|
+
const timer = controller ? setTimeout(() => controller.abort(), 60000) : null
|
|
751
|
+
let res = null
|
|
752
|
+
try {
|
|
753
|
+
res = await fetch('/dsh-voice/transcribe', {
|
|
754
|
+
method: 'POST',
|
|
755
|
+
headers: { 'content-type': 'application/json' },
|
|
756
|
+
body: JSON.stringify(payload),
|
|
757
|
+
signal: controller ? controller.signal : undefined,
|
|
758
|
+
})
|
|
759
|
+
} catch (e) {
|
|
760
|
+
if (e && e.name === 'AbortError') {
|
|
761
|
+
throw new Error('Transcription request timed out (60s)')
|
|
762
|
+
}
|
|
763
|
+
throw e
|
|
764
|
+
} finally {
|
|
765
|
+
if (timer) clearTimeout(timer)
|
|
766
|
+
}
|
|
767
|
+
let parsed = null
|
|
768
|
+
try { parsed = await res.json() } catch (e) { /* not json */ }
|
|
769
|
+
if (!res.ok || !parsed || !parsed.ok) {
|
|
770
|
+
throw new Error((parsed && parsed.error && parsed.error.message) || `HTTP ${res.status}`)
|
|
771
|
+
}
|
|
772
|
+
if (parsed.command) return { command: parsed.command, text: '', action: null }
|
|
773
|
+
return { text: String(parsed.text || '').trim(), command: null, action: parsed.action || null }
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const tryWebGpu = async () => {
|
|
777
|
+
const canGpu = voice.webGpu?.isSupported ? voice.webGpu.isSupported() : (typeof isWebGpuSupported === 'function' && isWebGpuSupported())
|
|
778
|
+
if (canGpu) {
|
|
739
779
|
try {
|
|
740
|
-
const
|
|
780
|
+
const fn = voice.webGpu?.transcribe || transcribeWithWebGpu
|
|
781
|
+
const out = await fn(blob, mime, {
|
|
741
782
|
language: voice._activeLang || (voice.settings && voice.settings.dictation && voice.settings.dictation.language) || '',
|
|
742
|
-
model:
|
|
783
|
+
model: chosenModel,
|
|
743
784
|
})
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
785
|
+
const text = (out && typeof out.text === 'string') ? tidyPhrase(out.text) : ''
|
|
786
|
+
return { text, command: null, action: null, provider: 'browser-webgpu' }
|
|
747
787
|
} catch (webGpuErr) {
|
|
748
|
-
|
|
749
|
-
console.warn('[dsh-voice] WebGPU Whisper failed, falling back to server chain:', webGpuErr)
|
|
788
|
+
console.warn('[dsh-voice] WebGPU Whisper failed:', webGpuErr)
|
|
750
789
|
}
|
|
751
790
|
}
|
|
791
|
+
return null
|
|
752
792
|
}
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
if (e && e.name === 'AbortError') {
|
|
769
|
-
throw new Error('Transcription request timed out (60s)')
|
|
793
|
+
|
|
794
|
+
const gpuIdx = activeChain.indexOf('browser-webgpu')
|
|
795
|
+
if (gpuIdx === -1) {
|
|
796
|
+
return await tryHost()
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const head = activeChain.slice(0, gpuIdx)
|
|
800
|
+
const tail = activeChain.slice(gpuIdx + 1)
|
|
801
|
+
|
|
802
|
+
if (head.length > 0) {
|
|
803
|
+
try {
|
|
804
|
+
const headOut = await tryHost(head)
|
|
805
|
+
if (headOut && (headOut.text || headOut.command || headOut.action)) return headOut
|
|
806
|
+
} catch (headErr) {
|
|
807
|
+
// Continue to WebGPU
|
|
770
808
|
}
|
|
771
|
-
throw e
|
|
772
|
-
} finally {
|
|
773
|
-
if (timer) clearTimeout(timer)
|
|
774
809
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
810
|
+
|
|
811
|
+
const gpuOut = await tryWebGpu()
|
|
812
|
+
if (gpuOut && gpuOut.text) return gpuOut
|
|
813
|
+
|
|
814
|
+
if (tail.length > 0) {
|
|
815
|
+
try {
|
|
816
|
+
const tailOut = await tryHost(tail)
|
|
817
|
+
if (tailOut && (tailOut.text || tailOut.command || tailOut.action)) return tailOut
|
|
818
|
+
} catch (tailErr) {
|
|
819
|
+
if (head.length === 0 && !gpuOut) throw tailErr
|
|
820
|
+
}
|
|
779
821
|
}
|
|
780
|
-
|
|
781
|
-
|
|
822
|
+
|
|
823
|
+
if (gpuOut) return gpuOut
|
|
824
|
+
return { text: '', command: null, action: null }
|
|
782
825
|
}
|
|
783
826
|
|
|
784
827
|
function tidyPhrase(text) {
|
|
@@ -836,8 +879,20 @@ window.__ModuleLoader__.load({
|
|
|
836
879
|
return t('undone')
|
|
837
880
|
}
|
|
838
881
|
|
|
839
|
-
function insertDraftText(text) {
|
|
840
|
-
|
|
882
|
+
function insertDraftText(text, target) {
|
|
883
|
+
if (voice.disposed) return false
|
|
884
|
+
const span = target && target.span
|
|
885
|
+
if (span) {
|
|
886
|
+
if (typeof span.insert === 'function') {
|
|
887
|
+
try { span.insert(text); return true } catch (_) {}
|
|
888
|
+
} else if (typeof span.insertText === 'function') {
|
|
889
|
+
try { span.insertText(text); return true } catch (_) {}
|
|
890
|
+
} else if (typeof span === 'function') {
|
|
891
|
+
try { span(text); return true } catch (_) {}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const actions = (target && target.actions) || voice.inputActions
|
|
895
|
+
const input = (target && target.input) || voice.input
|
|
841
896
|
if (!actions || (typeof actions.setDraft !== 'function' && typeof actions.insertText !== 'function')) {
|
|
842
897
|
voice.set({ phase: 'error', error: t('composerUnavailable') })
|
|
843
898
|
return false
|
|
@@ -845,7 +900,7 @@ window.__ModuleLoader__.load({
|
|
|
845
900
|
if (typeof actions.insertText === 'function') {
|
|
846
901
|
try { actions.insertText(text); return true } catch (_) {}
|
|
847
902
|
}
|
|
848
|
-
const draft =
|
|
903
|
+
const draft = input && typeof input.draft === 'string' ? input.draft : ''
|
|
849
904
|
const before = draft
|
|
850
905
|
const sep = (!draft || draft.endsWith('\n') || text.startsWith('\n')) ? '' : ' '
|
|
851
906
|
const after = draft + sep + text
|
|
@@ -858,100 +913,13 @@ window.__ModuleLoader__.load({
|
|
|
858
913
|
return true
|
|
859
914
|
}
|
|
860
915
|
|
|
861
|
-
function appendDraft(text) {
|
|
862
|
-
if (!text) return
|
|
916
|
+
function appendDraft(text, target) {
|
|
917
|
+
if (voice.disposed || !text) return
|
|
863
918
|
let clean = voice.settings.voiceCommands ? applyVoiceCommands(text) : tidyPhrase(text)
|
|
864
919
|
if (!clean) return
|
|
865
|
-
insertDraftText(clean)
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// Human-readable key name.
|
|
869
|
-
const KEY_LABELS = {
|
|
870
|
-
Control: 'Ctrl', Alt: 'Alt', Shift: 'Shift', Meta: 'Win',
|
|
871
|
-
Escape: 'Esc',
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
function keyLabel(name) {
|
|
875
|
-
if (!name) return t('keyUnset')
|
|
876
|
-
if (typeof name === 'string' && name.includes('+')) {
|
|
877
|
-
return name.split('+').map((p) => keyLabel(p)).join(' + ')
|
|
878
|
-
}
|
|
879
|
-
if (name === 'Space') return t('keySpace')
|
|
880
|
-
if (KEY_LABELS[name]) return KEY_LABELS[name]
|
|
881
|
-
// Drop the Key/Digit prefix from codes like KeyR and Digit5.
|
|
882
|
-
return String(name).replace(/^Key/, '').replace(/^Digit/, '')
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// What to store on key press. Pure modifiers are remembered by name:
|
|
886
|
-
// left and right have different codes but the user means "either".
|
|
887
|
-
// Supports combos like Control+Space or Alt+KeyV.
|
|
888
|
-
function keyFromEvent(event) {
|
|
889
|
-
if (!event) return ''
|
|
890
|
-
const modifiers = []
|
|
891
|
-
if (event.ctrlKey && event.key !== 'Control') modifiers.push('Control')
|
|
892
|
-
if (event.altKey && event.key !== 'Alt') modifiers.push('Alt')
|
|
893
|
-
if (event.shiftKey && event.key !== 'Shift') modifiers.push('Shift')
|
|
894
|
-
if (event.metaKey && event.key !== 'Meta') modifiers.push('Meta')
|
|
895
|
-
|
|
896
|
-
const main = ['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)
|
|
897
|
-
? event.key
|
|
898
|
-
: (event.code || event.key || '')
|
|
899
|
-
|
|
900
|
-
if (modifiers.length > 0 && !['Control', 'Alt', 'Shift', 'Meta'].includes(main)) {
|
|
901
|
-
return modifiers.concat(main).join('+')
|
|
902
|
-
}
|
|
903
|
-
return main
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function hotkeyMatches(event, name) {
|
|
907
|
-
if (!event || !name) return false
|
|
908
|
-
if (typeof name === 'string' && name.includes('+')) {
|
|
909
|
-
const parts = name.split('+')
|
|
910
|
-
const main = parts[parts.length - 1]
|
|
911
|
-
const needCtrl = parts.includes('Control')
|
|
912
|
-
const needAlt = parts.includes('Alt')
|
|
913
|
-
const needShift = parts.includes('Shift')
|
|
914
|
-
const needMeta = parts.includes('Meta')
|
|
915
|
-
if (needCtrl && !event.ctrlKey) return false
|
|
916
|
-
if (needAlt && !event.altKey) return false
|
|
917
|
-
if (needShift && !event.shiftKey) return false
|
|
918
|
-
if (needMeta && !event.metaKey) return false
|
|
919
|
-
return event.code === main || event.key === main
|
|
920
|
-
}
|
|
921
|
-
if (name === 'Control') return event.key === 'Control'
|
|
922
|
-
if (name === 'Alt') return event.key === 'Alt'
|
|
923
|
-
if (name === 'Shift') return event.key === 'Shift'
|
|
924
|
-
if (name === 'Meta') return event.key === 'Meta'
|
|
925
|
-
return event.code === name || event.key === name
|
|
920
|
+
insertDraftText(clean, target)
|
|
926
921
|
}
|
|
927
922
|
|
|
928
|
-
function isAllowedInEditable(name) {
|
|
929
|
-
if (!name) return false
|
|
930
|
-
if (['Control', 'Alt', 'Shift', 'Meta'].includes(name)) return true
|
|
931
|
-
if (/^F\d{1,2}$/.test(name)) return true
|
|
932
|
-
if (typeof name === 'string' && name.includes('+')) return true
|
|
933
|
-
if (['Tab', 'Pause', 'ScrollLock', 'Insert'].includes(name)) return true
|
|
934
|
-
return false
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function isHotkeyRelease(event, name) {
|
|
938
|
-
if (!event || !name) return false
|
|
939
|
-
if (typeof name === 'string' && name.includes('+')) {
|
|
940
|
-
const parts = name.split('+')
|
|
941
|
-
if (event.code === parts[parts.length - 1] || event.key === parts[parts.length - 1]) return true
|
|
942
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
943
|
-
if (event.key === parts[i] || event.code === parts[i]) return true
|
|
944
|
-
}
|
|
945
|
-
return false
|
|
946
|
-
}
|
|
947
|
-
return event.code === name || event.key === name
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Announce that the user started speaking.
|
|
951
|
-
//
|
|
952
|
-
// Playback should mute immediately: listening and talking at once is
|
|
953
|
-
// impossible. There is no plugin-to-plugin API — this broadcasts a window
|
|
954
|
-
// event that anyone may hear, so either side works alone.
|
|
955
923
|
function announceVoice(phase) {
|
|
956
924
|
try {
|
|
957
925
|
window.dispatchEvent(new CustomEvent('dsh-voice:speaking', { detail: { phase } }))
|
|
@@ -1087,35 +1055,32 @@ function isWebGpuSupported() {
|
|
|
1087
1055
|
return true
|
|
1088
1056
|
}
|
|
1089
1057
|
|
|
1090
|
-
|
|
1091
|
-
let _webGpuLoading = false
|
|
1058
|
+
const _webGpuPipelines = new Map()
|
|
1092
1059
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (
|
|
1096
|
-
|
|
1097
|
-
await new Promise((r) => setTimeout(r, 80))
|
|
1098
|
-
}
|
|
1099
|
-
if (_webGpuPipeline) return _webGpuPipeline
|
|
1060
|
+
function loadWebGpuWhisper(modelName) {
|
|
1061
|
+
const targetModel = modelName || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'
|
|
1062
|
+
if (_webGpuPipelines.has(targetModel)) {
|
|
1063
|
+
return _webGpuPipelines.get(targetModel)
|
|
1100
1064
|
}
|
|
1101
|
-
|
|
1102
|
-
try {
|
|
1065
|
+
const promise = (async () => {
|
|
1103
1066
|
if (!isWebGpuSupported()) {
|
|
1104
1067
|
throw new Error('WebGPU is not supported in this browser environment')
|
|
1105
1068
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1069
|
+
const mod = typeof globalThis !== 'undefined' && globalThis.__mockTransformers
|
|
1070
|
+
? globalThis.__mockTransformers
|
|
1071
|
+
: (await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.3.3'))
|
|
1108
1072
|
const pipeline = mod.pipeline || (mod.default && mod.default.pipeline)
|
|
1109
1073
|
if (!pipeline) throw new Error('Transformers pipeline not found in loaded bundle')
|
|
1110
|
-
|
|
1111
|
-
_webGpuPipeline = await pipeline('automatic-speech-recognition', targetModel, {
|
|
1074
|
+
return pipeline('automatic-speech-recognition', targetModel, {
|
|
1112
1075
|
device: 'webgpu',
|
|
1113
1076
|
dtype: 'fp32',
|
|
1114
1077
|
})
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1078
|
+
})().catch((err) => {
|
|
1079
|
+
_webGpuPipelines.delete(targetModel)
|
|
1080
|
+
throw err
|
|
1081
|
+
})
|
|
1082
|
+
_webGpuPipelines.set(targetModel, promise)
|
|
1083
|
+
return promise
|
|
1119
1084
|
}
|
|
1120
1085
|
|
|
1121
1086
|
async function audioBlobToFloat32Array(blob) {
|
|
@@ -1150,13 +1115,23 @@ async function transcribeWithWebGpu(blob, mime, options) {
|
|
|
1150
1115
|
voice.webGpu = {
|
|
1151
1116
|
isSupported: isWebGpuSupported,
|
|
1152
1117
|
transcribe: transcribeWithWebGpu,
|
|
1118
|
+
load: loadWebGpuWhisper,
|
|
1119
|
+
clearCache: () => { _webGpuPipelines.clear() },
|
|
1153
1120
|
}
|
|
1154
|
-
|
|
1121
|
+
voice._chainsPromise = null
|
|
1155
1122
|
function modeChain(mode) {
|
|
1156
1123
|
if (!voice._chainsPromise) {
|
|
1157
1124
|
voice._chainsPromise = fetch('/dsh-voice/status', { cache: 'no-store' })
|
|
1158
1125
|
.then((res) => res.json())
|
|
1159
|
-
.then((data) =>
|
|
1126
|
+
.then((data) => {
|
|
1127
|
+
if (data && typeof data.localOnly === 'boolean') {
|
|
1128
|
+
voice.settings.localOnly = data.localOnly
|
|
1129
|
+
}
|
|
1130
|
+
if (data && data.webgpuModel) {
|
|
1131
|
+
voice.settings.webgpuModel = data.webgpuModel
|
|
1132
|
+
}
|
|
1133
|
+
return (data && data.modes) || {}
|
|
1134
|
+
})
|
|
1160
1135
|
.catch(() => {
|
|
1161
1136
|
voice._chainsPromise = null
|
|
1162
1137
|
return {}
|
|
@@ -1164,9 +1139,21 @@ voice.webGpu = {
|
|
|
1164
1139
|
}
|
|
1165
1140
|
return voice._chainsPromise.then((modes) => {
|
|
1166
1141
|
const row = modes[mode] || {}
|
|
1142
|
+
let chain = Array.isArray(row.chain) ? row.chain : []
|
|
1143
|
+
let providers = chain.map((e) => (typeof e === 'string' ? e : e && e.provider)).filter(Boolean)
|
|
1144
|
+
if (voice.settings && voice.settings.localOnly) {
|
|
1145
|
+
providers = providers.filter((p) => p !== 'browser')
|
|
1146
|
+
}
|
|
1147
|
+
const modelMap = {}
|
|
1148
|
+
for (const entry of chain) {
|
|
1149
|
+
if (entry && entry.provider && entry.model) {
|
|
1150
|
+
modelMap[entry.provider] = entry.model
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1167
1153
|
return {
|
|
1168
|
-
chain:
|
|
1154
|
+
chain: providers,
|
|
1169
1155
|
language: row.language !== undefined ? row.language : '',
|
|
1156
|
+
modelMap,
|
|
1170
1157
|
}
|
|
1171
1158
|
})
|
|
1172
1159
|
}
|
|
@@ -1184,7 +1171,7 @@ voice.webGpu = {
|
|
|
1184
1171
|
if (!recorder || recorder.state === 'inactive') return Promise.resolve()
|
|
1185
1172
|
return new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
|
|
1186
1173
|
}
|
|
1187
|
-
async function openMic(mode) {
|
|
1174
|
+
async function openMic(mode, opId, target) {
|
|
1188
1175
|
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
1189
1176
|
throw new Error(t('micUnavailable'))
|
|
1190
1177
|
}
|
|
@@ -1197,42 +1184,54 @@ voice.webGpu = {
|
|
|
1197
1184
|
}
|
|
1198
1185
|
if (voice.settings.micDeviceId) audio.deviceId = { exact: voice.settings.micDeviceId }
|
|
1199
1186
|
const stream = await navigator.mediaDevices.getUserMedia({ audio })
|
|
1200
|
-
let
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
recorder
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1187
|
+
let rec = null
|
|
1188
|
+
try {
|
|
1189
|
+
let mimeType = 'audio/webm;codecs=opus'
|
|
1190
|
+
if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = ''
|
|
1191
|
+
const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream)
|
|
1192
|
+
rec = {
|
|
1193
|
+
recorder, stream, mode,
|
|
1194
|
+
target: target || voice._activeTarget,
|
|
1195
|
+
opId: opId || (voice.activeOpId || 0),
|
|
1196
|
+
chunks: [],
|
|
1197
|
+
mime: mimeType || recorder.mimeType || 'audio/webm',
|
|
1198
|
+
audioCtx: null, analyser: null,
|
|
1199
|
+
cutting: false, closing: false,
|
|
1200
|
+
silenceMs: 0, hadSpeech: false,
|
|
1201
|
+
streamMs: 0,
|
|
1202
|
+
}
|
|
1203
|
+
recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) rec.chunks.push(e.data) }
|
|
1204
|
+
const AC = typeof AudioContext !== 'undefined' ? AudioContext
|
|
1205
|
+
: (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
|
|
1206
|
+
if (AC) {
|
|
1207
|
+
rec.audioCtx = new AC()
|
|
1208
|
+
if (rec.audioCtx.state === 'suspended') { rec.audioCtx.resume().catch(() => {}) }
|
|
1209
|
+
const src = rec.audioCtx.createMediaStreamSource(stream)
|
|
1210
|
+
rec.srcNode = src
|
|
1211
|
+
let lastNode = src
|
|
1212
|
+
try {
|
|
1213
|
+
const filter = rec.audioCtx.createBiquadFilter()
|
|
1214
|
+
filter.type = 'highpass'
|
|
1215
|
+
filter.frequency.value = 80
|
|
1216
|
+
filter.Q.value = 0.707
|
|
1217
|
+
src.connect(filter)
|
|
1218
|
+
lastNode = filter
|
|
1219
|
+
rec.filterNode = filter
|
|
1220
|
+
} catch (e) { /* audio filter fallback */ }
|
|
1221
|
+
rec.analyser = rec.audioCtx.createAnalyser()
|
|
1222
|
+
rec.analyser.fftSize = 128
|
|
1223
|
+
lastNode.connect(rec.analyser)
|
|
1224
|
+
}
|
|
1225
|
+
recorder.start()
|
|
1226
|
+
return rec
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
if (rec) {
|
|
1229
|
+
teardown(rec)
|
|
1230
|
+
} else {
|
|
1231
|
+
try { stream.getTracks().forEach((t) => t.stop()) } catch (_) {}
|
|
1232
|
+
}
|
|
1233
|
+
throw err
|
|
1234
|
+
}
|
|
1236
1235
|
}
|
|
1237
1236
|
function currentLevel(rec) {
|
|
1238
1237
|
if (!rec || !rec.analyser) return 0
|
|
@@ -1252,6 +1251,8 @@ voice.webGpu = {
|
|
|
1252
1251
|
function cutPhrase() {
|
|
1253
1252
|
const rec = voice.rec
|
|
1254
1253
|
if (!rec || rec.cutting || rec.closing) return
|
|
1254
|
+
const opId = rec.opId || voice.activeOpId
|
|
1255
|
+
const target = rec.target || voice._activeTarget
|
|
1255
1256
|
rec.cutting = true
|
|
1256
1257
|
const stopped = waitStop(rec.recorder)
|
|
1257
1258
|
try {
|
|
@@ -1263,7 +1264,7 @@ voice.webGpu = {
|
|
|
1263
1264
|
return
|
|
1264
1265
|
}
|
|
1265
1266
|
stopped.then(async () => {
|
|
1266
|
-
if (rec.cancelled || voice.activeOpId !== opId) return
|
|
1267
|
+
if (rec.cancelled || voice.activeOpId !== opId || voice.disposed) return
|
|
1267
1268
|
const blob = new Blob(rec.chunks, { type: rec.mime })
|
|
1268
1269
|
rec.chunks = []
|
|
1269
1270
|
rec.silenceMs = 0
|
|
@@ -1277,42 +1278,42 @@ voice.webGpu = {
|
|
|
1277
1278
|
rec.cutting = false
|
|
1278
1279
|
if (rec.cancelled || blob.size < 600) return // cancelled or too short — not speech
|
|
1279
1280
|
dictationQueue = dictationQueue.then(async () => {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1281
|
+
if (rec.cancelled || voice.activeOpId !== opId || voice.disposed) return
|
|
1282
|
+
try {
|
|
1283
|
+
const out = await sendAudio(blob, rec.mime, 'dictation')
|
|
1284
|
+
if (rec.cancelled || voice.activeOpId !== opId || voice.disposed) return
|
|
1285
|
+
const text = out && out.text ? out.text : ''
|
|
1286
|
+
const action = out && out.action ? out.action : null
|
|
1287
|
+
if (voice.settings.structuredPromptVoice !== false && text) {
|
|
1288
|
+
const promptInfo = findActiveStructuredPrompt()
|
|
1289
|
+
if (promptInfo && submitToStructuredPrompt(text, promptInfo)) {
|
|
1290
|
+
return
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (action === 'send' || action === 'clear' || action === 'newline') {
|
|
1294
|
+
executeVoiceAction(action, text)
|
|
1289
1295
|
return
|
|
1290
1296
|
}
|
|
1297
|
+
if (action === 'cancel') {
|
|
1298
|
+
cancelCurrent()
|
|
1299
|
+
return
|
|
1300
|
+
}
|
|
1301
|
+
const delay = Number(voice.settings.sendDelayMs) || 0
|
|
1302
|
+
if (text && delay > 0 && !voice.holding) {
|
|
1303
|
+
appendDraft(text, target)
|
|
1304
|
+
voice.set({ phase: 'pending', pending: { text, undoOnly: true, leftMs: delay } })
|
|
1305
|
+
return
|
|
1306
|
+
}
|
|
1307
|
+
if (text) appendDraft(text, target)
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
if (!rec.cancelled && voice.activeOpId === opId && !voice.disposed) {
|
|
1310
|
+
voice.set({ error: String(e && e.message ? e.message : e) })
|
|
1311
|
+
}
|
|
1291
1312
|
}
|
|
1292
|
-
if (action === 'send' || action === 'clear' || action === 'newline') {
|
|
1293
|
-
executeVoiceAction(action, text)
|
|
1294
|
-
return
|
|
1295
|
-
}
|
|
1296
|
-
if (action === 'cancel') {
|
|
1297
|
-
cancelCurrent()
|
|
1298
|
-
return
|
|
1299
|
-
}
|
|
1300
|
-
const delay = Number(voice.settings.sendDelayMs) || 0
|
|
1301
|
-
if (text && delay > 0 && !voice.holding) {
|
|
1302
|
-
appendDraft(text)
|
|
1303
|
-
voice.set({ phase: 'pending', pending: { text, undoOnly: true, leftMs: delay } })
|
|
1304
|
-
return
|
|
1305
|
-
}
|
|
1306
|
-
if (text) appendDraft(text)
|
|
1307
|
-
} catch (e) {
|
|
1308
|
-
if (!rec.cancelled) {
|
|
1309
|
-
voice.set({ error: String(e && e.message ? e.message : e) })
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
1313
|
})
|
|
1313
1314
|
})
|
|
1314
1315
|
}
|
|
1315
|
-
function startBrowserLeg(mode, language) {
|
|
1316
|
+
function startBrowserLeg(mode, language, opId, target) {
|
|
1316
1317
|
if (voice.settings && voice.settings.localOnly) return false
|
|
1317
1318
|
if (!browserRecognitionAvailable()) return false
|
|
1318
1319
|
const finals = []
|
|
@@ -1321,6 +1322,7 @@ voice.webGpu = {
|
|
|
1321
1322
|
lang: language,
|
|
1322
1323
|
continuous: true,
|
|
1323
1324
|
onInterim: (text) => {
|
|
1325
|
+
if (voice.activeOpId !== opId || voice.phase !== 'recording' || voice.disposed) return
|
|
1324
1326
|
voice.caption = text
|
|
1325
1327
|
if (voice.settings.liveInterimPreview !== false) {
|
|
1326
1328
|
voice.interimTranscript = text
|
|
@@ -1336,27 +1338,34 @@ voice.webGpu = {
|
|
|
1336
1338
|
voice.browser = null
|
|
1337
1339
|
voice.caption = ''
|
|
1338
1340
|
voice.interimTranscript = ''
|
|
1339
|
-
openMic(mode)
|
|
1341
|
+
openMic(mode, opId, target)
|
|
1340
1342
|
.then((rec) => {
|
|
1341
|
-
if (voice.phase !== 'recording') {
|
|
1343
|
+
if (voice.activeOpId !== opId || voice.phase !== 'recording' || voice.disposed) {
|
|
1342
1344
|
teardown(rec)
|
|
1343
1345
|
return
|
|
1344
1346
|
}
|
|
1347
|
+
rec.opId = opId
|
|
1348
|
+
rec.target = target
|
|
1345
1349
|
voice.rec = rec
|
|
1346
1350
|
voice.notify()
|
|
1347
1351
|
})
|
|
1348
|
-
.catch((err) =>
|
|
1352
|
+
.catch((err) => {
|
|
1353
|
+
if (voice.activeOpId !== opId) return
|
|
1354
|
+
voice.set({ phase: 'error', error: String(err && err.message ? err.message : err), rec: null })
|
|
1355
|
+
})
|
|
1349
1356
|
}
|
|
1350
1357
|
}
|
|
1351
1358
|
},
|
|
1352
1359
|
onFinal: (text) => {
|
|
1360
|
+
if (voice.activeOpId !== opId || (voice.phase !== 'recording' && voice.phase !== 'finishing') || voice.disposed) return
|
|
1353
1361
|
finals.push(text)
|
|
1354
1362
|
voice.caption = ''
|
|
1355
1363
|
voice.interimTranscript = ''
|
|
1356
|
-
if (mode === 'dictation') appendDraft(text)
|
|
1364
|
+
if (mode === 'dictation') appendDraft(text, target)
|
|
1357
1365
|
else voice.notify()
|
|
1358
1366
|
},
|
|
1359
1367
|
onError: (reason) => {
|
|
1368
|
+
if (voice.activeOpId !== opId || voice.disposed) return
|
|
1360
1369
|
voice.browser = null
|
|
1361
1370
|
voice.interimTranscript = ''
|
|
1362
1371
|
voice.set({ phase: 'error', error: t('browserFailed') + reason })
|
|
@@ -1369,6 +1378,16 @@ voice.webGpu = {
|
|
|
1369
1378
|
function startRecording(mode) {
|
|
1370
1379
|
if (voice.phase !== 'idle' && voice.phase !== 'error') return
|
|
1371
1380
|
voice.activeOpId = (voice.activeOpId || 0) + 1
|
|
1381
|
+
const opId = voice.activeOpId
|
|
1382
|
+
const targetActions = voice.inputActions
|
|
1383
|
+
const targetInput = voice.input
|
|
1384
|
+
let span = null
|
|
1385
|
+
if (targetActions && typeof targetActions.captureInsertion === 'function') {
|
|
1386
|
+
try { span = targetActions.captureInsertion() } catch (_) {}
|
|
1387
|
+
}
|
|
1388
|
+
const target = { actions: targetActions, input: targetInput, span }
|
|
1389
|
+
voice._activeTarget = target
|
|
1390
|
+
|
|
1372
1391
|
if (voice.isTtsSpeaking) {
|
|
1373
1392
|
if (voice.settings.bargeIn) {
|
|
1374
1393
|
triggerBargeIn()
|
|
@@ -1381,121 +1400,31 @@ voice.webGpu = {
|
|
|
1381
1400
|
dictationQueue = Promise.resolve()
|
|
1382
1401
|
voice.set({ phase: 'recording', mode, error: '', levels: [], caption: '', interimTranscript: '' })
|
|
1383
1402
|
modeChain(mode).then((info) => {
|
|
1403
|
+
if (voice.activeOpId !== opId || voice.phase !== 'recording' || voice.disposed) return
|
|
1384
1404
|
voice._activeChain = info.chain || []
|
|
1385
1405
|
voice._activeLang = info.language || ''
|
|
1386
|
-
|
|
1387
|
-
|
|
1406
|
+
voice._activeModelMap = info.modelMap || {}
|
|
1407
|
+
if (info.chain[0] === 'browser' && !voice.settings.localOnly && startBrowserLeg(mode, info.language, opId, target)) return
|
|
1408
|
+
openMic(mode, opId, target)
|
|
1388
1409
|
.then((rec) => {
|
|
1389
|
-
if (voice.phase !== 'recording') {
|
|
1410
|
+
if (voice.activeOpId !== opId || voice.phase !== 'recording' || voice.disposed) {
|
|
1390
1411
|
teardown(rec)
|
|
1391
1412
|
return
|
|
1392
1413
|
}
|
|
1414
|
+
rec.opId = opId
|
|
1415
|
+
rec.target = target
|
|
1393
1416
|
voice.rec = rec
|
|
1394
1417
|
voice.notify()
|
|
1395
1418
|
})
|
|
1396
|
-
.catch((err) =>
|
|
1419
|
+
.catch((err) => {
|
|
1420
|
+
if (voice.activeOpId !== opId) return
|
|
1421
|
+
voice.set({ phase: 'error', error: String(err && err.message ? err.message : err), rec: null })
|
|
1422
|
+
})
|
|
1397
1423
|
})
|
|
1398
1424
|
}
|
|
1399
1425
|
function startDictation() { startRecording('dictation') }
|
|
1400
1426
|
function startMessage() { startRecording('message') }
|
|
1401
|
-
// --------------------------------------------------------- hold gesture
|
|
1402
|
-
const HOLD_THRESHOLD_MS = 350
|
|
1403
|
-
const hold = { active: false, mode: null, startedAt: 0, armed: false }
|
|
1404
|
-
let activePointerCleanup = null
|
|
1405
|
-
function beginHold(mode, pointerEvent) {
|
|
1406
|
-
if (hold.armed || (voice.phase !== 'idle' && voice.phase !== 'error')) return
|
|
1407
|
-
hold.armed = true
|
|
1408
|
-
hold.mode = mode
|
|
1409
|
-
hold.startedAt = Date.now()
|
|
1410
|
-
hold.active = false
|
|
1411
|
-
if (pointerEvent?.target?.setPointerCapture && pointerEvent.pointerId !== undefined) {
|
|
1412
|
-
try { pointerEvent.target.setPointerCapture(pointerEvent.pointerId) } catch (e) {}
|
|
1413
|
-
}
|
|
1414
|
-
if (typeof window !== 'undefined') {
|
|
1415
|
-
if (activePointerCleanup) { try { activePointerCleanup() } catch (e) {} }
|
|
1416
|
-
const onUp = () => endHold(false)
|
|
1417
|
-
const onCancel = () => endHold(true)
|
|
1418
|
-
window.addEventListener('pointerup', onUp, true)
|
|
1419
|
-
window.addEventListener('pointercancel', onCancel, true)
|
|
1420
|
-
window.addEventListener('blur', onCancel, true)
|
|
1421
|
-
activePointerCleanup = () => {
|
|
1422
|
-
window.removeEventListener('pointerup', onUp, true)
|
|
1423
|
-
window.removeEventListener('pointercancel', onCancel, true)
|
|
1424
|
-
window.removeEventListener('blur', onCancel, true)
|
|
1425
|
-
activePointerCleanup = null
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
startRecording(mode)
|
|
1429
|
-
voice.holding = true
|
|
1430
|
-
voice.notify()
|
|
1431
|
-
}
|
|
1432
|
-
function endHold(cancelled) {
|
|
1433
|
-
if (!hold.armed) return
|
|
1434
|
-
if (activePointerCleanup) { try { activePointerCleanup() } catch (e) {} }
|
|
1435
|
-
const heldMs = Date.now() - hold.startedAt
|
|
1436
|
-
hold.armed = false
|
|
1437
|
-
hold.active = false
|
|
1438
|
-
voice.holding = false
|
|
1439
|
-
if (!cancelled && heldMs < HOLD_THRESHOLD_MS) { voice.notify(); return }
|
|
1440
|
-
if (cancelled) cancelCurrent()
|
|
1441
|
-
else stopCurrent()
|
|
1442
|
-
}
|
|
1443
1427
|
|
|
1444
|
-
let activeHotkeyCleanup = null
|
|
1445
|
-
function clearGlobalHotkey() {
|
|
1446
|
-
if (typeof activeHotkeyCleanup === 'function') {
|
|
1447
|
-
try { activeHotkeyCleanup() } catch (_) { /* ignore */ }
|
|
1448
|
-
activeHotkeyCleanup = null
|
|
1449
|
-
}
|
|
1450
|
-
if (typeof window !== 'undefined' && typeof window.__dsh_voice_hotkey_cleanup === 'function') {
|
|
1451
|
-
try { window.__dsh_voice_hotkey_cleanup() } catch (_) { /* ignore */ }
|
|
1452
|
-
window.__dsh_voice_hotkey_cleanup = null
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
voice.clearGlobalHotkey = clearGlobalHotkey
|
|
1456
|
-
function installHotkey(ctx, keyName, mode) {
|
|
1457
|
-
clearGlobalHotkey()
|
|
1458
|
-
if (typeof document === 'undefined' || !keyName) return () => {}
|
|
1459
|
-
const down = (event) => {
|
|
1460
|
-
if (event.repeat) return
|
|
1461
|
-
const target = event.target
|
|
1462
|
-
const isEditable = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable || (typeof target.getAttribute === 'function' && target.getAttribute('contenteditable') === 'true'))
|
|
1463
|
-
if (isEditable && !isAllowedInEditable(keyName)) return
|
|
1464
|
-
if (hotkeyMatches(event, keyName)) {
|
|
1465
|
-
event.preventDefault()
|
|
1466
|
-
event.stopPropagation()
|
|
1467
|
-
beginHold(mode)
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
const up = (event) => {
|
|
1472
|
-
if (hold.armed && isHotkeyRelease(event, keyName)) {
|
|
1473
|
-
event.preventDefault()
|
|
1474
|
-
endHold(false)
|
|
1475
|
-
} else if (event.key === 'Escape' && hold.armed) {
|
|
1476
|
-
event.preventDefault()
|
|
1477
|
-
endHold(true)
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
const blur = () => { if (hold.armed) endHold(true) }
|
|
1481
|
-
document.addEventListener('keydown', down, true)
|
|
1482
|
-
document.addEventListener('keyup', up, true)
|
|
1483
|
-
window.addEventListener('blur', blur)
|
|
1484
|
-
const cleanup = () => {
|
|
1485
|
-
document.removeEventListener('keydown', down, true)
|
|
1486
|
-
document.removeEventListener('keyup', up, true)
|
|
1487
|
-
window.removeEventListener('blur', blur)
|
|
1488
|
-
if (activeHotkeyCleanup === cleanup) activeHotkeyCleanup = null
|
|
1489
|
-
if (typeof window !== 'undefined' && window.__dsh_voice_hotkey_cleanup === cleanup) {
|
|
1490
|
-
window.__dsh_voice_hotkey_cleanup = null
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
activeHotkeyCleanup = cleanup
|
|
1494
|
-
if (typeof window !== 'undefined') {
|
|
1495
|
-
window.__dsh_voice_hotkey_cleanup = cleanup
|
|
1496
|
-
}
|
|
1497
|
-
return cleanup
|
|
1498
|
-
}
|
|
1499
1428
|
function cancelCurrent() {
|
|
1500
1429
|
voice.activeOpId = (voice.activeOpId || 0) + 1
|
|
1501
1430
|
announceVoice('end')
|
|
@@ -1522,16 +1451,20 @@ voice.webGpu = {
|
|
|
1522
1451
|
announceVoice('end')
|
|
1523
1452
|
focusComposer()
|
|
1524
1453
|
if (voice.browser) {
|
|
1454
|
+
const opId = voice.activeOpId
|
|
1525
1455
|
const mode = voice.mode
|
|
1526
1456
|
const b = voice.browser
|
|
1457
|
+
const target = voice._activeTarget
|
|
1527
1458
|
voice.browser = null
|
|
1528
1459
|
voice.caption = ''
|
|
1460
|
+
voice.set({ phase: 'finishing' })
|
|
1529
1461
|
Promise.resolve(b.stop()).then(() => {
|
|
1462
|
+
if (voice.disposed || voice.activeOpId !== opId) return
|
|
1530
1463
|
const said = (voice.browserFinals || []).join(' ').trim()
|
|
1531
1464
|
voice.browserFinals = null
|
|
1532
1465
|
if (!said) { voice.set({ phase: 'idle' }); return }
|
|
1533
1466
|
if (mode === 'message') {
|
|
1534
|
-
appendDraft(said)
|
|
1467
|
+
appendDraft(said, target)
|
|
1535
1468
|
voice.set({ phase: 'pending', pending: { text: said, leftMs: voice.settings.autoSendMs } })
|
|
1536
1469
|
} else {
|
|
1537
1470
|
voice.set({ phase: 'idle' })
|
|
@@ -1542,17 +1475,23 @@ voice.webGpu = {
|
|
|
1542
1475
|
const rec = voice.rec
|
|
1543
1476
|
if (!rec || rec.closing) return
|
|
1544
1477
|
rec.closing = true
|
|
1545
|
-
|
|
1546
|
-
const
|
|
1478
|
+
const opId = rec.opId || voice.activeOpId
|
|
1479
|
+
const target = rec.target || voice._activeTarget
|
|
1547
1480
|
const mode = rec.mode
|
|
1548
1481
|
const stopped = waitStop(rec.recorder)
|
|
1549
1482
|
try {
|
|
1550
1483
|
if (rec.recorder && rec.recorder.state !== 'inactive') rec.recorder.stop()
|
|
1551
1484
|
} catch (e) { /* already stopped */ }
|
|
1552
1485
|
stopped.then(async () => {
|
|
1486
|
+
if (voice.disposed || rec.cancelled || voice.activeOpId !== opId) {
|
|
1487
|
+
teardown(rec)
|
|
1488
|
+
if (voice.rec === rec) voice.rec = null
|
|
1489
|
+
return
|
|
1490
|
+
}
|
|
1553
1491
|
const blob = new Blob(rec.chunks, { type: rec.mime })
|
|
1554
1492
|
teardown(rec)
|
|
1555
|
-
voice.rec = null
|
|
1493
|
+
if (voice.rec === rec) voice.rec = null
|
|
1494
|
+
if (voice.disposed || rec.cancelled || voice.activeOpId !== opId) return
|
|
1556
1495
|
if (blob.size < (mode === 'dictation' ? 600 : 1200)) {
|
|
1557
1496
|
if (mode === 'dictation') { try { await dictationQueue } catch (_) {} }
|
|
1558
1497
|
voice.set({ phase: 'idle' })
|
|
@@ -1566,10 +1505,10 @@ voice.webGpu = {
|
|
|
1566
1505
|
voice.lastNote = { blob, url: noteUrl, mime: rec.mime, text: '' }
|
|
1567
1506
|
voice.set({ phase: 'processing' })
|
|
1568
1507
|
const processTail = async () => {
|
|
1569
|
-
if (rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
|
|
1508
|
+
if (voice.disposed || rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
|
|
1570
1509
|
try {
|
|
1571
1510
|
const out = await sendAudio(blob, rec.mime, mode)
|
|
1572
|
-
if (rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
|
|
1511
|
+
if (voice.disposed || rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
|
|
1573
1512
|
if (out.command) { runSessionCommand(out.command); return }
|
|
1574
1513
|
const text = out.text || ''
|
|
1575
1514
|
const action = out.action || null
|
|
@@ -1590,7 +1529,7 @@ voice.webGpu = {
|
|
|
1590
1529
|
return
|
|
1591
1530
|
}
|
|
1592
1531
|
if (action === 'cancel') { cancelCurrent(); return }
|
|
1593
|
-
appendDraft(text)
|
|
1532
|
+
appendDraft(text, target)
|
|
1594
1533
|
if (voice.lastNote) voice.lastNote.text = text
|
|
1595
1534
|
if (mode === 'message') {
|
|
1596
1535
|
voice.set({ phase: 'pending', pending: { text, totalMs: voice.settings.autoSendMs, leftMs: voice.settings.autoSendMs, audioUrl: noteUrl } })
|
|
@@ -1598,7 +1537,7 @@ voice.webGpu = {
|
|
|
1598
1537
|
voice.set({ phase: 'idle' })
|
|
1599
1538
|
}
|
|
1600
1539
|
} catch (e) {
|
|
1601
|
-
if (rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
|
|
1540
|
+
if (voice.disposed || rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
|
|
1602
1541
|
voice.set({ phase: 'error', error: String(e && e.message ? e.message : e) })
|
|
1603
1542
|
}
|
|
1604
1543
|
}
|
|
@@ -1612,78 +1551,69 @@ voice.webGpu = {
|
|
|
1612
1551
|
}
|
|
1613
1552
|
function clearDraft() {
|
|
1614
1553
|
const actions = voice.inputActions
|
|
1615
|
-
if (
|
|
1616
|
-
voice.set({ phase: 'error', error: t('composerUnavailable') })
|
|
1617
|
-
return false
|
|
1618
|
-
}
|
|
1619
|
-
actions.setDraft('')
|
|
1620
|
-
return true
|
|
1554
|
+
if (actions && typeof actions.setDraft === 'function') actions.setDraft('')
|
|
1621
1555
|
}
|
|
1622
|
-
|
|
1556
|
+
|
|
1557
|
+
// Voice actions (#29-7): spoken triggers perform composer actions.
|
|
1558
|
+
function executeVoiceAction(action, rawText) {
|
|
1623
1559
|
if (action === 'send') {
|
|
1560
|
+
const text = rawText.replace(/(^|[\s,.!?])(отправь|пошли|send)([\s,.!?]|$)/gi, '').trim()
|
|
1624
1561
|
if (text) appendDraft(text)
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
const payload = text ? (text.endsWith('\n') ? text : text + '\n') : '\n'
|
|
1562
|
+
if (typeof voice.inputActions?.submit === 'function') {
|
|
1563
|
+
voice.inputActions.submit()
|
|
1564
|
+
}
|
|
1565
|
+
voice.set({ phase: 'idle' })
|
|
1566
|
+
} else if (action === 'clear') {
|
|
1567
|
+
clearDraft()
|
|
1568
|
+
voice.set({ phase: 'idle' })
|
|
1569
|
+
} else if (action === 'newline') {
|
|
1570
|
+
const payload = (rawText.replace(/(^|[\s,.!?])(с новой строки|новая строка|абзац|new line|paragraph)([\s,.!?]|$)/gi, '').trim())
|
|
1635
1571
|
appendDraft(payload)
|
|
1636
|
-
|
|
1637
|
-
return true
|
|
1572
|
+
voice.set({ phase: 'idle' })
|
|
1638
1573
|
}
|
|
1639
|
-
if (action === 'cancel') { cancelCurrent(); return true }
|
|
1640
|
-
return false
|
|
1641
1574
|
}
|
|
1642
|
-
|
|
1575
|
+
|
|
1643
1576
|
function submitPending() {
|
|
1644
|
-
|
|
1577
|
+
const pending = voice.pending
|
|
1578
|
+
if (!pending) return
|
|
1579
|
+
const text = pending.text
|
|
1645
1580
|
const actions = voice.inputActions
|
|
1646
|
-
|
|
1647
|
-
voice.set({ phase: 'error', error: t('composerUnavailable') })
|
|
1648
|
-
return
|
|
1649
|
-
}
|
|
1581
|
+
voice.pending = null
|
|
1650
1582
|
voice.set({ phase: 'idle' })
|
|
1583
|
+
|
|
1651
1584
|
if (voice.settings.polishSend) {
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
body: JSON.stringify({ text: draft }),
|
|
1663
|
-
signal: controller ? controller.signal : undefined,
|
|
1664
|
-
})
|
|
1665
|
-
const parsed = await res.json().catch(() => null)
|
|
1666
|
-
if (parsed && parsed.ok && typeof parsed.text === 'string' && parsed.text.trim()) {
|
|
1667
|
-
actions.setDraft(parsed.text.trim())
|
|
1668
|
-
}
|
|
1669
|
-
} finally {
|
|
1670
|
-
if (timer) clearTimeout(timer)
|
|
1671
|
-
}
|
|
1585
|
+
fetch('/dsh-voice/polish', {
|
|
1586
|
+
method: 'POST',
|
|
1587
|
+
headers: { 'content-type': 'application/json' },
|
|
1588
|
+
body: JSON.stringify({ text }),
|
|
1589
|
+
})
|
|
1590
|
+
.then((res) => res.json())
|
|
1591
|
+
.then((data) => {
|
|
1592
|
+
const polished = (data && data.text) || text
|
|
1593
|
+
if (actions && typeof actions.setDraft === 'function') {
|
|
1594
|
+
actions.setDraft(polished)
|
|
1672
1595
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1596
|
+
if (actions && typeof actions.submit === 'function') {
|
|
1597
|
+
actions.submit()
|
|
1598
|
+
}
|
|
1599
|
+
})
|
|
1600
|
+
.catch(() => {
|
|
1601
|
+
if (actions && typeof actions.submit === 'function') {
|
|
1602
|
+
actions.submit()
|
|
1603
|
+
}
|
|
1604
|
+
})
|
|
1605
|
+
} else if (actions && typeof actions.submit === 'function') {
|
|
1606
|
+
actions.submit()
|
|
1677
1607
|
}
|
|
1678
|
-
setTimeout(() => { try { actions.submit() } catch (e) { /* composer busy */ } }, 0)
|
|
1679
1608
|
}
|
|
1609
|
+
|
|
1680
1610
|
function runSessionCommand(cmd) {
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
if (
|
|
1684
|
-
|
|
1685
|
-
}
|
|
1611
|
+
if (cmd === 'send') submitPending()
|
|
1612
|
+
else if (cmd === 'cancel') cancelCurrent()
|
|
1613
|
+
else if (cmd === 'stop') cancelCurrent()
|
|
1614
|
+
else if (cmd === 'continue') { /* keep recording */ }
|
|
1686
1615
|
}
|
|
1616
|
+
|
|
1687
1617
|
function keepPending() {
|
|
1688
1618
|
voice.pending = null
|
|
1689
1619
|
voice.set({ phase: 'idle' })
|
|
@@ -1727,6 +1657,194 @@ voice.webGpu = {
|
|
|
1727
1657
|
)
|
|
1728
1658
|
}
|
|
1729
1659
|
|
|
1660
|
+
// Human-readable key name.
|
|
1661
|
+
const KEY_LABELS = {
|
|
1662
|
+
Control: 'Ctrl', Alt: 'Alt', Shift: 'Shift', Meta: 'Win',
|
|
1663
|
+
Escape: 'Esc',
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function keyLabel(name) {
|
|
1667
|
+
if (!name) return t('keyUnset')
|
|
1668
|
+
if (typeof name === 'string' && name.includes('+')) {
|
|
1669
|
+
return name.split('+').map((p) => keyLabel(p)).join(' + ')
|
|
1670
|
+
}
|
|
1671
|
+
if (name === 'Space') return t('keySpace')
|
|
1672
|
+
if (KEY_LABELS[name]) return KEY_LABELS[name]
|
|
1673
|
+
// Drop the Key/Digit prefix from codes like KeyR and Digit5.
|
|
1674
|
+
return String(name).replace(/^Key/, '').replace(/^Digit/, '')
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// What to store on key press. Pure modifiers are remembered by name:
|
|
1678
|
+
// left and right have different codes but the user means "either".
|
|
1679
|
+
// Supports combos like Control+Space or Alt+KeyV.
|
|
1680
|
+
function keyFromEvent(event) {
|
|
1681
|
+
if (!event) return ''
|
|
1682
|
+
const modifiers = []
|
|
1683
|
+
if (event.ctrlKey && event.key !== 'Control') modifiers.push('Control')
|
|
1684
|
+
if (event.altKey && event.key !== 'Alt') modifiers.push('Alt')
|
|
1685
|
+
if (event.shiftKey && event.key !== 'Shift') modifiers.push('Shift')
|
|
1686
|
+
if (event.metaKey && event.key !== 'Meta') modifiers.push('Meta')
|
|
1687
|
+
|
|
1688
|
+
const main = ['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)
|
|
1689
|
+
? event.key
|
|
1690
|
+
: (event.code || event.key || '')
|
|
1691
|
+
|
|
1692
|
+
if (modifiers.length > 0 && !['Control', 'Alt', 'Shift', 'Meta'].includes(main)) {
|
|
1693
|
+
return modifiers.concat(main).join('+')
|
|
1694
|
+
}
|
|
1695
|
+
return main
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function hotkeyMatches(event, name) {
|
|
1699
|
+
if (!event || !name) return false
|
|
1700
|
+
if (typeof name === 'string' && name.includes('+')) {
|
|
1701
|
+
const parts = name.split('+')
|
|
1702
|
+
const main = parts[parts.length - 1]
|
|
1703
|
+
const needCtrl = parts.includes('Control')
|
|
1704
|
+
const needAlt = parts.includes('Alt')
|
|
1705
|
+
const needShift = parts.includes('Shift')
|
|
1706
|
+
const needMeta = parts.includes('Meta')
|
|
1707
|
+
if (needCtrl && !event.ctrlKey) return false
|
|
1708
|
+
if (needAlt && !event.altKey) return false
|
|
1709
|
+
if (needShift && !event.shiftKey) return false
|
|
1710
|
+
if (needMeta && !event.metaKey) return false
|
|
1711
|
+
return event.code === main || event.key === main
|
|
1712
|
+
}
|
|
1713
|
+
if (name === 'Control') return event.key === 'Control'
|
|
1714
|
+
if (name === 'Alt') return event.key === 'Alt'
|
|
1715
|
+
if (name === 'Shift') return event.key === 'Shift'
|
|
1716
|
+
if (name === 'Meta') return event.key === 'Meta'
|
|
1717
|
+
return event.code === name || event.key === name
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function isAllowedInEditable(name) {
|
|
1721
|
+
if (!name) return false
|
|
1722
|
+
if (['Control', 'Alt', 'Shift', 'Meta'].includes(name)) return true
|
|
1723
|
+
if (/^F\d{1,2}$/.test(name)) return true
|
|
1724
|
+
if (typeof name === 'string' && name.includes('+')) return true
|
|
1725
|
+
if (['Tab', 'Pause', 'ScrollLock', 'Insert'].includes(name)) return true
|
|
1726
|
+
return false
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function isHotkeyRelease(event, name) {
|
|
1730
|
+
if (!event || !name) return false
|
|
1731
|
+
if (typeof name === 'string' && name.includes('+')) {
|
|
1732
|
+
const parts = name.split('+')
|
|
1733
|
+
if (event.code === parts[parts.length - 1] || event.key === parts[parts.length - 1]) return true
|
|
1734
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1735
|
+
if (event.key === parts[i] || event.code === parts[i]) return true
|
|
1736
|
+
}
|
|
1737
|
+
return false
|
|
1738
|
+
}
|
|
1739
|
+
return event.code === name || event.key === name
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Announce that the user started speaking.
|
|
1743
|
+
//
|
|
1744
|
+
// Playback should mute immediately: listening and talking at once is
|
|
1745
|
+
// impossible. There is no plugin-to-plugin API — this broadcasts a window
|
|
1746
|
+
// event that anyone may hear, so either side works alone.
|
|
1747
|
+
|
|
1748
|
+
// --------------------------------------------------------- hold gesture
|
|
1749
|
+
const HOLD_THRESHOLD_MS = 350
|
|
1750
|
+
const hold = { active: false, mode: null, startedAt: 0, armed: false }
|
|
1751
|
+
let activePointerCleanup = null
|
|
1752
|
+
function beginHold(mode, pointerEvent) {
|
|
1753
|
+
if (hold.armed || (voice.phase !== 'idle' && voice.phase !== 'error')) return
|
|
1754
|
+
hold.armed = true
|
|
1755
|
+
hold.mode = mode
|
|
1756
|
+
hold.startedAt = Date.now()
|
|
1757
|
+
hold.active = false
|
|
1758
|
+
if (pointerEvent?.target?.setPointerCapture && pointerEvent.pointerId !== undefined) {
|
|
1759
|
+
try { pointerEvent.target.setPointerCapture(pointerEvent.pointerId) } catch (e) {}
|
|
1760
|
+
}
|
|
1761
|
+
if (typeof window !== 'undefined') {
|
|
1762
|
+
if (activePointerCleanup) { try { activePointerCleanup() } catch (e) {} }
|
|
1763
|
+
const onUp = () => endHold(false)
|
|
1764
|
+
const onCancel = () => endHold(true)
|
|
1765
|
+
window.addEventListener('pointerup', onUp, { once: true })
|
|
1766
|
+
window.addEventListener('pointercancel', onCancel, { once: true })
|
|
1767
|
+
activePointerCleanup = () => {
|
|
1768
|
+
window.removeEventListener('pointerup', onUp)
|
|
1769
|
+
window.removeEventListener('pointercancel', onCancel)
|
|
1770
|
+
activePointerCleanup = null
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
voice.holding = false
|
|
1774
|
+
startRecording(mode)
|
|
1775
|
+
setTimeout(() => {
|
|
1776
|
+
if (hold.armed) {
|
|
1777
|
+
hold.active = true
|
|
1778
|
+
voice.holding = true
|
|
1779
|
+
voice.notify()
|
|
1780
|
+
}
|
|
1781
|
+
}, HOLD_THRESHOLD_MS)
|
|
1782
|
+
}
|
|
1783
|
+
function endHold(cancelled) {
|
|
1784
|
+
if (activePointerCleanup) { try { activePointerCleanup() } catch (e) {} }
|
|
1785
|
+
if (!hold.armed) return
|
|
1786
|
+
hold.armed = false
|
|
1787
|
+
voice.holding = false
|
|
1788
|
+
voice.notify()
|
|
1789
|
+
if (cancelled) cancelCurrent()
|
|
1790
|
+
else stopCurrent()
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
let activeHotkeyCleanup = null
|
|
1794
|
+
function clearGlobalHotkey() {
|
|
1795
|
+
if (typeof activeHotkeyCleanup === 'function') {
|
|
1796
|
+
try { activeHotkeyCleanup() } catch (_) { /* ignore */ }
|
|
1797
|
+
activeHotkeyCleanup = null
|
|
1798
|
+
}
|
|
1799
|
+
if (typeof window !== 'undefined' && typeof window.__dsh_voice_hotkey_cleanup === 'function') {
|
|
1800
|
+
try { window.__dsh_voice_hotkey_cleanup() } catch (_) { /* ignore */ }
|
|
1801
|
+
window.__dsh_voice_hotkey_cleanup = null
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
voice.clearGlobalHotkey = clearGlobalHotkey
|
|
1805
|
+
function installHotkey(ctx, keyName, mode) {
|
|
1806
|
+
clearGlobalHotkey()
|
|
1807
|
+
if (typeof document === 'undefined' || !keyName) return () => {}
|
|
1808
|
+
const down = (event) => {
|
|
1809
|
+
if (event.repeat) return
|
|
1810
|
+
const target = event.target
|
|
1811
|
+
const isEditable = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable || (typeof target.getAttribute === 'function' && target.getAttribute('contenteditable') === 'true'))
|
|
1812
|
+
if (isEditable && !isAllowedInEditable(keyName)) return
|
|
1813
|
+
if (hotkeyMatches(event, keyName)) {
|
|
1814
|
+
event.preventDefault()
|
|
1815
|
+
event.stopPropagation()
|
|
1816
|
+
beginHold(mode)
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
const up = (event) => {
|
|
1821
|
+
if (hold.armed && isHotkeyRelease(event, keyName)) {
|
|
1822
|
+
event.preventDefault()
|
|
1823
|
+
endHold(false)
|
|
1824
|
+
} else if (event.key === 'Escape' && hold.armed) {
|
|
1825
|
+
event.preventDefault()
|
|
1826
|
+
endHold(true)
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const blur = () => { if (hold.armed) endHold(true) }
|
|
1830
|
+
document.addEventListener('keydown', down, true)
|
|
1831
|
+
document.addEventListener('keyup', up, true)
|
|
1832
|
+
window.addEventListener('blur', blur)
|
|
1833
|
+
const cleanup = () => {
|
|
1834
|
+
document.removeEventListener('keydown', down, true)
|
|
1835
|
+
document.removeEventListener('keyup', up, true)
|
|
1836
|
+
window.removeEventListener('blur', blur)
|
|
1837
|
+
if (activeHotkeyCleanup === cleanup) activeHotkeyCleanup = null
|
|
1838
|
+
if (typeof window !== 'undefined' && window.__dsh_voice_hotkey_cleanup === cleanup) {
|
|
1839
|
+
window.__dsh_voice_hotkey_cleanup = null
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
activeHotkeyCleanup = cleanup
|
|
1843
|
+
if (typeof window !== 'undefined') {
|
|
1844
|
+
window.__dsh_voice_hotkey_cleanup = cleanup
|
|
1845
|
+
}
|
|
1846
|
+
return cleanup
|
|
1847
|
+
}
|
|
1730
1848
|
// ----------------------------------------------------------- visualizers
|
|
1731
1849
|
let _accentColorCache = { color: '', expires: 0 }
|
|
1732
1850
|
let _softColorCache = { color: '', expires: 0 }
|
|
@@ -2180,6 +2298,7 @@ voice.webGpu = {
|
|
|
2180
2298
|
Object.assign(voice.settings, {
|
|
2181
2299
|
beep: !!(data && data.beep),
|
|
2182
2300
|
localOnly: !!(data && data.localOnly),
|
|
2301
|
+
webgpuModel: String((data && data.webgpuModel) || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'),
|
|
2183
2302
|
micDeviceId: String((data && data.micDeviceId) || ''),
|
|
2184
2303
|
historyLimit: Number(data && data.historyLimit),
|
|
2185
2304
|
voiceCommands: !!(data && data.voiceCommands),
|
|
@@ -2709,7 +2828,7 @@ voice.webGpu = {
|
|
|
2709
2828
|
style: { flex: 1 },
|
|
2710
2829
|
},
|
|
2711
2830
|
React.createElement('option', { value: 'cpu' }, 'CPU (Standard)'),
|
|
2712
|
-
React.createElement('option', { value: '
|
|
2831
|
+
React.createElement('option', { value: 'rknn' }, 'Rockchip RK3588 NPU (RKNN)'),
|
|
2713
2832
|
React.createElement('option', { value: 'openvino' }, 'Intel OpenVINO'),
|
|
2714
2833
|
React.createElement('option', { value: 'cuda' }, 'NVIDIA CUDA'),
|
|
2715
2834
|
),
|
|
@@ -3124,7 +3243,7 @@ voice.webGpu = {
|
|
|
3124
3243
|
// ------------------------------------------------------- settings voice section
|
|
3125
3244
|
function VoiceSection(props) {
|
|
3126
3245
|
const t = (props && props.t) || moduleT
|
|
3127
|
-
const ctx = props.ctx
|
|
3246
|
+
const ctx = (props && props.ctx) || {}
|
|
3128
3247
|
// On a non-localhost page the kernel disables settings entirely: the
|
|
3129
3248
|
// shared document is not readable, every section gets "unavailable" and
|
|
3130
3249
|
// writes are dropped. The server does not share that restriction, and
|
|
@@ -3227,6 +3346,27 @@ voice.webGpu = {
|
|
|
3227
3346
|
try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
|
|
3228
3347
|
}
|
|
3229
3348
|
|
|
3349
|
+
const setTop = (key, val) => {
|
|
3350
|
+
setDraft((d) => Object.assign({}, d || {}, { [key]: val }))
|
|
3351
|
+
}
|
|
3352
|
+
const setIn = (mode, key, val) => {
|
|
3353
|
+
setDraft((d) => {
|
|
3354
|
+
const next = Object.assign({}, d || {})
|
|
3355
|
+
next[mode] = Object.assign({}, next[mode] || {}, { [key]: val })
|
|
3356
|
+
return next
|
|
3357
|
+
})
|
|
3358
|
+
}
|
|
3359
|
+
const textField = (key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
3360
|
+
React.createElement('label', null, label),
|
|
3361
|
+
React.createElement('input', {
|
|
3362
|
+
type: 'text',
|
|
3363
|
+
value: (draft && draft[key]) ?? '',
|
|
3364
|
+
disabled: !writable,
|
|
3365
|
+
onChange: (e) => setTop(key, e.target.value),
|
|
3366
|
+
}),
|
|
3367
|
+
hint ? React.createElement('span', { className: 'dvs-sub' }, hint) : null,
|
|
3368
|
+
)
|
|
3369
|
+
|
|
3230
3370
|
React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
|
|
3231
3371
|
|
|
3232
3372
|
React.useEffect(() => {
|
|
@@ -3259,13 +3399,10 @@ voice.webGpu = {
|
|
|
3259
3399
|
techJargonCorrection: val.techJargonCorrection !== false,
|
|
3260
3400
|
liveInterimPreview: val.liveInterimPreview !== false,
|
|
3261
3401
|
structuredPromptVoice: val.structuredPromptVoice !== false,
|
|
3402
|
+
webgpuModel: String(val.webgpuModel || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'),
|
|
3262
3403
|
})
|
|
3263
3404
|
}, [ready, value])
|
|
3264
3405
|
|
|
3265
|
-
const { sensevoiceState, installingSensevoice, triggerInstallSensevoice } = useSensevoiceInstaller(fetchStatus)
|
|
3266
|
-
|
|
3267
|
-
const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
|
|
3268
|
-
|
|
3269
3406
|
const [statusData, setStatusData] = React.useState(null)
|
|
3270
3407
|
const [statusLatency, setStatusLatency] = React.useState(null)
|
|
3271
3408
|
const fetchStatus = React.useCallback(() => {
|
|
@@ -3279,6 +3416,9 @@ voice.webGpu = {
|
|
|
3279
3416
|
setStatusLatency(Math.round(performance.now() - start))
|
|
3280
3417
|
if (data && data.ok) {
|
|
3281
3418
|
setStatusData(data)
|
|
3419
|
+
if (data.webgpuModel && voice.settings) {
|
|
3420
|
+
voice.settings.webgpuModel = data.webgpuModel
|
|
3421
|
+
}
|
|
3282
3422
|
}
|
|
3283
3423
|
})
|
|
3284
3424
|
.catch(() => {
|
|
@@ -3291,6 +3431,10 @@ voice.webGpu = {
|
|
|
3291
3431
|
fetchStatus()
|
|
3292
3432
|
}, [fetchStatus])
|
|
3293
3433
|
|
|
3434
|
+
const { sensevoiceState, installingSensevoice, triggerInstallSensevoice } = useSensevoiceInstaller(fetchStatus)
|
|
3435
|
+
|
|
3436
|
+
const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
|
|
3437
|
+
|
|
3294
3438
|
const { updaterStatus, updating, updaterMsg, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
3295
3439
|
|
|
3296
3440
|
const save = async () => {
|
|
@@ -3783,6 +3927,15 @@ voice.webGpu = {
|
|
|
3783
3927
|
isHotkeyRelease,
|
|
3784
3928
|
extractContextKeywords,
|
|
3785
3929
|
resolveBrowserRecognitionLang,
|
|
3930
|
+
VoiceSection,
|
|
3931
|
+
cutPhrase,
|
|
3932
|
+
sendAudio,
|
|
3933
|
+
modeChain,
|
|
3934
|
+
openMic,
|
|
3935
|
+
startRecording,
|
|
3936
|
+
cancelCurrent,
|
|
3937
|
+
stopCurrent,
|
|
3938
|
+
loadWebGpuWhisper,
|
|
3786
3939
|
}
|
|
3787
3940
|
return module.exports
|
|
3788
3941
|
},
|