Files
homeclaw/web-ui/voice-utils.js
T
kimandClaude Sonnet 4.6 75a36cdc7d v3.2.2: 폴더블 폰 모바일 패널 토글 전면 수정 + 탐정앱 네트워크 분석 + 음성 유틸
- mobToggle 가드 640→1024: 음악·슬라이드·코드·날씨·언어·치과·ESP32·아두이노 앱
  (CSS max-width:1024px인데 JS guard가 >640으로 남아 폴드 내부화면에서 토글 불가)
- #mob-tabbar z-index 400/490→600, 패널 bottom 0→52px, backdrop inset 0 0 52px 0
  (backdrop이 탭바를 가려 열린 상태에서 다른 탭 전환 불가 문제 해결)
- 모든 innerWidth<=640 자동트리거 →<=1024 통일 (9개 앱)
- 탐정앱: 네트워크 분석 패널 (IP조회/Traceroute/Nmap/DNS/WHOIS/SSL/취약점/해시/웹스캔/배너/CVE)
- voice-utils.js 분리 — 서브앱 공통 음성입력+TTS 유틸
- app.js: 컨텍스트 토큰 게이지 (세션 목록에 tok/max 표시), usage 이벤트 처리
- routes-music.ts: Demucs 보컬분리 + YouTube 다운로드 잡 관리 (TTL 자동정리)
- server-v2.ts: 탐정 네트워크 API (traceroute/dns/whois/ssl/nmap/vuln/hash/webscan/cve)
- session.ts: resolveNumCtx export, /api/agent/session contextTokenEstimate 노출
- config.json: 카카오 채널 설정, ITS 교통 API 키 vault 참조
- styles.css: app-mic-btn / app-tts-btn 공통 스타일

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 19:16:59 +09:00

84 lines
3.3 KiB
JavaScript

// Shared voice input + TTS for sub-apps. Requires authH() to be defined by the host page.
var _appSendFn = null;
var _isRecording = false;
var _speechRecog = null;
var _micAutosend = true;
var _ttsAudio = null;
var _ttsBtnActive = null;
function initAppVoice(sendFn) {
_appSendFn = sendFn || null;
}
function toggleMicRecording() {
if (_isRecording) stopMicRecording();
else startMicRecording();
}
function startMicRecording() {
var SpeechRecog = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecog) { alert('이 브라우저는 음성 인식을 지원하지 않습니다.\nChrome을 사용해주세요.'); return; }
_speechRecog = new SpeechRecog();
_speechRecog.lang = 'ko-KR';
_speechRecog.interimResults = false;
_speechRecog.maxAlternatives = 1;
_speechRecog.continuous = false;
var btn = document.getElementById('mic-btn');
_speechRecog.onstart = function() {
_isRecording = true;
if (btn) { btn.textContent = '⏹'; btn.title = '말하는 중… (클릭 시 중지)'; btn.classList.add('recording'); }
};
_speechRecog.onresult = function(e) {
var transcript = e.results[0][0].transcript;
var input = document.getElementById('chat-input');
if (input && transcript) {
input.value = (input.value ? input.value + ' ' : '') + transcript;
input.focus();
input.dispatchEvent(new Event('input'));
if (_micAutosend && _appSendFn) setTimeout(function() { _appSendFn(); }, 50);
}
};
_speechRecog.onerror = function(e) {
if (e.error !== 'no-speech') alert('음성 인식 오류: ' + e.error);
};
_speechRecog.onend = function() {
_isRecording = false;
_speechRecog = null;
if (btn) { btn.textContent = '🎤'; btn.title = '음성 입력'; btn.classList.remove('recording'); }
};
_speechRecog.start();
}
function stopMicRecording() {
if (_speechRecog) _speechRecog.stop();
}
function playTTSText(btn, text) {
text = (text || '').replace(/```[\s\S]*?```/g, '').replace(/[#*`_~>]/g, ' ').replace(/\s+/g, ' ').trim();
if (!text) return;
if (_ttsBtnActive === btn && _ttsAudio) {
_ttsAudio.pause(); _ttsAudio = null;
btn.textContent = '🔊'; btn.classList.remove('playing');
_ttsBtnActive = null; return;
}
if (_ttsAudio) { _ttsAudio.pause(); _ttsAudio = null; }
if (_ttsBtnActive) { _ttsBtnActive.textContent = '🔊'; _ttsBtnActive.classList.remove('playing'); }
btn.textContent = '⌛'; btn.classList.add('playing');
_ttsBtnActive = btn;
fetch('/api/voice/tts', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, typeof authH === 'function' ? authH() : {}),
credentials: 'include',
body: JSON.stringify({ text: text.slice(0, 2000) }),
})
.then(function(r) { if (!r.ok) throw new Error(r.status); return r.blob(); })
.then(function(blob) {
var url = URL.createObjectURL(blob);
_ttsAudio = new Audio(url);
_ttsAudio.onended = function() { URL.revokeObjectURL(url); _ttsAudio = null; btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; };
_ttsAudio.onerror = function() { URL.revokeObjectURL(url); _ttsAudio = null; btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; };
btn.textContent = '⏹'; _ttsAudio.play();
})
.catch(function() { btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; });
}