@wendongfly/myhi 1.3.55 → 1.3.57

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/chat.html CHANGED
@@ -2953,159 +2953,94 @@
2953
2953
  // ── 语音输入 ──────────────────────────────────
2954
2954
  let recognition = null;
2955
2955
  let isRecording = false;
2956
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
2957
- const VOICE_FALLBACK_KEY = 'myhi_voice_fallback';
2958
-
2959
- // 检测语音支持级别
2960
- function detectVoiceSupport() {
2961
- if (!SpeechRecognition) return 'none';
2962
- const ua = navigator.userAgent;
2963
- // iOS (iPhone/iPad/iPod) 走 Apple Siri,中国可用
2964
- if (/iPhone|iPad|iPod/i.test(ua)) return 'native';
2965
- // macOS Safari 也走 Siri
2966
- if (/Macintosh.*Safari/i.test(ua) && !/Chrome/i.test(ua)) return 'native';
2967
- // 其他有 API 的浏览器(Android Chrome、国产浏览器等)依赖 Google,不可靠
2968
- return 'unreliable';
2956
+ // ── 语音输入(MediaRecorder SenseVoice 服务端 ASR)──────────────
2957
+ let _mediaRecorder = null;
2958
+ let _audioChunks = [];
2959
+ let _voiceActive = false;
2960
+ const _voiceBtn = document.getElementById('voice-btn');
2961
+ const _isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches;
2962
+
2963
+ function _voicePlaceholder(text) {
2964
+ cmdInput.placeholder = text || (currentSession?.mode === 'agent' ? '输入消息...' : '输入命令...');
2969
2965
  }
2970
2966
 
2971
- const _voiceSupport = detectVoiceSupport();
2972
-
2973
- function showVoiceFallbackTip() {
2974
- const ua = navigator.userAgent;
2975
- if (/Firefox/i.test(ua)) {
2976
- addStatusMessage('Firefox 不支持语音识别,请使用 Chrome/Safari 或键盘上的 🎤 语音输入');
2977
- } else if (/Android/i.test(ua)) {
2978
- addStatusMessage('语音识别不可用,请点击键盘上的 🎤 麦克风图标使用输入法语音');
2979
- } else {
2980
- addStatusMessage('语音识别不可用,请使用输入法的语音功能(键盘上的 🎤)');
2981
- }
2982
- cmdInput.focus();
2983
- }
2984
-
2985
- window.toggleVoice = function() {
2986
- // 完全不支持
2987
- if (_voiceSupport === 'none') {
2988
- showVoiceFallbackTip();
2989
- return;
2990
- }
2991
-
2992
- // 非 HTTPS 且非 localhost 时,语音识别无法工作
2993
- if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
2994
- const httpsUrl = location.href.replace(/^http:/, 'https:').replace(':' + location.port, ':3443');
2995
- addStatusMessage('语音识别需要 HTTPS,请通过 HTTPS 地址访问');
2996
- return;
2997
- }
2998
-
2999
- // 之前失败过,直接显示回退提示(避免反复失败)
3000
- if (_voiceSupport === 'unreliable' && localStorage.getItem(VOICE_FALLBACK_KEY)) {
3001
- showVoiceFallbackTip();
3002
- addStatusMessage('(长按语音按钮可重试语音识别)');
2967
+ async function _startVoice() {
2968
+ if (_voiceActive) { _stopVoice(); return; }
2969
+ if (!navigator.mediaDevices?.getUserMedia) {
2970
+ addStatusMessage('浏览器不支持麦克风录音(需要 HTTPS)');
3003
2971
  return;
3004
2972
  }
3005
-
3006
- if (isRecording) {
3007
- recognition.stop();
2973
+ let stream;
2974
+ try {
2975
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
2976
+ } catch (e) {
2977
+ addStatusMessage(e.name === 'NotAllowedError'
2978
+ ? '麦克风权限被拒绝,请点击地址栏 🔒 → 允许麦克风'
2979
+ : '麦克风访问失败: ' + e.message);
3008
2980
  return;
3009
2981
  }
3010
-
3011
- recognition = new SpeechRecognition();
3012
- recognition.lang = 'zh-CN';
3013
- recognition.interimResults = true;
3014
- recognition.continuous = false;
3015
- recognition.maxAlternatives = 1;
3016
-
3017
- const voiceBtn = document.getElementById('voice-btn');
3018
- const savedValue = cmdInput.value;
3019
- let hasResult = false;
3020
- let gotAudio = false;
3021
- let errorOccurred = false;
3022
- let networkTimeout = null;
3023
-
3024
- function stopRecording() {
3025
- isRecording = false;
3026
- clearTimeout(networkTimeout);
3027
- voiceBtn.classList.remove('recording');
3028
- cmdInput.placeholder = currentSession?.mode === 'agent' ? '输入消息...' : '输入命令...';
3029
- }
3030
-
3031
- recognition.onstart = () => {
3032
- isRecording = true;
3033
- hasResult = false;
3034
- gotAudio = false;
3035
- errorOccurred = false;
3036
- voiceBtn.classList.add('recording');
3037
- cmdInput.placeholder = '正在听...';
3038
- // 不可靠环境下设置超时:3秒内没结果视为失败
3039
- if (_voiceSupport === 'unreliable') {
3040
- networkTimeout = setTimeout(() => {
3041
- if (!hasResult && !errorOccurred) {
3042
- recognition.abort();
3043
- localStorage.setItem(VOICE_FALLBACK_KEY, '1');
3044
- stopRecording();
3045
- addStatusMessage('语音服务连接超时,请使用输入法的语音功能(键盘上的 🎤)');
3046
- }
3047
- }, 3000);
3048
- }
3049
- };
3050
-
3051
- recognition.onaudiostart = () => { gotAudio = true; };
3052
-
3053
- recognition.onresult = (e) => {
3054
- hasResult = true;
3055
- clearTimeout(networkTimeout);
3056
- let interim = '', final = '';
3057
- for (let i = e.resultIndex; i < e.results.length; i++) {
3058
- if (e.results[i].isFinal) final += e.results[i][0].transcript;
3059
- else interim += e.results[i][0].transcript;
3060
- }
3061
- cmdInput.value = savedValue + final + interim;
3062
- };
3063
-
3064
- recognition.onend = () => {
3065
- stopRecording();
3066
- if (!errorOccurred && !hasResult) {
3067
- if (!gotAudio) {
3068
- addStatusMessage('未检测到麦克风音频,请检查麦克风是否正常');
3069
- } else {
3070
- addStatusMessage('未识别到语音内容,请重试');
3071
- }
3072
- }
3073
- cmdInput.focus();
2982
+ _audioChunks = [];
2983
+ _mediaRecorder = new MediaRecorder(stream);
2984
+ _mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) _audioChunks.push(e.data); };
2985
+ _mediaRecorder.onstop = async () => {
2986
+ stream.getTracks().forEach(t => t.stop());
2987
+ const blob = new Blob(_audioChunks, { type: _mediaRecorder.mimeType || 'audio/webm' });
2988
+ await _transcribe(blob);
3074
2989
  };
2990
+ _mediaRecorder.start();
2991
+ _voiceActive = true;
2992
+ _voiceBtn.classList.add('recording');
2993
+ _voicePlaceholder(_isTouchDevice() ? '录音中... 松手停止' : '录音中... 再次点击停止');
2994
+ }
3075
2995
 
3076
- recognition.onerror = (e) => {
3077
- errorOccurred = true;
3078
- console.warn('[语音识别错误]', e.error, e.message);
3079
- stopRecording();
3080
- if (e.error === 'network' || e.error === 'service-not-allowed') {
3081
- // 记住失败状态,下次直接显示回退提示
3082
- localStorage.setItem(VOICE_FALLBACK_KEY, '1');
3083
- addStatusMessage('语音服务连接失败,请使用输入法的语音功能(键盘上的 🎤)');
3084
- } else if (e.error === 'not-allowed') {
3085
- addStatusMessage('麦克风权限被拒绝,请在浏览器设置中允许');
3086
- } else if (e.error === 'no-speech') {
3087
- addStatusMessage('未检测到语音,请对着麦克风说话后重试');
3088
- } else if (e.error === 'audio-capture') {
3089
- addStatusMessage('无法捕获音频,请检查麦克风设备');
3090
- } else if (e.error !== 'aborted') {
3091
- addStatusMessage('语音识别失败: ' + e.error);
3092
- }
3093
- };
2996
+ function _stopVoice() {
2997
+ if (!_voiceActive || !_mediaRecorder) return;
2998
+ _voiceActive = false;
2999
+ _voiceBtn.classList.remove('recording');
3000
+ _voicePlaceholder('识别中...');
3001
+ _mediaRecorder.stop();
3002
+ }
3094
3003
 
3004
+ async function _transcribe(blob) {
3095
3005
  try {
3096
- recognition.start();
3006
+ const form = new FormData();
3007
+ form.append('audio', blob, 'audio.webm');
3008
+ const resp = await fetch(`/api/voice-transcribe?sessionId=${SESSION_ID}`, { method: 'POST', body: form });
3009
+ const data = await resp.json();
3010
+ if (resp.status === 503) {
3011
+ addStatusMessage('ASR 服务未配置(需在 myhi server 设置 ASR_URL 环境变量)');
3012
+ } else if (data.text) {
3013
+ cmdInput.value = (cmdInput.value ? cmdInput.value + ' ' : '') + data.text;
3014
+ cmdInput.focus();
3015
+ } else {
3016
+ addStatusMessage('未识别到语音内容,请重试');
3017
+ }
3097
3018
  } catch (e) {
3098
- stopRecording();
3099
- addStatusMessage('语音识别启动失败: ' + e.message);
3019
+ addStatusMessage('语音上传失败: ' + e.message);
3020
+ } finally {
3021
+ _voicePlaceholder();
3022
+ cmdInput.focus();
3100
3023
  }
3101
- };
3024
+ }
3102
3025
 
3103
- // 长按语音按钮重置回退状态,允许重试
3104
- document.getElementById('voice-btn').addEventListener('contextmenu', (e) => {
3026
+ // 桌面端:点击切换;触屏:按住录音,松手发送
3027
+ _voiceBtn.addEventListener('click', (e) => {
3028
+ if (_isTouchDevice()) return; // 触屏由 pointer 事件处理
3029
+ _startVoice();
3030
+ });
3031
+ _voiceBtn.addEventListener('pointerdown', (e) => {
3032
+ if (!_isTouchDevice()) return;
3105
3033
  e.preventDefault();
3106
- localStorage.removeItem(VOICE_FALLBACK_KEY);
3107
- addStatusMessage('语音识别已重置,点击 🎤 重试');
3034
+ _startVoice();
3108
3035
  });
3036
+ _voiceBtn.addEventListener('pointerup', (e) => {
3037
+ if (!_isTouchDevice() || !_voiceActive) return;
3038
+ e.preventDefault();
3039
+ _stopVoice();
3040
+ });
3041
+ _voiceBtn.addEventListener('pointercancel', () => { if (_voiceActive) _stopVoice(); });
3042
+
3043
+ window.toggleVoice = _startVoice; // 保留 onclick 兼容
3109
3044
 
3110
3045
  cmdInput.focus();
3111
3046