@yh-ui/hooks 0.1.10 → 0.1.15
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/dist/index.cjs +909 -12
- package/dist/index.d.cts +447 -7
- package/dist/index.d.mts +447 -7
- package/dist/index.d.ts +447 -7
- package/dist/index.mjs +897 -13
- package/dist/use-ai/index.cjs +60 -0
- package/dist/use-ai/index.d.ts +5 -0
- package/dist/use-ai/index.mjs +5 -0
- package/dist/use-ai/use-ai-chat.cjs +193 -0
- package/dist/use-ai/use-ai-chat.d.ts +115 -0
- package/dist/use-ai/use-ai-chat.mjs +182 -0
- package/dist/use-ai/use-ai-conversations.cjs +254 -0
- package/dist/use-ai/use-ai-conversations.d.ts +148 -0
- package/dist/use-ai/use-ai-conversations.mjs +241 -0
- package/dist/use-ai/use-ai-request.cjs +35 -0
- package/dist/use-ai/use-ai-request.d.ts +17 -0
- package/dist/use-ai/use-ai-request.mjs +29 -0
- package/dist/use-ai/use-ai-stream.cjs +223 -0
- package/dist/use-ai/use-ai-stream.d.ts +76 -0
- package/dist/use-ai/use-ai-stream.mjs +210 -0
- package/dist/use-ai/use-ai-voice.cjs +218 -0
- package/dist/use-ai/use-ai-voice.d.ts +83 -0
- package/dist/use-ai/use-ai-voice.mjs +215 -0
- package/dist/use-form-item/index.d.ts +1 -1
- package/dist/use-locale/dayjs-locale.cjs +19 -12
- package/dist/use-locale/dayjs-locale.d.ts +1 -8
- package/dist/use-locale/dayjs-locale.mjs +20 -12
- package/package.json +4 -3
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useAiVoice = useAiVoice;
|
|
7
|
+
var _vue = require("vue");
|
|
8
|
+
function useAiVoice(options = {}) {
|
|
9
|
+
const {
|
|
10
|
+
language = "zh-CN",
|
|
11
|
+
interimResults = true,
|
|
12
|
+
continuous = false,
|
|
13
|
+
vad = true,
|
|
14
|
+
vadThreshold = 2e3,
|
|
15
|
+
volumeThreshold = 0.05,
|
|
16
|
+
waveCount = 20,
|
|
17
|
+
useSTT = true
|
|
18
|
+
} = options;
|
|
19
|
+
const isRecording = (0, _vue.ref)(false);
|
|
20
|
+
const transcript = (0, _vue.ref)("");
|
|
21
|
+
const interimTranscript = (0, _vue.ref)("");
|
|
22
|
+
const volume = (0, _vue.ref)(0);
|
|
23
|
+
const amplitudes = (0, _vue.ref)(new Array(waveCount).fill(5));
|
|
24
|
+
const audioBlob = (0, _vue.ref)(null);
|
|
25
|
+
const recognition = (0, _vue.shallowRef)(null);
|
|
26
|
+
const audioContext = (0, _vue.shallowRef)(null);
|
|
27
|
+
const analyser = (0, _vue.shallowRef)(null);
|
|
28
|
+
const stream = (0, _vue.shallowRef)(null);
|
|
29
|
+
const mediaRecorder = (0, _vue.shallowRef)(null);
|
|
30
|
+
let chunks = [];
|
|
31
|
+
let animationId = null;
|
|
32
|
+
let silenceStart = null;
|
|
33
|
+
const _window = typeof window !== "undefined" ? window : null;
|
|
34
|
+
const SpeechRecognition = _window?.SpeechRecognition || _window?.webkitSpeechRecognition;
|
|
35
|
+
const sttSupported = !!SpeechRecognition;
|
|
36
|
+
const initMediaRecorder = mediaStream => {
|
|
37
|
+
chunks = [];
|
|
38
|
+
const recorder = new MediaRecorder(mediaStream);
|
|
39
|
+
recorder.ondataavailable = e => {
|
|
40
|
+
if (e.data.size > 0) chunks.push(e.data);
|
|
41
|
+
};
|
|
42
|
+
recorder.onstop = () => {
|
|
43
|
+
audioBlob.value = new Blob(chunks, {
|
|
44
|
+
type: "audio/webm"
|
|
45
|
+
});
|
|
46
|
+
if (isRecording.value === false && chunks.length > 0) {
|
|
47
|
+
options.onStop?.(transcript.value, audioBlob.value);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
mediaRecorder.value = recorder;
|
|
51
|
+
};
|
|
52
|
+
const initRecognition = () => {
|
|
53
|
+
if (!sttSupported || !useSTT) return;
|
|
54
|
+
const recognitionInstance = new SpeechRecognition();
|
|
55
|
+
recognitionInstance.lang = language;
|
|
56
|
+
recognitionInstance.interimResults = interimResults;
|
|
57
|
+
recognitionInstance.continuous = continuous;
|
|
58
|
+
recognitionInstance.onresult = event => {
|
|
59
|
+
let currentInterim = "";
|
|
60
|
+
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
|
61
|
+
if (event.results[i].isFinal) {
|
|
62
|
+
transcript.value += event.results[i][0].transcript;
|
|
63
|
+
options.onResult?.(transcript.value);
|
|
64
|
+
} else {
|
|
65
|
+
currentInterim += event.results[i][0].transcript;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
interimTranscript.value = currentInterim;
|
|
69
|
+
options.onPartialResult?.(currentInterim);
|
|
70
|
+
};
|
|
71
|
+
recognitionInstance.onerror = event => {
|
|
72
|
+
if (event.error !== "no-speech" && event.error !== "aborted") {
|
|
73
|
+
options.onError?.(event);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
recognition.value = recognitionInstance;
|
|
77
|
+
};
|
|
78
|
+
const initAudioAnalyzer = async mediaStream => {
|
|
79
|
+
try {
|
|
80
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
81
|
+
audioContext.value = new AudioCtx();
|
|
82
|
+
if (audioContext.value.state === "suspended") {
|
|
83
|
+
await audioContext.value.resume();
|
|
84
|
+
}
|
|
85
|
+
analyser.value = audioContext.value.createAnalyser();
|
|
86
|
+
analyser.value.fftSize = 256;
|
|
87
|
+
const source = audioContext.value.createMediaStreamSource(mediaStream);
|
|
88
|
+
source.connect(analyser.value);
|
|
89
|
+
const bufferLength = analyser.value.frequencyBinCount;
|
|
90
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
91
|
+
const process = () => {
|
|
92
|
+
if (!isRecording.value) {
|
|
93
|
+
amplitudes.value = new Array(waveCount).fill(5);
|
|
94
|
+
volume.value = 0;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
animationId = requestAnimationFrame(process);
|
|
98
|
+
analyser.value.getByteFrequencyData(dataArray);
|
|
99
|
+
let total = 0;
|
|
100
|
+
for (let i = 0; i < bufferLength; i++) total += dataArray[i];
|
|
101
|
+
const avg = total / bufferLength;
|
|
102
|
+
volume.value = Math.min(100, avg / 128 * 100);
|
|
103
|
+
const step = Math.floor(bufferLength / waveCount);
|
|
104
|
+
const newAmps = [];
|
|
105
|
+
for (let i = 0; i < waveCount; i++) {
|
|
106
|
+
const val = dataArray[i * step];
|
|
107
|
+
newAmps.push(6 + val / 255 * 34);
|
|
108
|
+
}
|
|
109
|
+
amplitudes.value = newAmps;
|
|
110
|
+
if (vad) {
|
|
111
|
+
const normalizedVol = avg / 255;
|
|
112
|
+
if (normalizedVol < volumeThreshold) {
|
|
113
|
+
if (silenceStart === null) silenceStart = Date.now();else if (Date.now() - silenceStart > vadThreshold) {
|
|
114
|
+
stop();
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
silenceStart = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
process();
|
|
122
|
+
} catch (err) {
|
|
123
|
+
options.onError?.(err);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const start = async () => {
|
|
127
|
+
if (isRecording.value) return;
|
|
128
|
+
try {
|
|
129
|
+
transcript.value = "";
|
|
130
|
+
interimTranscript.value = "";
|
|
131
|
+
audioBlob.value = null;
|
|
132
|
+
silenceStart = null;
|
|
133
|
+
stream.value = await navigator.mediaDevices.getUserMedia({
|
|
134
|
+
audio: true
|
|
135
|
+
});
|
|
136
|
+
isRecording.value = true;
|
|
137
|
+
initMediaRecorder(stream.value);
|
|
138
|
+
initRecognition();
|
|
139
|
+
await initAudioAnalyzer(stream.value);
|
|
140
|
+
mediaRecorder.value?.start(1e3);
|
|
141
|
+
recognition.value?.start();
|
|
142
|
+
options.onStart?.();
|
|
143
|
+
} catch (err) {
|
|
144
|
+
isRecording.value = false;
|
|
145
|
+
if (stream.value) {
|
|
146
|
+
stream.value.getTracks().forEach(t => t.stop());
|
|
147
|
+
stream.value = null;
|
|
148
|
+
}
|
|
149
|
+
console.error("[yh-ui/hooks] useAiVoice start failed:", err);
|
|
150
|
+
options.onError?.(err);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const stop = () => {
|
|
154
|
+
if (!isRecording.value) return;
|
|
155
|
+
isRecording.value = false;
|
|
156
|
+
if (stream.value) {
|
|
157
|
+
stream.value.getTracks().forEach(track => track.stop());
|
|
158
|
+
stream.value = null;
|
|
159
|
+
}
|
|
160
|
+
if (recognition.value) {
|
|
161
|
+
try {
|
|
162
|
+
recognition.value.stop();
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
if (mediaRecorder.value && mediaRecorder.value.state !== "inactive") {
|
|
166
|
+
try {
|
|
167
|
+
mediaRecorder.value.stop();
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
cleanup();
|
|
171
|
+
};
|
|
172
|
+
const cancel = () => {
|
|
173
|
+
if (!isRecording.value) return;
|
|
174
|
+
isRecording.value = false;
|
|
175
|
+
if (stream.value) {
|
|
176
|
+
stream.value.getTracks().forEach(track => track.stop());
|
|
177
|
+
stream.value = null;
|
|
178
|
+
}
|
|
179
|
+
if (recognition.value) {
|
|
180
|
+
try {
|
|
181
|
+
recognition.value.abort();
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
if (mediaRecorder.value && mediaRecorder.value.state !== "inactive") {
|
|
185
|
+
try {
|
|
186
|
+
mediaRecorder.value.stop();
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
cleanup();
|
|
190
|
+
};
|
|
191
|
+
const cleanup = () => {
|
|
192
|
+
if (animationId) {
|
|
193
|
+
cancelAnimationFrame(animationId);
|
|
194
|
+
animationId = null;
|
|
195
|
+
}
|
|
196
|
+
if (audioContext.value && audioContext.value.state !== "closed") {
|
|
197
|
+
audioContext.value.close().catch(_err => {});
|
|
198
|
+
audioContext.value = null;
|
|
199
|
+
}
|
|
200
|
+
amplitudes.value = new Array(waveCount).fill(5);
|
|
201
|
+
volume.value = 0;
|
|
202
|
+
};
|
|
203
|
+
(0, _vue.onUnmounted)(() => {
|
|
204
|
+
if (isRecording.value) stop();else cleanup();
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
isRecording,
|
|
208
|
+
transcript,
|
|
209
|
+
interimTranscript,
|
|
210
|
+
amplitudes,
|
|
211
|
+
volume,
|
|
212
|
+
audioBlob,
|
|
213
|
+
start,
|
|
214
|
+
stop,
|
|
215
|
+
cancel,
|
|
216
|
+
sttSupported
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface UseAiVoiceOptions {
|
|
2
|
+
/**
|
|
3
|
+
* 语言代码 (用于 SpeechRecognition)
|
|
4
|
+
* @default 'zh-CN'
|
|
5
|
+
*/
|
|
6
|
+
language?: string;
|
|
7
|
+
/**
|
|
8
|
+
* 是否需要临时结果(在说话过程中实时返回)
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
interimResults?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* 是否连续识别
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
continuous?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 智能静音检测(VAD)
|
|
19
|
+
* 开启后,当检测到长时间无声会自动停止录音
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
vad?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* 静音检测阈值 (ms)
|
|
25
|
+
* @default 2000
|
|
26
|
+
*/
|
|
27
|
+
vadThreshold?: number;
|
|
28
|
+
/**
|
|
29
|
+
* 音量变化敏感度 (0-1)
|
|
30
|
+
* @default 0.05
|
|
31
|
+
*/
|
|
32
|
+
volumeThreshold?: number;
|
|
33
|
+
/**
|
|
34
|
+
* 返回波形柱的数量(对应 AiVoiceTrigger 的 amplitudes)
|
|
35
|
+
* @default 20
|
|
36
|
+
*/
|
|
37
|
+
waveCount?: number;
|
|
38
|
+
/**
|
|
39
|
+
* 是否在开始时自动执行浏览器语音识别 (SpeechRecognition)
|
|
40
|
+
* 如果关闭,则只进行物理音频录制
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
useSTT?: boolean;
|
|
44
|
+
/** 回调事件 */
|
|
45
|
+
onStart?: () => void;
|
|
46
|
+
/** 停止回调,包含最终转写文本和录音文件 Blob */
|
|
47
|
+
onStop?: (transcript: string, blob: Blob | null) => void;
|
|
48
|
+
onResult?: (transcript: string) => void;
|
|
49
|
+
onPartialResult?: (transcript: string) => void;
|
|
50
|
+
onError?: (error: unknown) => void;
|
|
51
|
+
}
|
|
52
|
+
export interface UseAiVoiceReturn {
|
|
53
|
+
/** 是否正在录音 */
|
|
54
|
+
isRecording: import('vue').Ref<boolean>;
|
|
55
|
+
/** 最终转写文本 */
|
|
56
|
+
transcript: import('vue').Ref<string>;
|
|
57
|
+
/** 过程中的临时文本 */
|
|
58
|
+
interimTranscript: import('vue').Ref<string>;
|
|
59
|
+
/** 实时波形数据 */
|
|
60
|
+
amplitudes: import('vue').Ref<number[]>;
|
|
61
|
+
/** 实时音量 (0-100) */
|
|
62
|
+
volume: import('vue').Ref<number>;
|
|
63
|
+
/** 录音文件的 Blob */
|
|
64
|
+
audioBlob: import('vue').Ref<Blob | null>;
|
|
65
|
+
/** 开始录音 */
|
|
66
|
+
start: () => Promise<void>;
|
|
67
|
+
/** 停止录音 */
|
|
68
|
+
stop: () => void;
|
|
69
|
+
/** 取消并放弃当前结果 */
|
|
70
|
+
cancel: () => void;
|
|
71
|
+
/** 浏览器是否支持 SpeechRecognition (用于显示警告) */
|
|
72
|
+
sttSupported: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* useAiVoice - 专业级 AI 语音交互 Hook
|
|
76
|
+
*
|
|
77
|
+
* 核心能力:
|
|
78
|
+
* 1. 【音频录制】:通过 MediaRecorder 真实录制音频并导出 Blob 文件。
|
|
79
|
+
* 2. 【视觉分析】:通过 Web Audio API 实时输出驱动 AiVoiceTrigger 的波形数组。
|
|
80
|
+
* 3. 【智能 VAD】:多维检测静音状态,支持自动停顿结束。
|
|
81
|
+
* 4. 【语音转写】:内置 Web Speech API 实时转写及临时结果反馈。
|
|
82
|
+
*/
|
|
83
|
+
export declare function useAiVoice(options?: UseAiVoiceOptions): UseAiVoiceReturn;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { ref, onUnmounted, shallowRef } from "vue";
|
|
2
|
+
export function useAiVoice(options = {}) {
|
|
3
|
+
const {
|
|
4
|
+
language = "zh-CN",
|
|
5
|
+
interimResults = true,
|
|
6
|
+
continuous = false,
|
|
7
|
+
vad = true,
|
|
8
|
+
vadThreshold = 2e3,
|
|
9
|
+
volumeThreshold = 0.05,
|
|
10
|
+
waveCount = 20,
|
|
11
|
+
useSTT = true
|
|
12
|
+
} = options;
|
|
13
|
+
const isRecording = ref(false);
|
|
14
|
+
const transcript = ref("");
|
|
15
|
+
const interimTranscript = ref("");
|
|
16
|
+
const volume = ref(0);
|
|
17
|
+
const amplitudes = ref(new Array(waveCount).fill(5));
|
|
18
|
+
const audioBlob = ref(null);
|
|
19
|
+
const recognition = shallowRef(null);
|
|
20
|
+
const audioContext = shallowRef(null);
|
|
21
|
+
const analyser = shallowRef(null);
|
|
22
|
+
const stream = shallowRef(null);
|
|
23
|
+
const mediaRecorder = shallowRef(null);
|
|
24
|
+
let chunks = [];
|
|
25
|
+
let animationId = null;
|
|
26
|
+
let silenceStart = null;
|
|
27
|
+
const _window = typeof window !== "undefined" ? window : null;
|
|
28
|
+
const SpeechRecognition = _window?.SpeechRecognition || _window?.webkitSpeechRecognition;
|
|
29
|
+
const sttSupported = !!SpeechRecognition;
|
|
30
|
+
const initMediaRecorder = (mediaStream) => {
|
|
31
|
+
chunks = [];
|
|
32
|
+
const recorder = new MediaRecorder(mediaStream);
|
|
33
|
+
recorder.ondataavailable = (e) => {
|
|
34
|
+
if (e.data.size > 0) chunks.push(e.data);
|
|
35
|
+
};
|
|
36
|
+
recorder.onstop = () => {
|
|
37
|
+
audioBlob.value = new Blob(chunks, { type: "audio/webm" });
|
|
38
|
+
if (isRecording.value === false && chunks.length > 0) {
|
|
39
|
+
options.onStop?.(transcript.value, audioBlob.value);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
mediaRecorder.value = recorder;
|
|
43
|
+
};
|
|
44
|
+
const initRecognition = () => {
|
|
45
|
+
if (!sttSupported || !useSTT) return;
|
|
46
|
+
const recognitionInstance = new SpeechRecognition();
|
|
47
|
+
recognitionInstance.lang = language;
|
|
48
|
+
recognitionInstance.interimResults = interimResults;
|
|
49
|
+
recognitionInstance.continuous = continuous;
|
|
50
|
+
recognitionInstance.onresult = (event) => {
|
|
51
|
+
let currentInterim = "";
|
|
52
|
+
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
|
53
|
+
if (event.results[i].isFinal) {
|
|
54
|
+
transcript.value += event.results[i][0].transcript;
|
|
55
|
+
options.onResult?.(transcript.value);
|
|
56
|
+
} else {
|
|
57
|
+
currentInterim += event.results[i][0].transcript;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
interimTranscript.value = currentInterim;
|
|
61
|
+
options.onPartialResult?.(currentInterim);
|
|
62
|
+
};
|
|
63
|
+
recognitionInstance.onerror = (event) => {
|
|
64
|
+
if (event.error !== "no-speech" && event.error !== "aborted") {
|
|
65
|
+
options.onError?.(event);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
recognition.value = recognitionInstance;
|
|
69
|
+
};
|
|
70
|
+
const initAudioAnalyzer = async (mediaStream) => {
|
|
71
|
+
try {
|
|
72
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
73
|
+
audioContext.value = new AudioCtx();
|
|
74
|
+
if (audioContext.value.state === "suspended") {
|
|
75
|
+
await audioContext.value.resume();
|
|
76
|
+
}
|
|
77
|
+
analyser.value = audioContext.value.createAnalyser();
|
|
78
|
+
analyser.value.fftSize = 256;
|
|
79
|
+
const source = audioContext.value.createMediaStreamSource(mediaStream);
|
|
80
|
+
source.connect(analyser.value);
|
|
81
|
+
const bufferLength = analyser.value.frequencyBinCount;
|
|
82
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
83
|
+
const process = () => {
|
|
84
|
+
if (!isRecording.value) {
|
|
85
|
+
amplitudes.value = new Array(waveCount).fill(5);
|
|
86
|
+
volume.value = 0;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
animationId = requestAnimationFrame(process);
|
|
90
|
+
analyser.value.getByteFrequencyData(dataArray);
|
|
91
|
+
let total = 0;
|
|
92
|
+
for (let i = 0; i < bufferLength; i++) total += dataArray[i];
|
|
93
|
+
const avg = total / bufferLength;
|
|
94
|
+
volume.value = Math.min(100, avg / 128 * 100);
|
|
95
|
+
const step = Math.floor(bufferLength / waveCount);
|
|
96
|
+
const newAmps = [];
|
|
97
|
+
for (let i = 0; i < waveCount; i++) {
|
|
98
|
+
const val = dataArray[i * step];
|
|
99
|
+
newAmps.push(6 + val / 255 * 34);
|
|
100
|
+
}
|
|
101
|
+
amplitudes.value = newAmps;
|
|
102
|
+
if (vad) {
|
|
103
|
+
const normalizedVol = avg / 255;
|
|
104
|
+
if (normalizedVol < volumeThreshold) {
|
|
105
|
+
if (silenceStart === null) silenceStart = Date.now();
|
|
106
|
+
else if (Date.now() - silenceStart > vadThreshold) {
|
|
107
|
+
stop();
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
silenceStart = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
process();
|
|
115
|
+
} catch (err) {
|
|
116
|
+
options.onError?.(err);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const start = async () => {
|
|
120
|
+
if (isRecording.value) return;
|
|
121
|
+
try {
|
|
122
|
+
transcript.value = "";
|
|
123
|
+
interimTranscript.value = "";
|
|
124
|
+
audioBlob.value = null;
|
|
125
|
+
silenceStart = null;
|
|
126
|
+
stream.value = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
127
|
+
isRecording.value = true;
|
|
128
|
+
initMediaRecorder(stream.value);
|
|
129
|
+
initRecognition();
|
|
130
|
+
await initAudioAnalyzer(stream.value);
|
|
131
|
+
mediaRecorder.value?.start(1e3);
|
|
132
|
+
recognition.value?.start();
|
|
133
|
+
options.onStart?.();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
isRecording.value = false;
|
|
136
|
+
if (stream.value) {
|
|
137
|
+
stream.value.getTracks().forEach((t) => t.stop());
|
|
138
|
+
stream.value = null;
|
|
139
|
+
}
|
|
140
|
+
console.error("[yh-ui/hooks] useAiVoice start failed:", err);
|
|
141
|
+
options.onError?.(err);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const stop = () => {
|
|
145
|
+
if (!isRecording.value) return;
|
|
146
|
+
isRecording.value = false;
|
|
147
|
+
if (stream.value) {
|
|
148
|
+
stream.value.getTracks().forEach((track) => track.stop());
|
|
149
|
+
stream.value = null;
|
|
150
|
+
}
|
|
151
|
+
if (recognition.value) {
|
|
152
|
+
try {
|
|
153
|
+
recognition.value.stop();
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (mediaRecorder.value && mediaRecorder.value.state !== "inactive") {
|
|
158
|
+
try {
|
|
159
|
+
mediaRecorder.value.stop();
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
cleanup();
|
|
164
|
+
};
|
|
165
|
+
const cancel = () => {
|
|
166
|
+
if (!isRecording.value) return;
|
|
167
|
+
isRecording.value = false;
|
|
168
|
+
if (stream.value) {
|
|
169
|
+
stream.value.getTracks().forEach((track) => track.stop());
|
|
170
|
+
stream.value = null;
|
|
171
|
+
}
|
|
172
|
+
if (recognition.value) {
|
|
173
|
+
try {
|
|
174
|
+
recognition.value.abort();
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (mediaRecorder.value && mediaRecorder.value.state !== "inactive") {
|
|
179
|
+
try {
|
|
180
|
+
mediaRecorder.value.stop();
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
cleanup();
|
|
185
|
+
};
|
|
186
|
+
const cleanup = () => {
|
|
187
|
+
if (animationId) {
|
|
188
|
+
cancelAnimationFrame(animationId);
|
|
189
|
+
animationId = null;
|
|
190
|
+
}
|
|
191
|
+
if (audioContext.value && audioContext.value.state !== "closed") {
|
|
192
|
+
audioContext.value.close().catch((_err) => {
|
|
193
|
+
});
|
|
194
|
+
audioContext.value = null;
|
|
195
|
+
}
|
|
196
|
+
amplitudes.value = new Array(waveCount).fill(5);
|
|
197
|
+
volume.value = 0;
|
|
198
|
+
};
|
|
199
|
+
onUnmounted(() => {
|
|
200
|
+
if (isRecording.value) stop();
|
|
201
|
+
else cleanup();
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
isRecording,
|
|
205
|
+
transcript,
|
|
206
|
+
interimTranscript,
|
|
207
|
+
amplitudes,
|
|
208
|
+
volume,
|
|
209
|
+
audioBlob,
|
|
210
|
+
start,
|
|
211
|
+
stop,
|
|
212
|
+
cancel,
|
|
213
|
+
sttSupported
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -14,7 +14,7 @@ export interface FormContext {
|
|
|
14
14
|
layout?: string;
|
|
15
15
|
addField: (field: FormItemContext) => void;
|
|
16
16
|
removeField: (field: FormItemContext) => void;
|
|
17
|
-
themeOverrides?: Record<string,
|
|
17
|
+
themeOverrides?: Record<string, string | undefined>;
|
|
18
18
|
}
|
|
19
19
|
export interface FormItemContext {
|
|
20
20
|
prop: string;
|
|
@@ -6,20 +6,18 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.updateDayjsMonths = exports.setDayjsLocaleSync = exports.setDayjsLocale = exports.getDayjsLocale = void 0;
|
|
7
7
|
var _dayjs = _interopRequireWildcard(require("dayjs"));
|
|
8
8
|
require("dayjs/locale/en");
|
|
9
|
-
require("dayjs/locale/zh-cn");
|
|
10
|
-
require("dayjs/locale/zh-tw");
|
|
11
|
-
require("dayjs/locale/ja");
|
|
12
|
-
require("dayjs/locale/ko");
|
|
13
9
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
14
10
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
15
11
|
const dayjs = _dayjs.default || _dayjs;
|
|
16
|
-
const
|
|
12
|
+
const dayjsLocales = import.meta.glob(["../../../../node_modules/dayjs/locale/*.js", "!../../../../node_modules/dayjs/locale/en.js"], {
|
|
13
|
+
eager: false
|
|
14
|
+
});
|
|
15
|
+
const loadedLocales = /* @__PURE__ */new Set(["en"]);
|
|
17
16
|
const localeMapping = {
|
|
18
17
|
"zh-cn": "zh-cn",
|
|
19
18
|
"zh-tw": "zh-tw",
|
|
20
19
|
"zh-hk": "zh-hk",
|
|
21
20
|
"zh-mo": "zh-tw",
|
|
22
|
-
// 澳门使用繁体
|
|
23
21
|
en: "en",
|
|
24
22
|
ja: "ja",
|
|
25
23
|
ko: "ko",
|
|
@@ -94,12 +92,21 @@ const setDayjsLocale = async localeCode => {
|
|
|
94
92
|
dayjs.locale(dayjsLocale);
|
|
95
93
|
return;
|
|
96
94
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
if (dayjsLocale === "en") {
|
|
96
|
+
dayjs.locale("en");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const path = `../../../../node_modules/dayjs/locale/${dayjsLocale}.js`;
|
|
100
|
+
const loader = dayjsLocales[path];
|
|
101
|
+
if (loader) {
|
|
102
|
+
try {
|
|
103
|
+
await loader();
|
|
104
|
+
loadedLocales.add(dayjsLocale);
|
|
105
|
+
dayjs.locale(dayjsLocale);
|
|
106
|
+
} catch {
|
|
107
|
+
dayjs.locale("en");
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
103
110
|
dayjs.locale("en");
|
|
104
111
|
}
|
|
105
112
|
};
|
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
import 'dayjs/locale/en';
|
|
2
|
-
import 'dayjs/locale/zh-cn';
|
|
3
|
-
import 'dayjs/locale/zh-tw';
|
|
4
|
-
import 'dayjs/locale/ja';
|
|
5
|
-
import 'dayjs/locale/ko';
|
|
6
2
|
/**
|
|
7
3
|
* 获取 dayjs locale code
|
|
8
4
|
*/
|
|
9
5
|
export declare const getDayjsLocale: (localeCode: string) => string;
|
|
10
6
|
/**
|
|
11
7
|
* 动态加载并设置 dayjs locale
|
|
12
|
-
* 使用动态导入来按需加载,避免打包所有语言
|
|
13
8
|
*/
|
|
14
9
|
export declare const setDayjsLocale: (localeCode: string) => Promise<void>;
|
|
15
10
|
/**
|
|
16
|
-
* 同步设置 dayjs locale
|
|
17
|
-
* 用于需要立即同步的场景
|
|
11
|
+
* 同步设置 dayjs locale(立即生效,异步加载后会更新)
|
|
18
12
|
*/
|
|
19
13
|
export declare const setDayjsLocaleSync: (localeCode: string) => void;
|
|
20
14
|
/**
|
|
21
15
|
* 使用自定义月份名称更新 dayjs locale
|
|
22
|
-
* 这样可以确保 dayjs 使用我们语言包中定义的月份名称
|
|
23
16
|
*/
|
|
24
17
|
export declare const updateDayjsMonths: (localeCode: string, months: {
|
|
25
18
|
jan: string;
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import * as _dayjs from "dayjs";
|
|
2
2
|
const dayjs = _dayjs.default || _dayjs;
|
|
3
3
|
import "dayjs/locale/en";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const loadedLocales = /* @__PURE__ */ new Set(["en"
|
|
4
|
+
const dayjsLocales = import.meta.glob(
|
|
5
|
+
["../../../../node_modules/dayjs/locale/*.js", "!../../../../node_modules/dayjs/locale/en.js"],
|
|
6
|
+
{ eager: false }
|
|
7
|
+
);
|
|
8
|
+
const loadedLocales = /* @__PURE__ */ new Set(["en"]);
|
|
9
9
|
const localeMapping = {
|
|
10
10
|
"zh-cn": "zh-cn",
|
|
11
11
|
"zh-tw": "zh-tw",
|
|
12
12
|
"zh-hk": "zh-hk",
|
|
13
13
|
"zh-mo": "zh-tw",
|
|
14
|
-
// 澳门使用繁体
|
|
15
14
|
en: "en",
|
|
16
15
|
ja: "ja",
|
|
17
16
|
ko: "ko",
|
|
@@ -85,12 +84,21 @@ export const setDayjsLocale = async (localeCode) => {
|
|
|
85
84
|
dayjs.locale(dayjsLocale);
|
|
86
85
|
return;
|
|
87
86
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
if (dayjsLocale === "en") {
|
|
88
|
+
dayjs.locale("en");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const path = `../../../../node_modules/dayjs/locale/${dayjsLocale}.js`;
|
|
92
|
+
const loader = dayjsLocales[path];
|
|
93
|
+
if (loader) {
|
|
94
|
+
try {
|
|
95
|
+
await loader();
|
|
96
|
+
loadedLocales.add(dayjsLocale);
|
|
97
|
+
dayjs.locale(dayjsLocale);
|
|
98
|
+
} catch {
|
|
99
|
+
dayjs.locale("en");
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
94
102
|
dayjs.locale("en");
|
|
95
103
|
}
|
|
96
104
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yh-ui/hooks",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "YH-UI composition hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
"dist"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
"@yh-ui/locale": "0.1.
|
|
26
|
+
"dayjs": "^1.11.19",
|
|
27
|
+
"@yh-ui/locale": "0.1.15",
|
|
28
|
+
"@yh-ui/utils": "0.1.15"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"vue": "^3.5.27",
|