UNPKG

voice-agent

Version:

A Vue.js voice agent plugin for real-time voice communication via WebSocket

1,206 lines (1,205 loc) 83.3 kB
// Auto-injected CSS styles for voice-agent (function() { if (typeof document !== 'undefined') { var styleId = 'voice-agent-styles-1760598088148'; var existingStyle = document.querySelector('style[data-voice-agent]'); if (!existingStyle) { var style = document.createElement('style'); style.setAttribute('data-voice-agent', 'true'); style.textContent = ".loading-panel{display:flex;flex-direction:column;align-items:center;gap:20px;padding:40px;background:#fff;border-radius:16px;box-shadow:0 4px 20px #0000001a}.spinner{width:40px;height:40px;border:4px solid rgba(52,152,219,.3);border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite}.init-step{font-size:16px;color:#34495e;text-align:center;font-weight:500}.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#000c;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);display:flex;justify-content:center;align-items:center;z-index:1000;animation:fadeIn .3s ease}.modal-content{background:#fff;overflow:hidden;width:100vw;height:100vh;box-shadow:0 10px 30px #0000004d;animation:slideUp .3s ease;display:flex;flex-direction:column}@media (min-width: 801px){.modal-overlay{justify-content:flex-end;background:#00000080}.modal-content{width:500px;height:100vh;border-radius:0;box-shadow:-5px 0 20px #0000004d;animation:slideInRight .3s ease}}@keyframes slideInRight{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}.modal-header{background:#4f008c;color:#fff;padding:50px 16px 8px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 10px #0000001a;flex-shrink:0;height:100px}.header-left{display:flex;align-items:center;gap:20px}.header-right{display:flex;align-items:center;gap:12px}.phone-selector{position:relative;display:flex;align-items:center}.phone-select{background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;padding:8px 12px;font-size:14px;cursor:pointer;outline:none;transition:all .3s ease}.phone-select:hover{background:#fff3}.phone-select option{background:#34495e;color:#fff}.phone-select-trigger{background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;padding:12px 16px;font-size:14px;cursor:pointer;outline:none;transition:all .3s ease;display:flex;align-items:center;justify-content:space-between;gap:12px;-webkit-user-select:none;user-select:none}.phone-select-trigger:hover{background:#fff3;border-color:#ffffff4d}.phone-select-trigger:active{transform:scale(.98)}.selected-phone{flex:1;text-align:left;font-weight:500}.dropdown-arrow{transition:transform .3s ease;opacity:.8;flex-shrink:0}.dropdown-arrow.rotate{transform:rotate(180deg)}.phone-dropdown{position:absolute;top:calc(100% + 8px);left:0;right:0;background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:12px;box-shadow:0 8px 25px #00000026;z-index:1000;overflow:hidden;animation:dropdownSlide .2s ease;max-height:300px;overflow-y:auto}.phone-option{padding:0 10px;color:#2c3e50;font-size:14px;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(0,0,0,.05);min-height:48px}.phone-option:last-child{border-bottom:none}.phone-option:hover{background:#f8f9fa}.phone-option:active{background:#e9ecef;transform:scale(.98)}.phone-option.selected{background:#e3f2fd;color:#1976d2;font-weight:600}.phone-option.selected:hover{background:#e3f2fd}.check-icon{color:#1976d2;opacity:.8;flex-shrink:0}.current-status{display:flex;align-items:center;gap:8px;font-size:13px}.status-dot{width:8px;height:8px;border-radius:50%;background:#95a5a6;transition:all .3s ease}.status-dot.connected{background:#27ae60;box-shadow:0 0 10px #27ae6080}.status-dot.recording{background:#e74c3c;animation:pulse 1.5s infinite;box-shadow:0 0 10px #e74c3c80}.status-dot.disconnected{background:#95a5a6}.status-text{font-weight:500}.header-clear-btn{background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;padding:6px;cursor:pointer;transition:all .3s ease;display:flex;align-items:center;justify-content:center}.header-clear-btn:hover{background:#fff3;border-color:#ffffff4d;transform:scale(1.05)}.close-btn{background:#ffffff1a;border:none;border-radius:8px;color:#fff;padding:8px;cursor:pointer;transition:all .3s ease;display:flex;align-items:center;justify-content:center}.close-btn:hover{background:#fff3}.chat-container{flex:1;display:flex;flex-direction:column;overflow:hidden;background:#f8f9fa;height:calc(100vh - 180px)}.audio-visualizer{display:flex;flex-direction:column;align-items:center;justify-content:center;padding-top:20px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff}.wave-container{display:flex;align-items:center;gap:4px;margin-bottom:20px}.wave{width:4px;height:20px;background:#fff;border-radius:2px;animation:wave 1.2s ease-in-out infinite}.recording-text{margin:0;font-size:16px;font-weight:500;opacity:.9}.conversation-area{flex:1;display:flex;flex-direction:column;overflow:hidden}.conversation-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px}.message-item{display:flex;max-width:80%}.message-item.user{align-self:flex-end}.message-item.assistant{align-self:flex-start}.message-bubble{background:#fff;border-radius:18px;padding:12px 16px;box-shadow:0 2px 8px #0000001a;position:relative;border:1px solid #e1e8ed}.message-item.user .message-bubble{background:#007bff;color:#fff;border-color:#007bff}.message-item.assistant .message-bubble{background:#fff;color:#2c3e50}.message-text{line-height:1.4;word-wrap:break-word}.loading-dots{display:inline-flex;gap:4px}.loading-dots span{width:6px;height:6px;border-radius:50%;background:#bdc3c7;animation:loadingDots 1.4s infinite ease-in-out}.loading-dots span:nth-child(1){animation-delay:-.32s}.loading-dots span:nth-child(2){animation-delay:-.16s}.loading-dots span:nth-child(3){animation-delay:0s}.audio-controls{margin-top:8px;display:flex;align-items:center;gap:8px}.play-audio-btn{background:#f1f3f4;border:none;border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .3s ease;color:#5f6368}.play-audio-btn:hover{background:#e8eaed;transform:scale(1.1)}.play-audio-btn.playing{background:#34a853;color:#fff}.message-time{font-size:11px;color:#8e9aaf;margin-top:4px;text-align:right}.message-item.user .message-time{color:#ffffffb3}.empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:#8e9aaf;padding:40px 20px}.empty-icon{font-size:48px;margin-bottom:16px;opacity:.6}.empty-state p{margin:8px 0;font-size:16px}.empty-hint{font-size:14px!important;opacity:.7}.call-controls{background:#fff;padding:10px 10px 30px;border-top:1px solid #e1e8ed;display:flex;justify-content:center;align-items:center;gap:16px;flex-shrink:0;height:80px;position:relative;z-index:20}.call-controls.video-mode{background:#0000004d;border-top:1px solid rgba(255,255,255,.2);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.call-btn{display:flex;align-items:center;border:none;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s ease;width:40px;height:40px;border-radius:50%;justify-content:center}.call-btn.mic-off-btn{background:#6c757d;color:#fff;box-shadow:0 2px 8px #6c757d4d}.call-btn.mic-on-btn{background:#28a745;color:#fff;box-shadow:0 2px 8px #28a7454d}.call-btn.reconnect-btn{background:#6c757d;color:#fff;box-shadow:0 2px 8px #6c757d4d}.call-btn.hangup-btn{background:#dc3545;color:#fff;box-shadow:0 2px 8px #dc35454d}.call-btn.video-off-btn{background:#6c757d;color:#fff;box-shadow:0 2px 8px #6c757d4d}.call-btn.video-on-btn{background:#28a745;color:#fff;box-shadow:0 2px 8px #28a7454d}.video-container{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;background:#000;border-radius:0;overflow:hidden;z-index:10}.video-container.video-hidden{display:none}.video-display{width:100%;height:100%;object-fit:cover;display:block}.video-status{position:absolute;top:20px;left:20px;background:#000000b3;padding:8px 16px;border-radius:20px;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);z-index:11;display:flex;align-items:center;gap:8px}.video-indicator{color:#e74c3c;font-size:14px;font-weight:500;display:flex;align-items:center;gap:6px}.content-area{flex:1;display:flex;flex-direction:column;overflow:hidden}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideUp{0%{opacity:0;transform:translateY(50px)}to{opacity:1;transform:translateY(0)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes wave{0%,40%,to{transform:scaleY(.4)}20%{transform:scaleY(1)}}@keyframes loadingDots{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.typing-indicator{animation:blink 1s infinite;color:#3498db;font-weight:700}@keyframes blink{0%,50%{opacity:1}51%,to{opacity:0}}@keyframes dropdownSlide{0%{opacity:0;transform:translateY(-10px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}\n"; document.head.appendChild(style); } } })(); var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { computed, ref, onMounted, onUnmounted, createElementBlock, createCommentVNode, openBlock, createElementVNode, toDisplayString, withModifiers, withDirectives, Fragment, renderList, normalizeClass, vShow, normalizeStyle, createTextVNode, nextTick, createApp } from "vue"; const _imports_0 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='%23fff'%20viewBox='0%200%2016%2016'%3e%3cpath%20d='M5.5%205.5A.5.5%200%200%201%206%206v6a.5.5%200%200%201-1%200V6a.5.5%200%200%201%20.5-.5zm2.5%200a.5.5%200%200%201%20.5.5v6a.5.5%200%200%201-1%200V6a.5.5%200%200%201%20.5-.5zm3%20.5a.5.5%200%200%200-1%200v6a.5.5%200%200%200%201%200V6a.5.5%200%200%200-.5-.5z'/%3e%3cpath%20fill-rule='evenodd'%20d='M14.5%203a1%201%200%200%201-1%201H13v9a2%202%200%200%201-2%202H5a2%202%200%200%201-2-2V4h-.5a1%201%200%200%201-1-1V2a1%201%200%200%201%201-1H6a1%201%200%200%201%201-1h2a1%201%200%200%201%201%201h3.5a1%201%200%200%201%201%201v1zM4.118%204%204%204.059V13a1%201%200%200%200%201%201h6a1%201%200%200%200%201-1V4.059L11.882%204H4.118zM2.5%203V2h11v1h-11z'/%3e%3c/svg%3e"; const _imports_1 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='20'%20height='20'%20viewBox='0%200%2024%2024'%20fill='%23ffffff'%3e%3cpath%20d='M18%206L6%2018M6%206l12%2012'%20stroke='%23ffffff'%20stroke-width='2'%20stroke-linecap='round'/%3e%3c/svg%3e"; const _imports_2 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23EA3323'%3e%3cpath%20d='M24%2024H0V0h24v24z'%20fill='none'/%3e%3ccircle%20cx='12'%20cy='12'%20r='8'/%3e%3c/svg%3e"; const _imports_3 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0zm0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M19%2011h-1.7c0%20.74-.16%201.43-.43%202.05l1.23%201.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9%203.34%209%205v.18l5.98%205.99zM4.27%203L3%204.27l6.01%206.01V11c0%201.66%201.33%203%202.99%203%20.22%200%20.44-.03.65-.08l1.66%201.66c-.71.33-1.5.52-2.31.52-2.76%200-5.3-2.1-5.3-5.1H5c0%203.41%202.72%206.23%206%206.72V21h2v-3.28c.91-.13%201.77-.45%202.54-.9L19.73%2021%2021%2019.73%204.27%203z'/%3e%3c/svg%3e"; const _imports_4 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M12%2015c1.66%200%202.99-1.34%202.99-3L15%206c0-1.66-1.34-3-3-3S9%204.34%209%206v6c0%201.66%201.34%203%203%203zm5.3-3c0%203-2.54%205.1-5.3%205.1S6.7%2015%206.7%2012H5c0%203.42%202.72%206.23%206%206.72V22h2v-3.28c3.28-.48%206-3.3%206-6.72h-1.7z'/%3e%3c/svg%3e"; const _imports_5 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0zm0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M21%206.5l-4%204V7c0-.55-.45-1-1-1H9.82L21%2017.18V6.5zM3.27%202L2%203.27%204.73%206H4c-.55%200-1%20.45-1%201v10c0%20.55.45%201%201%201h12c.21%200%20.39-.08.54-.18L19.73%2021%2021%2019.73%203.27%202z'/%3e%3c/svg%3e"; const _imports_6 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M17%2010.5V7c0-.55-.45-1-1-1H4c-.55%200-1%20.45-1%201v10c0%20.55.45%201%201%201h12c.55%200%201-.45%201-1v-3.5l4%204v-11l-4%204z'/%3e%3c/svg%3e"; const _imports_7 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23ffffff'%3e%3cpath%20d='M0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M6.62%2010.79c1.44%202.83%203.76%205.14%206.59%206.59l2.2-2.2c.27-.27.67-.36%201.02-.24%201.12.37%202.33.57%203.57.57.55%200%201%20.45%201%201V20c0%20.55-.45%201-1%201-9.39%200-17-7.61-17-17%200-.55.45-1%201-1h3.5c.55%200%201%20.45%201%201%200%201.25.2%202.45.57%203.57.11.35.03.74-.25%201.02l-2.2%202.2z'/%3e%3c/svg%3e"; const _imports_8 = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20height='24px'%20viewBox='0%200%2024%2024'%20width='24px'%20fill='%23fff'%3e%3cpath%20d='M0%200h24v24H0z'%20fill='none'/%3e%3cpath%20d='M17.34%2014.54l-1.43-1.43c.56-.73%201.05-1.5%201.47-2.32l-2.2-2.2c-.28-.28-.36-.67-.25-1.02.37-1.12.57-2.32.57-3.57%200-.55.45-1%201-1H20c.55%200%201%20.45%201%201%200%203.98-1.37%207.64-3.66%2010.54zm-2.82%202.81C11.63%2019.64%207.97%2021%204%2021c-.55%200-1-.45-1-1v-3.49c0-.55.45-1%201-1%201.24%200%202.45-.2%203.57-.57.35-.12.75-.03%201.02.24l2.2%202.2c.81-.42%201.58-.9%202.3-1.46L1.39%204.22l1.42-1.41L21.19%2021.2l-1.41%201.41-5.26-5.26z'/%3e%3c/svg%3e"; const DEFAULT_CONFIG = { // WebSocket configuration websocket: { url: "", token: "", reconnect: false, reconnectInterval: 5e3, maxReconnectAttempts: 3 }, // Audio configuration audio: { sampleRate: 16e3, channelCount: 1, bitsPerSample: 16, chunkDuration: 1e3, // milliseconds echoCancellation: true, noiseSuppression: true, autoGainControl: true }, // Video configuration video: { enabled: false, frameRate: 2, quality: 0.8, facingMode: "environment" }, // User information configuration user: { phoneId: "", phoneList: [], // List of available phone numbers sessionId: "" }, // UI configuration ui: { showLogs: true, showVisualizer: true, logHeight: 300, theme: "default" // default, dark, light }, // Message configuration messages: { startSession: "AI agent start session", connecting: "Connecting...", connected: "Connected", disconnected: "Disconnected", recording: "Recording", stopped: "Stopped", waiting: "Waiting" }, // Callback functions callbacks: { onConnect: void 0, // Connection established callback onDisconnect: void 0, // Connection lost callback onError: void 0, // Error callback onMessage: void 0, // Message received callback onTranscript: void 0, // Transcription completed callback onAudioOutput: void 0 // Audio output callback } }; function mergeConfig(userConfig = {}) { return deepMerge(DEFAULT_CONFIG, userConfig); } function deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) { result[key] = deepMerge(target[key] || {}, source[key]); } else { result[key] = source[key]; } } return result; } function validateConfig(config) { const errors = []; if (!config.websocket.url) { errors.push("WebSocket URL is required"); } if (!config.websocket.token) { errors.push("WebSocket token is required"); } if (config.audio.sampleRate && config.audio.sampleRate <= 0) { errors.push("Audio sample rate must be greater than 0"); } if (config.audio.channelCount && config.audio.channelCount <= 0) { errors.push("Audio channel count must be greater than 0"); } if (config.audio.chunkDuration && config.audio.chunkDuration <= 0) { errors.push("Audio chunk duration must be greater than 0"); } if (!config.user.phoneId && (!config.user.phoneList || config.user.phoneList.length === 0)) { errors.push("Either phoneId or phoneList must be provided"); } return errors; } class AudioQueueManager { constructor() { __publicField(this, "audioQueue", []); // 音频片段队列 __publicField(this, "isPlaying", false); // 是否正在播放 __publicField(this, "isPaused", false); // 是否暂停(用于语音打断) __publicField(this, "currentMessage", null); // 当前关联的消息 __publicField(this, "audioContext", null); // 音频上下文 __publicField(this, "gainNode", null); // 音量控制节点 __publicField(this, "playbackStartTime", 0); // 播放开始时间 __publicField(this, "scheduledBuffers", []); // 已调度的音频缓冲区 __publicField(this, "nextStartTime", 0); // 下一个音频片段的开始时间 __publicField(this, "sampleRate", 22050); // 采样率 __publicField(this, "processingQueue", false); } // 是否正在处理队列 /** * 初始化音频上下文 */ async initAudioContext() { if (!this.audioContext) { const AudioContextClass = window.AudioContext || window.webkitAudioContext; this.audioContext = new AudioContextClass(); this.gainNode = this.audioContext.createGain(); this.gainNode.connect(this.audioContext.destination); } if (this.audioContext.state === "suspended") { await this.audioContext.resume(); } } /** * 添加音频片段到队列 */ async enqueueAudio(base64Audio, message = null) { try { const audioBuffer = await this.decodeAudioData(base64Audio); this.audioQueue.push({ buffer: audioBuffer, base64Audio, message, timestamp: Date.now() }); console.log(`[AudioQueue] 添加音频片段到队列,队列长度: ${this.audioQueue.length}`); if (this.isPaused) { console.log(`[AudioQueue] 暂停状态,音频片段已解码并添加到队列,等待恢复播放`); return; } if (!this.isPlaying) { await this.startPlayback(message); } } catch (error) { console.error("[AudioQueue] 添加音频片段失败:", error); } } /** * 解码Base64音频数据为AudioBuffer */ async decodeAudioData(base64Audio) { await this.initAudioContext(); const binaryString = atob(base64Audio); const pcmData = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { pcmData[i] = binaryString.charCodeAt(i); } const audioBuffer = this.audioContext.createBuffer( 1, // 单声道 pcmData.length / 2, // 采样点数 (16位PCM) this.sampleRate // 采样率 ); const channelData = audioBuffer.getChannelData(0); const dataView = new DataView(pcmData.buffer); for (let i = 0; i < channelData.length; i++) { const sample = dataView.getInt16(i * 2, true); channelData[i] = sample / 32768; } return audioBuffer; } /** * 开始播放队列中的音频 */ async startPlayback(message = null) { if (this.isPlaying || this.isPaused) { return; } await this.initAudioContext(); this.isPlaying = true; this.currentMessage = message; this.playbackStartTime = this.audioContext.currentTime; this.nextStartTime = this.playbackStartTime; if (this.currentMessage) { this.currentMessage.isPlaying = true; } console.log("[AudioQueue] 开始流式播放音频"); this.processQueue(); } /** * 处理音频队列 */ async processQueue() { if (this.processingQueue) { return; } this.processingQueue = true; while (this.isPlaying && !this.isPaused && (this.audioQueue.length > 0 || this.scheduledBuffers.length > 0)) { if (this.isPaused || !this.isPlaying) { console.log("[AudioQueue] 队列处理被打断,停止处理"); break; } if (this.audioQueue.length > 0) { const audioItem = this.audioQueue.shift(); if (this.isPaused || !this.isPlaying) { console.log("[AudioQueue] 在处理音频片段时被打断,停止处理"); break; } if (!audioItem.buffer && audioItem.base64Audio) { try { audioItem.buffer = await this.decodeAudioData(audioItem.base64Audio); } catch (error) { console.error("[AudioQueue] 解码音频失败:", error); continue; } } if (this.isPaused || !this.isPlaying) { console.log("[AudioQueue] 解码完成后发现已被打断,停止处理"); break; } if (audioItem.buffer) { await this.scheduleAudioBuffer(audioItem.buffer); } } else { await new Promise((resolve) => setTimeout(resolve, 50)); } } this.processingQueue = false; if (!this.isPaused && this.isPlaying) { if (this.scheduledBuffers.length > 0) { const lastBuffer = this.scheduledBuffers[this.scheduledBuffers.length - 1]; const waitTime = (lastBuffer.stopTime - this.audioContext.currentTime) * 1e3; if (waitTime > 0) { setTimeout(() => { if (!this.isPaused && this.isPlaying) { this.stopPlayback(); } }, waitTime); } } else { this.stopPlayback(); } } } /** * 调度音频缓冲区播放 */ async scheduleAudioBuffer(audioBuffer) { if (!this.audioContext || !audioBuffer) { return; } const source = this.audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(this.gainNode); const startTime = this.nextStartTime; const stopTime = startTime + audioBuffer.duration; const bufferInfo = { source, startTime, stopTime }; this.scheduledBuffers.push(bufferInfo); source.onended = () => { const index2 = this.scheduledBuffers.indexOf(bufferInfo); if (index2 > -1) { this.scheduledBuffers.splice(index2, 1); } }; source.start(startTime); this.nextStartTime = stopTime; console.log(`[AudioQueue] 调度音频缓冲区播放,开始时间: ${startTime.toFixed(3)}s,持续时间: ${audioBuffer.duration.toFixed(3)}s`); } /** * 打断当前播放(用于语音检测开始时) */ interrupt() { console.log("[AudioQueue] 收到打断信号,立即停止所有音频播放"); this.isPaused = true; this.isPlaying = false; this.processingQueue = false; this.scheduledBuffers.forEach((bufferInfo) => { if (bufferInfo.source) { try { bufferInfo.source.stop(); } catch (error) { console.log("[AudioQueue] 音频源已停止或未开始:", error.message); } } }); this.scheduledBuffers = []; this.audioQueue = []; this.nextStartTime = 0; console.log("[AudioQueue] 已清空音频队列和调度列表,不保留被打断的音频"); if (this.currentMessage) { this.currentMessage.isPlaying = false; } console.log("[AudioQueue] 音频播放已完全打断,进入暂停状态,等待新音频"); } /** * 恢复播放(用于语音检测结束时) */ async resume() { if (!this.isPaused) { return; } console.log("[AudioQueue] 收到恢复信号,准备恢复音频播放"); this.isPaused = false; if (this.audioQueue.length > 0) { await this.startPlayback(this.currentMessage); console.log("[AudioQueue] 音频播放已恢复"); } else { console.log("[AudioQueue] 队列为空,等待新的音频片段"); } } /** * 停止播放 */ stopPlayback() { console.log("[AudioQueue] 停止音频播放"); this.isPlaying = false; this.isPaused = false; this.scheduledBuffers.forEach((bufferInfo) => { if (bufferInfo.source) { try { bufferInfo.source.stop(); } catch (error) { } } }); this.scheduledBuffers = []; this.nextStartTime = 0; if (this.currentMessage) { this.currentMessage.isPlaying = false; this.currentMessage = null; } } /** * 清空队列 */ clearQueue() { this.audioQueue = []; console.log("[AudioQueue] 音频队列已清空"); } /** * 销毁管理器 */ destroy() { console.log("[AudioQueue] 销毁音频队列管理器"); this.stopPlayback(); this.clearQueue(); if (this.audioContext) { this.audioContext.close(); this.audioContext = null; } this.gainNode = null; this.currentMessage = null; } /** * 获取队列状态 */ getStatus() { return { queueLength: this.audioQueue.length, isPlaying: this.isPlaying, isPaused: this.isPaused, scheduledBuffers: this.scheduledBuffers.length }; } } const _Logger = class _Logger { constructor() { if (_Logger.instance) { return _Logger.instance; } _Logger.instance = this; } /** * 通用日志输出方法 */ log(content, type = "info", module = "") { const importantTypes = ["error", "warning", "success"]; const isImportantContent = content.includes("ready") || content.includes("Transcription complete") || content.includes("Final response") || content.includes("connection") || content.includes("initialized") || content.includes("stopped"); if (importantTypes.includes(type) || isImportantContent) { const modulePrefix = module ? `[${module}] ` : ""; console.log(`[${type.toUpperCase()}] ${modulePrefix}${content}`); } } /** * 错误日志 */ error(content, module = "") { this.log(content, "error", module); } /** * 警告日志 */ warning(content, module = "") { this.log(content, "warning", module); } /** * 成功日志 */ success(content, module = "") { this.log(content, "success", module); } /** * 信息日志 */ info(content, module = "") { this.log(content, "info", module); } /** * 调试日志 */ debug(content, module = "") { this.log(content, "debug", module); } }; __publicField(_Logger, "instance", null); let Logger = _Logger; const logger = new Logger(); const log = (content, type = "info", module = "") => logger.log(content, type, module); class WebSocketManager { constructor() { __publicField(this, "websocket", null); __publicField(this, "config", null); __publicField(this, "callbacks", {}); } /** * 初始化配置和回调 */ init(config, callbacks = {}) { this.config = config; this.callbacks = callbacks; } /** * 连接WebSocket */ connect(selectedPhoneNumber) { return new Promise((resolve, reject) => { try { const wsUrl = `${this.config.websocket.url}?token=${this.config.websocket.token}`; this.websocket = new WebSocket(wsUrl); this.websocket.onopen = () => { var _a, _b; log("WebSocket connection successful", "success", "WebSocket"); const initMessage = { cmd: "start_session", content: ((_a = this.config.messages) == null ? void 0 : _a.startSession) || "AI agent start session", phoneId: selectedPhoneNumber || this.config.user.phoneId, sessionId: ((_b = this.config) == null ? void 0 : _b.sessionId) || "" }; this.websocket.send(JSON.stringify(initMessage)); log(`Sent initialization message, using number: ${initMessage.phoneId}`, "info", "WebSocket"); resolve(); }; this.websocket.onmessage = (event) => { if (this.callbacks.onMessage) { this.callbacks.onMessage(event.data); } }; this.websocket.onclose = (event) => { log(`Connection closed: ${event.code} - ${event.reason}`, "warning", "WebSocket"); if (this.callbacks.onClose) { this.callbacks.onClose(event); } }; this.websocket.onerror = (error) => { log("WebSocket connection error", "error", "WebSocket"); if (this.callbacks.onError) { this.callbacks.onError(error); } reject(error); }; } catch (error) { reject(error); } }); } /** * 发送消息 */ send(message) { if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { log("WebSocket not connected, cannot send message", "error", "WebSocket"); return false; } try { this.websocket.send(JSON.stringify(message)); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Message send failed: ${errorMessage}`, "error", "WebSocket"); return false; } } /** * 发送音频数据 */ sendAudioData(audioBuffer) { if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { log("WebSocket not connected, cannot send audio", "error", "WebSocket"); return false; } try { const byteArray = new Uint8Array(audioBuffer.length * 2); const dataView = new DataView(byteArray.buffer); for (let i = 0; i < audioBuffer.length; i++) { dataView.setInt16(i * 2, audioBuffer[i], true); } const base64Audio = btoa(String.fromCharCode(...byteArray)); const audioMessage = { cmd: "audio_input", content: base64Audio }; this.websocket.send(JSON.stringify(audioMessage)); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Audio send failed: ${errorMessage}`, "error", "WebSocket"); return false; } } /** * 发送视频数据 */ sendVideoData(base64Data) { if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { log("WebSocket not connected, cannot send video", "error", "WebSocket"); return false; } try { const message = { cmd: "video_input", content: base64Data }; this.websocket.send(JSON.stringify(message)); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Video data send failed: ${errorMessage}`, "error", "WebSocket"); return false; } } /** * 关闭连接 */ close() { if (this.websocket) { log("WebSocket closed", "info", "WebSocket"); this.websocket.close(); this.websocket = null; } } /** * 检查连接状态 */ isConnected() { return this.websocket !== null && this.websocket.readyState === WebSocket.OPEN; } } class AudioManager { constructor() { __publicField(this, "audioStream", null); __publicField(this, "audioContext", null); __publicField(this, "processor", null); __publicField(this, "config", null); __publicField(this, "callbacks", {}); } /** * 初始化配置和回调 */ init(config, callbacks = {}) { this.config = config; this.callbacks = callbacks; } /** * 请求麦克风权限并初始化音频流 */ async requestMicrophonePermission() { try { this.audioStream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: this.config.audio.sampleRate, channelCount: this.config.audio.channelCount, echoCancellation: this.config.audio.echoCancellation, noiseSuppression: this.config.audio.noiseSuppression, autoGainControl: this.config.audio.autoGainControl } }); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Microphone permission request failed: ${errorMessage}`, "error", "Audio"); throw error; } } /** * 初始化音频处理 */ async initAudioProcessing() { try { const AudioContextClass = window.AudioContext || window.webkitAudioContext; this.audioContext = new AudioContextClass({ sampleRate: this.config.audio.sampleRate }); log("Audio context initialized successfully", "success", "Audio"); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Audio initialization failed: ${errorMessage}`, "error", "Audio"); throw error; } } /** * 开始音频捕获 */ startAudioCapture() { if (!this.audioStream || !this.audioContext) { log("Audio stream or context not ready", "error", "Audio"); return false; } try { const source = this.audioContext.createMediaStreamSource(this.audioStream); this.processor = this.audioContext.createScriptProcessor(4096, 1, 1); let audioBuffer = []; let lastSendTime = Date.now(); this.processor.onaudioprocess = (event) => { const inputBuffer = event.inputBuffer; const inputData = inputBuffer.getChannelData(0); const pcm16Data = new Int16Array(inputData.length); for (let i = 0; i < inputData.length; i++) { pcm16Data[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32767)); } audioBuffer.push(...Array.from(pcm16Data)); const now = Date.now(); if (now - lastSendTime >= (this.config.audio.chunkDuration || 1e3) && audioBuffer.length > 0) { if (this.callbacks.onAudioData) { this.callbacks.onAudioData(audioBuffer); } audioBuffer = []; lastSendTime = now; } }; source.connect(this.processor); this.processor.connect(this.audioContext.destination); log("Audio capture started", "success", "Audio"); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Audio capture startup failed: ${errorMessage}`, "error", "Audio"); return false; } } /** * 停止音频捕获 */ stopAudioCapture() { try { if (this.processor) { this.processor.disconnect(); this.processor = null; log("Audio processor disconnected", "info", "Audio"); } if (this.audioStream) { this.audioStream.getTracks().forEach((track) => { track.stop(); log(`Audio track stopped: ${track.label}`, "info", "Audio"); }); this.audioStream = null; } if (this.audioContext && this.audioContext.state !== "closed") { this.audioContext.close(); this.audioContext = null; log("Audio context closed", "info", "Audio"); } log("Audio capture stopped successfully", "success", "Audio"); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Error stopping audio capture: ${errorMessage}`, "error", "Audio"); return false; } } /** * 恢复音频捕获 */ async resumeAudioCapture() { try { await this.requestMicrophonePermission(); const AudioContextClass = window.AudioContext || window.webkitAudioContext; this.audioContext = new AudioContextClass({ sampleRate: this.config.audio.sampleRate }); return this.startAudioCapture(); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Failed to resume audio capture: ${errorMessage}`, "error", "Audio"); return false; } } /** * 播放音频响应 */ async playAudioResponse(base64Audio, message) { try { log("Starting audio playback...", "info", "Audio"); const binaryString = atob(base64Audio); const pcmData = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { pcmData[i] = binaryString.charCodeAt(i); } const wavData = this.pcmToWav(pcmData, 22050, 1); const blob = new Blob([wavData], { type: "audio/wav" }); const audioUrl = URL.createObjectURL(blob); const audio = new Audio(audioUrl); if (message) { message.audioInstance = audio; message.isPlaying = true; } audio.onloadstart = () => log("Audio loading started", "info", "Audio"); audio.oncanplay = () => log("Audio can play", "info", "Audio"); audio.onplay = () => log("Audio playback started", "info", "Audio"); audio.onended = () => { log("Audio playback completed", "info", "Audio"); URL.revokeObjectURL(audioUrl); if (message) { message.isPlaying = false; message.audioInstance = null; } }; audio.onerror = () => { log("Audio playback error", "error", "Audio"); URL.revokeObjectURL(audioUrl); if (message) { message.isPlaying = false; message.audioInstance = null; } }; await audio.play(); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Audio playback failed: ${errorMessage}`, "error", "Audio"); console.error("Audio playback error:", error); if (message) { message.isPlaying = false; message.audioInstance = null; } return false; } } /** * Convert PCM data to WAV format */ pcmToWav(pcmData, sampleRate, numChannels) { const length = pcmData.length; const buffer = new ArrayBuffer(44 + length); const view = new DataView(buffer); const writeString = (offset, string) => { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } }; writeString(0, "RIFF"); view.setUint32(4, 36 + length, true); writeString(8, "WAVE"); writeString(12, "fmt "); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * numChannels * 2, true); view.setUint16(32, numChannels * 2, true); view.setUint16(34, 16, true); writeString(36, "data"); view.setUint32(40, length, true); const dataView = new Uint8Array(buffer, 44); dataView.set(pcmData); return buffer; } /** * 检查是否正在录音 */ isRecording() { return this.processor !== null && this.audioStream !== null; } /** * 清理资源 */ cleanup() { this.stopAudioCapture(); } } class VideoManager { constructor() { __publicField(this, "videoStream", null); __publicField(this, "videoElement", null); __publicField(this, "canvas", null); __publicField(this, "context", null); __publicField(this, "config", null); __publicField(this, "callbacks", {}); __publicField(this, "captureInterval", null); __publicField(this, "isCapturing", false); } /** * 初始化配置和回调 */ init(config, callbacks = {}) { this.config = config; this.callbacks = callbacks; } /** * 请求摄像头权限并初始化视频流 */ async requestCameraPermission() { var _a, _b; try { this.videoStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: ((_b = (_a = this.config) == null ? void 0 : _a.video) == null ? void 0 : _b.facingMode) || "environment", // 后置摄像头 width: { ideal: 640 }, height: { ideal: 480 } }, audio: false }); log("Camera permission granted successfully", "success", "Video"); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Camera permission request failed: ${errorMessage}`, "error", "Video"); throw error; } } /** * 初始化视频元素和画布 */ async initVideoElements(videoElement) { if (!this.videoStream) { throw new Error("Video stream not available. Call requestCameraPermission first."); } this.videoElement = videoElement; this.videoElement.srcObject = this.videoStream; try { await this.videoElement.play(); log("Video element started playing", "success", "Video"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Failed to play video: ${errorMessage}`, "error", "Video"); throw error; } this.canvas = document.createElement("canvas"); this.context = this.canvas.getContext("2d"); return new Promise((resolve) => { const handleLoadedMetadata = () => { this.canvas.width = this.videoElement.videoWidth || 640; this.canvas.height = this.videoElement.videoHeight || 480; log(`Video initialized: ${this.canvas.width}x${this.canvas.height}`, "info", "Video"); this.videoElement.removeEventListener("loadedmetadata", handleLoadedMetadata); resolve(); }; if (this.videoElement.readyState >= 1) { handleLoadedMetadata(); } else { this.videoElement.addEventListener("loadedmetadata", handleLoadedMetadata); } }); } /** * 开始捕获视频帧 */ startCapture() { var _a, _b; if (this.isCapturing) return; this.isCapturing = true; const frameRate = ((_b = (_a = this.config) == null ? void 0 : _a.video) == null ? void 0 : _b.frameRate) || 2; const interval = 1e3 / frameRate; this.captureInterval = setInterval(() => { this.captureFrame(); }, interval); log(`Video capture started (${frameRate} FPS)`, "success", "Video"); } /** * 停止捕获视频帧 */ stopCapture() { if (!this.isCapturing) return; this.isCapturing = false; if (this.captureInterval) { clearInterval(this.captureInterval); this.captureInterval = null; } log("Video capture stopped", "info", "Video"); } /** * 捕获当前视频帧并转换为base64 */ captureFrame() { var _a, _b; if (!this.videoElement || !this.canvas || !this.context) return; try { this.context.drawImage( this.videoElement, 0, 0, this.canvas.width, this.canvas.height ); const quality = ((_b = (_a = this.config) == null ? void 0 : _a.video) == null ? void 0 : _b.quality) || 0.8; const base64Data = this.canvas.toDataURL("image/jpeg", quality).split(",")[1]; if (this.callbacks.onVideoFrame) { this.callbacks.onVideoFrame(base64Data); } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Failed to capture frame: ${errorMessage}`, "error", "Video"); } } /** * 停止视频流 */ stopVideoStream() { this.stopCapture(); if (this.videoStream) { this.videoStream.getTracks().forEach((track) => { track.stop(); }); this.videoStream = null; } if (this.videoElement) { this.videoElement.srcObject = null; } log("Video stream stopped", "info", "Video"); } /** * 检查是否正在捕获 */ isVideoCapturing() { return this.isCapturing; } /** * 清理资源 */ destroy() { this.stopVideoStream(); this.canvas = null; this.context = null; this.videoElement = null; } } class MessageHandler { constructor() { __publicField(this, "config", null); __publicField(this, "callbacks", {}); __publicField(this, "audioOutputCache", ""); __publicField(this, "textOutputCache", ""); //@ts-ignore __publicField(this, "currentUserMessage", null); __publicField(this, "currentAssistantMessage", null); __publicField(this, "shouldDiscardAudio", false); } /** * 初始化配置和回调 */ init(config, callbacks = {}) { this.config = config; this.callbacks = callbacks; this.currentUserMessage = null; } /** * 处理WebSocket消息 */ handleMessage(messageData, conversationList, audioQueueManager) { var _a, _b; try { const payload = JSON.parse(messageData); log(`Received message: ${payload.cmd}`, "info", "Message"); if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onMessage) { this.config.callbacks.onMessage(payload); } switch (payload.cmd) { case "ready": this.handleReady(); break; case "speech_start": this.handleSpeechStart(audioQueueManager); break; case "speech_stop": this.handleSpeechStop(); break; case "transcript_done": this.handleTranscriptDone(payload, conversationList); break; case "response_create": this.handleResponseCreate(conversationList); break; case "text_output": this.handleTextOutput(payload, conversationList); break; case "audio_output": this.handleAudioOutput(payload, audioQueueManager); break; case "audio_done": this.handleAudioDone(); break; case "text_done": this.handleTextDone(); break; case "final_response": this.handleFinalResponse(payload); break; default: log(`Unknown command: ${payload.cmd}`, "warning", "Message"); } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; log(`Message parsing error: ${errorMessage}`, "error", "Message"); } } /** * 处理ready消息 */ handleReady() { log("Server ready, starting voice input", "success", "Message"); if (this.callbacks.onReady) { this.callbacks.onReady(); } } /** * 处理语音开始 */ handleSpeechStart(audioQueueManager) { var _a, _b; log("Speech detected, stopping current audio playback", "info", "Message"); this.shouldDiscardAudio = true; if (audioQueueManager) { audioQueueManager.interrupt(); log("Audio playback interrupted, all future audio will be discarded until next response_create", "info", "Message"); } if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onSpeechStart) { this.config.callbacks.onSpeechStart(); } } /** * 处理语音结束 */ handleSpeechStop() { var _a, _b; log("Speech ended, but audio will remain discarded until next response_create", "info", "Message"); if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onSpeechStop) { this.config.callbacks.onSpeechStop(); } } /** * 处理转录完成 */ handleTranscriptDone(payload, conversationList) { var _a, _b; log(`Transcription complete: ${payload.content}`, "info", "Message"); this.currentUserMessage = payload.content; conversationList.value.push({ id: Date.now(), type: "user", content: payload.content, timestamp: /* @__PURE__ */ new Date(), audioData: null }); if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onTranscript) { this.config.callbacks.onTranscript(payload.content); } if (this.callbacks.scrollToBottom) { this.callbacks.scrollToBottom(); } } /** * 处理响应创建 */ handleResponseCreate(conversationList) { log("Starting response generation - creating new audio queue for this response", "info", "Message"); this.shouldDiscardAudio = false; if (this.callbacks.initAudioQueueManager) { this.callbacks.initAudioQueueManager(); } this.audioOutputCache = ""; this.textOutputCache = ""; this.currentAssistantMessage = { id: Date.now(), type: "assistant", content: "", // Initial content is empty timestamp: /* @__PURE__ */ new Date(), audioData: null, isLoading: true, isStreaming: true // Mark as streaming playback }; conversationList.value.push(this.currentAssistantMessage); if (this.callbacks.scrollToBottom) { this.callbacks.scrollToBottom(); } } /** * 处理文本输出 */ handleTextOutput(payload, conversationList) { log(`Text output segment: ${payload.content}`, "info", "Message"); this.textOutputCache += payload.content; if (this.currentAssistantMessage) { const messageIndex = conversationList.value.findIndex((msg) => msg.id === this.currentAssistantMessage.id); if (messageIndex !== -1) { conversationList.value[messageIndex] = { ...conversationList.value[messageIndex], content: this.textOutputCache, isLoading: false }; if (this.callbacks.scrollToBottom) { setTimeout(() => this.callbacks.scrollToBottom(), 0); } } } } /** * 处理音频输出 */ handleAudioOutput(payload, audioQueueManager) { var _a, _b, _c, _d; if (this.shouldDiscardAudio) { log(`Audio segment discarded due to speech interruption`, "info", "Message"); this.audioOutputCache += payload.content; if ((_b = (_a = this.config) == null ? void 0 : _a.callbacks) == null ? void 0 : _b.onAudioOutput) { this.config.callbacks.onAudioOutput(payload.content); } return; } log(`Received audio segment, adding to current response queue`, "info", "Message"); if (audioQueueManager && payload.content) { audioQueueManager.enqueueAudio(payload.content, this.currentAssistantMessage || void 0); if (this.currentAssistantMessage && !this.currentAssistantMessage.hasStartedPlaying) { this.currentAssistantMessage.hasStartedPlaying = true; this.currentAssistantMessage.isPlaying = true; log("Starting streaming audio playback for current response", "info", "Message"); } } this.audioOutputCache += payload.content; if ((_d = (_c = this.config) == null ? void 0 : _c.callbacks) == null ? void 0 : _d.onAudioOutput) { this.config.callbacks.onAudioOutput(payload.content); } } /** * 处理音频完成 */ handleAudioDone() { log("Audio transmission complete for current response", "info", "Message"); if (this.currentAssistantMessage && this.audioOutputCache.length > 0) { this.currentAssistantMessage.audioData = this.audioOutputCache; this