/* ===== 0. 全局状态与基础配置 ===== */ let extractedImages = []; let currentImageIndex = -1; let currentMediaImage = null; let currentFilter = ""; let currentMediaDataURL = ""; let isFrameMode = false; let chatContext = [{ role: "system", content: "你是一个得力的 AI 助手,请简明扼要地回答问题。" }]; window.addEventListener('load', () => { const savedKey = localStorage.getItem('datxy_global_api_key'); if(savedKey) document.getElementById('globalApiKey').value = savedKey; updateResolutionState(); document.getElementById('imageModelSelect').addEventListener('change', updateResolutionState); updateStats(); clearApiLogs(); }); document.getElementById('globalApiKey').addEventListener('change', (e) => { localStorage.setItem('datxy_global_api_key', e.target.value.trim()); }); // 全局监听左右键切图 window.addEventListener('keydown', (e) => { const viewMedia = document.getElementById('view-media'); if (viewMedia.style.display !== 'none' && isFrameMode) { if (e.key === 'ArrowLeft') { if (currentImageIndex > 0) { currentImageIndex--; displayExtractedFrame(); } } else if (e.key === 'ArrowRight') { if (currentImageIndex < extractedImages.length - 1) { currentImageIndex++; displayExtractedFrame(); } } } }); function switchTab(tabId) { document.querySelectorAll('.editor-tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.workspace-view').forEach(v => v.style.display = 'none'); document.getElementById('tab-' + tabId).classList.add('active'); document.getElementById('view-' + tabId).style.display = 'flex'; } function toggleCollapse(contentId, headerElement) { const content = document.getElementById(contentId); content.classList.toggle('collapsed'); headerElement.classList.toggle('collapsed'); } function showError(msg) { const errorMsg = document.getElementById('errorMsg'); errorMsg.innerText = msg; errorMsg.style.display = "block"; setTimeout(() => { errorMsg.style.display = "none"; }, 4000); } /* ===== 1. API 调试日志处理 ===== */ function logApiCall(type, endpoint, reqData, resData, isError = false) { const container = document.getElementById('apiLogContainer'); if (container.innerHTML.includes("暂无 API 调用记录")) container.innerHTML = ""; const time = new Date().toLocaleTimeString(); const logHTML = `
[${time}] ${type} 请求: ${endpoint}
📦 请求 Payload:
${JSON.stringify(reqData, null, 2)}
${isError ? '❌ 报错信息' : '✅ 响应结果'}:
${JSON.stringify(resData, null, 2)}
`; container.innerHTML = logHTML + container.innerHTML; } function clearApiLogs() { document.getElementById('apiLogContainer').innerHTML = '
暂无 API 调用记录...
'; } /* ===== 2. 聊天对话系统 ===== */ function appendChatMessage(role, content) { const history = document.getElementById('chatHistory'); const div = document.createElement('div'); div.className = `chat-msg ${role === 'user' ? 'msg-user' : 'msg-ai'}`; div.innerText = content; history.appendChild(div); history.scrollTop = history.scrollHeight; } // 清空对话 document.getElementById('clearChatBtn').addEventListener('click', () => { chatContext = [{ role: "system", content: "你是一个得力的 AI 助手,请简明扼要地回答问题。" }]; document.getElementById('chatHistory').innerHTML = '
对话记忆已清空。请在下方输入问题。
'; }); document.getElementById('chatInput').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); document.getElementById('sendChatBtn').click(); } }); document.getElementById('sendChatBtn').addEventListener('click', async () => { const apiKey = document.getElementById('globalApiKey').value.trim(); const model = document.getElementById('chatModelSelect').value; const inputEl = document.getElementById('chatInput'); const text = inputEl.value.trim(); const btn = document.getElementById('sendChatBtn'); if (!apiKey) return showError("❌ 请输入顶部全局 API Key"); if (!text) return; localStorage.setItem('datxy_global_api_key', apiKey); // UI 更新 appendChatMessage('user', text); chatContext.push({ role: "user", content: String(text) }); inputEl.value = ""; inputEl.disabled = true; btn.disabled = true; btn.innerText = "思考中"; const safeMessages = chatContext.map(msg => ({ role: msg.role, content: msg.content })); const reqPayload = { model: model, messages: safeMessages, stream: false, max_tokens: 1000 }; try { const res = await fetch("https://api.poixe.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, body: JSON.stringify(reqPayload) }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.error?.message || `HTTP 错误码: ${res.status}`); } const result = await res.json(); logApiCall('智能对话', 'https://api.poixe.com/v1/chat/completions', reqPayload, result, false); if (result.choices && result.choices[0] && result.choices[0].message) { const aiMsg = result.choices[0].message.content; appendChatMessage('ai', aiMsg); chatContext.push({ role: "assistant", content: String(aiMsg) }); } else { throw new Error('API 返回数据缺失 choices 节点'); } } catch(e) { showError("❌ 对话失败: " + e.message); appendChatMessage('ai', `❌ 请求失败,请检查 API Key 或网络 (${e.message})`); logApiCall('智能对话', 'https://api.poixe.com/v1/chat/completions', reqPayload, { error: e.message }, true); chatContext.pop(); } finally { inputEl.disabled = false; btn.disabled = false; btn.innerText = "发送"; inputEl.focus(); } }); /* ===== 3. AI 文本测试 (针对文本区单次调用) ===== */ document.getElementById('aiProcessTextBtn').addEventListener('click', async () => { const apiKey = document.getElementById('globalApiKey').value.trim(); const model = document.getElementById('textModelSelect').value; const text = document.getElementById('textarea').value.trim(); const btn = document.getElementById('aiProcessTextBtn'); if (!apiKey) return showError("❌ 请输入全局 API Key"); if (!text) return showError("❌ 请在左侧文本区输入请求内容"); localStorage.setItem('datxy_global_api_key', apiKey); switchTab('text'); btn.disabled = true; btn.innerText = "⏳ 正在处理..."; const reqPayload = { model: model, messages: [{ role: "system", content: "你是一个文本处理助手。回复不超500字。" }, { role: "user", content: text }], stream: false, max_tokens: 1000 }; try { const res = await fetch("https://api.poixe.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, body: JSON.stringify(reqPayload) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const result = await res.json(); logApiCall('文本单次处理', 'https://api.poixe.com/v1/chat/completions', reqPayload, result, false); if (result.choices && result.choices[0] && result.choices[0].message) { document.getElementById('textarea').value = result.choices[0].message.content; } else { document.getElementById('textarea').value = JSON.stringify(result, null, 2); } updateStats(); } catch(e) { showError("❌ 请求失败: " + e.message); logApiCall('文本单次处理', 'https://api.poixe.com/v1/chat/completions', reqPayload, { error: e.message }, true); } finally { btn.disabled = false; btn.innerText = "🤖 AI 处理文本"; } }); /* ===== 4. AI 图像生成 ===== */ function updateResolutionState() { const modelSelect = document.getElementById('imageModelSelect'); const imageSizeSelect = document.getElementById('imageSize'); imageSizeSelect.disabled = (modelSelect.value !== 'nano-banana-pro'); } document.getElementById('generateBtn').addEventListener('click', async () => { const apiKey = document.getElementById('globalApiKey').value.trim(); const model = document.getElementById('imageModelSelect').value; const prompt = document.getElementById('imagePrompt').value.trim(); const aspectRatio = document.getElementById('aspectRatio').value.split(' ')[0]; const imageSize = document.getElementById('imageSize').value.split(' ')[0]; const btn = document.getElementById('generateBtn'); if (!apiKey) return showError("❌ 请在顶部输入全局 API Key"); if (!prompt) return showError("❌ 图像提示词不能为空"); localStorage.setItem('datxy_global_api_key', apiKey); switchTab('image'); document.getElementById('imagePlaceholder').style.display = "none"; document.getElementById('aiResultWrapper').style.display = "none"; document.getElementById('imageLoader').style.display = "block"; btn.disabled = true; btn.innerText = "⏳ 正在绘制中..."; const requestBody = { contents: [ { parts: [ { text: prompt } ] } ], generationConfig: { responseModalities: ["Text", "Image"], imageConfig: { aspectRatio: aspectRatio, ...(model === 'nano-banana-pro' && { imageSize: imageSize }) } } }; try { const response = await fetch(`https://api.poixe.com/v1beta/models/${model}:generateContent`, { method: 'POST', headers: { 'x-goog-api-key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.error?.message || `HTTP ${response.status}`); } const data = await response.json(); let logData = JSON.parse(JSON.stringify(data)); try { if(logData.candidates && logData.candidates[0] && logData.candidates[0].content && logData.candidates[0].content.parts){ logData.candidates[0].content.parts.forEach(p => { if(p.inlineData && p.inlineData.data) p.inlineData.data = "[Base64 Data Truncated For Log...]"; }); } } catch(e){} logApiCall('图像生成', `https://api.poixe.com/v1beta/models/${model}:generateContent`, requestBody, logData, false); if (data.candidates && data.candidates.length > 0) { let imageUrl = ""; data.candidates[0].content.parts.forEach(part => { if (part.inlineData) imageUrl = `data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`; }); if (imageUrl) { document.getElementById('aiResultImage').src = imageUrl; document.getElementById('aiResultWrapper').style.display = "flex"; } else { showError("⚠️ 未返回图片数据"); document.getElementById('imagePlaceholder').style.display = "flex"; } } else { showError("❌ 未获取到生成结果"); document.getElementById('imagePlaceholder').style.display = "flex"; } } catch (error) { showError("❌ 生成失败: " + error.message); document.getElementById('imagePlaceholder').style.display = "flex"; logApiCall('图像生成', `https://api.poixe.com/v1beta/models/${model}:generateContent`, requestBody, { error: error.message }, true); } finally { document.getElementById('imageLoader').style.display = "none"; btn.disabled = false; btn.innerText = "🚀 开始绘制图像"; } }); function downloadImage() { const img = document.getElementById('aiResultImage'); if (!img.src) return; const link = document.createElement('a'); link.href = img.src; link.download = `DatXY_Vision_${new Date().getTime()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } /* ===== 5. 文件转码与媒体解析 ===== */ function parseMimeFromDataURI(dataURI) { const m = /^data:([^;,]+)[^,]*,/.exec(dataURI); return m ? m[1].trim() : ''; } document.getElementById('btnToB64').addEventListener('click', () => { const file = document.getElementById('fileInput').files[0]; if(!file) return showError('❌ 请选择一个文件'); if (file.type.startsWith('video/') && file.size > 5 * 1024 * 1024) { return showError('❌ 视频文件大于5MB,不支持直接转码'); } const btn = document.getElementById('btnToB64'); btn.innerText = "⏳ 处理中..."; const reader = new FileReader(); reader.onload = e => { document.getElementById('textarea').value = e.target.result; switchTab('text'); updateStats(); btn.innerText = "文件 ➔ Base64"; }; reader.onerror = () => { showError("❌ 读取失败"); btn.innerText = "文件 ➔ Base64"; }; reader.readAsDataURL(file); }); document.getElementById('btnFromB64').addEventListener('click', () => { const s = document.getElementById('textarea').value.trim(); if(!s || !s.startsWith('data:')) return showError('❌ 请在文本区提供有效的 Base64 DataURI'); const mime = parseMimeFromDataURI(s); if(!mime) return showError('❌ 无法解析 MIME 类型'); switchTab('media'); document.getElementById('mediaPlaceholder').style.display = 'none'; const playerContainer = document.getElementById('mediaPlayerContainer'); const frameContainer = document.getElementById('frameDisplayContainer'); const wrapper = document.getElementById('mediaPlayerWrapper'); // Reset wrapper.innerHTML = ""; playerContainer.style.display = 'none'; frameContainer.style.display = 'none'; currentFilter = ''; isFrameMode = false; if(mime.startsWith('image/')){ const img = document.getElementById('frameDisplayedImage'); img.src = s; currentMediaImage = img; img.style.filter = currentFilter; document.getElementById('mediaToolbar').style.display = 'flex'; document.querySelectorAll('.frame-btn').forEach(el => el.style.display = 'none'); document.getElementById('framePager').style.display = 'none'; frameContainer.style.display = 'flex'; } else if(mime.startsWith('audio/') || mime.startsWith('video/')){ const media = document.createElement(mime.startsWith('audio/') ? 'audio' : 'video'); media.controls = true; media.src = s; media.preload = 'metadata'; wrapper.appendChild(media); currentMediaDataURL = s; playerContainer.style.display = 'flex'; } else { showError("⚠️ 不支持预览的媒体格式,保留在文本区"); switchTab('text'); } }); document.getElementById('btnDownloadMedia').addEventListener('click', () => { if(!currentMediaDataURL) return; const mime = parseMimeFromDataURI(currentMediaDataURL); const ext = mime.includes('audio/') ? 'mp3' : mime.includes('video/') ? 'mp4' : 'bin'; const a = document.createElement('a'); a.href = currentMediaDataURL; a.download = `media_decode.${ext}`; a.click(); }); // 视频抽帧逻辑 function seekTo(vid, t) { return new Promise(resolve => { const handler = () => { vid.removeEventListener('seeked', handler); resolve(); }; vid.addEventListener('seeked', handler, { once: true }); vid.currentTime = Math.min(t, Math.max(0, vid.duration - 0.001)); }); } document.getElementById('btnFrames').addEventListener('click', async () => { const file = document.getElementById('fileInput').files[0]; const fps = parseInt(document.getElementById('intervalInput').value, 10); const btn = document.getElementById('btnFrames'); if(!file || !file.type.startsWith('video/')) return showError('❌ 请在上方选择一个视频文件'); if(isNaN(fps) || fps <= 0) return showError('❌ 请输入有效的抽帧率 (FPS)'); btn.innerText = "⏳ 提取中..."; const objectURL = URL.createObjectURL(file); const video = document.createElement('video'); video.preload = 'metadata'; video.src = objectURL; video.onloadedmetadata = async () => { extractedImages = []; currentImageIndex = -1; currentFilter = ''; isFrameMode = true; switchTab('media'); document.getElementById('mediaPlaceholder').style.display = 'none'; document.getElementById('mediaPlayerContainer').style.display = 'none'; document.getElementById('frameDisplayContainer').style.display = 'flex'; document.getElementById('mediaToolbar').style.display = 'flex'; document.querySelectorAll('.frame-btn').forEach(el => el.style.display = 'inline-flex'); document.getElementById('framePager').style.display = 'block'; const frameDuration = 1 / fps; const total = Math.floor(video.duration * fps) + 1; let currentTime = 0; for(let i=0; i 0){ currentImageIndex = 0; displayExtractedFrame(); } else { showError('❌ 未能提取到任何帧'); } }; }); function displayExtractedFrame() { if(currentImageIndex < 0 || currentImageIndex >= extractedImages.length) return; const f = extractedImages[currentImageIndex]; const img = document.getElementById('frameDisplayedImage'); img.src = f.url; currentMediaImage = img; img.style.filter = currentFilter; document.getElementById('framePager').textContent = `[ ${currentImageIndex + 1} / ${extractedImages.length} ] 时间轴: ${f.time.toFixed(2)} 秒`; } document.getElementById('prevButton').addEventListener('click', () => { if(currentImageIndex > 0) { currentImageIndex--; displayExtractedFrame(); } }); document.getElementById('nextButton').addEventListener('click', () => { if(currentImageIndex < extractedImages.length - 1) { currentImageIndex++; displayExtractedFrame(); } }); document.getElementById('btnDownloadFrame').addEventListener('click', () => { if(extractedImages.length === 0) return; const f = extractedImages[currentImageIndex]; const a = document.createElement('a'); a.href = f.url; a.download = `frame_${String(currentImageIndex+1).padStart(4,'0')}.png`; a.click(); }); // 图像滤镜 document.getElementById('btnInvert').addEventListener('click', () => { currentFilter = (currentFilter + ' invert(1)').trim(); if(currentMediaImage) currentMediaImage.style.filter = currentFilter; }); document.getElementById('btnGray').addEventListener('click', () => { currentFilter = (currentFilter + ' grayscale(1)').trim(); if(currentMediaImage) currentMediaImage.style.filter = currentFilter; }); document.getElementById('btnRestore').addEventListener('click', () => { currentFilter = ''; if(currentMediaImage) currentMediaImage.style.filter = currentFilter; }); document.getElementById('btnDownloadEditedImage').addEventListener('click', () => { if(!currentMediaImage || !currentMediaImage.src) return; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const w = currentMediaImage.naturalWidth || currentMediaImage.width; const h = currentMediaImage.naturalHeight || currentMediaImage.height; canvas.width = w; canvas.height = h; ctx.filter = currentFilter || 'none'; ctx.drawImage(currentMediaImage, 0, 0, w, h); const a = document.createElement('a'); a.href = canvas.toDataURL('image/png'); a.download = 'processed_image.png'; a.click(); }); /* 修改点:彻底清空各个视图和临时变量的所有状态 */ document.getElementById('btnClearOutput').addEventListener('click', () => { // 1. 清空文本及查找框 document.getElementById('textarea').value = ""; document.getElementById('search-input').value = ""; document.getElementById('replace-input').value = ""; updateStats(); // 2. 清空媒体区及文件框 document.getElementById('fileInput').value = ""; extractedImages = []; currentImageIndex = -1; currentMediaImage = null; currentFilter = ''; document.getElementById('mediaPlaceholder').style.display = 'flex'; document.getElementById('mediaPlayerContainer').style.display = 'none'; document.getElementById('frameDisplayContainer').style.display = 'none'; document.getElementById('mediaPlayerWrapper').innerHTML = ''; // 3. 清空聊天记忆 chatContext = [{ role: "system", content: "你是一个得力的 AI 助手,请简明扼要地回答问题。" }]; document.getElementById('chatHistory').innerHTML = '
对话记忆已清空。请在下方输入问题。
'; // 4. 清空 AI 图像生成区 document.getElementById('imagePlaceholder').style.display = "flex"; document.getElementById('aiResultWrapper').style.display = "none"; document.getElementById('aiResultImage').src = ""; document.getElementById('imagePrompt').value = ""; // 5. 清除 API 调试日志与错误提示 clearApiLogs(); document.getElementById('errorMsg').style.display = "none"; }); /* ===== 6. 文本正则与编辑区 ===== */ const textarea = document.getElementById("textarea"); const searchInput = document.getElementById("search-input"); const replaceInput = document.getElementById("replace-input"); textarea.addEventListener("input", updateStats); document.getElementById("remove-duplicates-btn").addEventListener("click", () => { const textList = textarea.value.split("\n"); const uniqueList = [...new Set(textList)]; document.getElementById("duplicate").innerHTML = `重复:${textList.length - uniqueList.length}`; textarea.value = uniqueList.join("\n"); updateStats(); }); document.getElementById("preserve-matches-btn").addEventListener("click", function() { try { const regex = new RegExp(searchInput.value, 'g'); let matches = textarea.value.match(regex); if (matches && matches.length > 0) { textarea.value = matches.join("\n"); document.getElementById("regex-replace-count").innerHTML = `正则:${matches.length}`; } else { document.getElementById("regex-replace-count").innerHTML = `正则:0`; } } catch (e) { showError("❌ 正则表达式有误"); } }); document.getElementById("replace-btn").addEventListener("click", () => { if (searchInput.value !== "") { const textArr = textarea.value.split(searchInput.value); document.getElementById("replace-count").innerHTML = `替换:${textArr.length - 1}`; textarea.value = textArr.join(replaceInput.value); updateStats(); } }); document.getElementById("regex-replace-btn").addEventListener("click", () => { if (searchInput.value !== "") { try { let matchCount = (textarea.value.match(new RegExp(searchInput.value, 'g')) || []).length; textarea.value = textarea.value.replace(new RegExp(searchInput.value, 'g'), replaceInput.value); document.getElementById("regex-replace-count").innerHTML = `正则:${matchCount}`; updateStats(); } catch (e) { showError("❌ 正则表达式有误"); } } }); function updateStats() { const text = textarea.value; document.getElementById("total").innerHTML = `总数:${text.length}`; document.getElementById("chinese").innerHTML = `汉字:${(text.match(/[\u4e00-\u9fa5]/g) || []).length}`; document.getElementById("punctuation").innerHTML = `标点:${(text.match(/[^\u4e00-\u9fa5\w]/g) || []).length}`; document.getElementById("alphabet").innerHTML = `字母:${(text.match(/[a-zA-Z]/g) || []).length}`; document.getElementById("numbers").innerHTML = `数字:${(text.match(/\d/g) || []).length}`; const searchVal = searchInput.value; if(searchVal) { document.getElementById("replace-count").innerHTML = `替换:${text.split(searchVal).length - 1}`; try{ document.getElementById("regex-replace-count").innerHTML = `正则:${(text.match(new RegExp(searchVal, 'g')) || []).length}`; }catch(e){} } const lines = text.split('\n'); const map = new Map(); let dup=0; for(const line of lines){ if(line) map.set(line,(map.get(line)||0)+1); } for(const cnt of map.values()){ if(cnt>1) dup += (cnt-1); } document.getElementById("duplicate").innerHTML = `重复:${dup}`; } document.getElementById("copy-btn").addEventListener("click", function() { let textToCopy = textarea.value || textarea.getAttribute("placeholder"); const tempInput = document.createElement("textarea"); document.body.appendChild(tempInput); tempInput.value = textToCopy; tempInput.select(); document.execCommand("copy"); document.body.removeChild(tempInput); // UI 反馈 const originalText = this.innerHTML; this.innerHTML = "已复"; setTimeout(() => { this.innerHTML = originalText; }, 2000); }); document.getElementById("paste-btn").addEventListener("click", function() { navigator.clipboard.readText().then(clipText => { textarea.value = clipText; updateStats(); // UI 反馈 const originalText = this.innerHTML; this.innerHTML = "已粘"; setTimeout(() => { this.innerHTML = originalText; }, 2000); }).catch(err => { showError("❌ 无法访问剪贴板,请手动粘贴"); }); }); /* 修改点:新增了清空文本与网页预览功能 */ document.getElementById("clear-text-btn").addEventListener("click", function() { textarea.value = ""; updateStats(); }); document.getElementById("preview-web-btn").addEventListener("click", function() { const htmlContent = textarea.value.trim(); if (!htmlContent) return showError("❌ 文本区为空,无法预览"); // 生成 Blob URL 进行安全的新标签页渲染 const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); }); // 7. Web Speech Synthesis (TTS) (function() { const voiceSelect = document.getElementById('tts-voiceSelect'); const filterChineseBox = document.getElementById('tts-filterChinese'); let speaking = false; function listVoices() { const voices = window.speechSynthesis.getVoices(); voiceSelect.innerHTML = ''; const filtered = filterChineseBox.checked ? voices.filter(v => v.lang && v.lang.toLowerCase().startsWith('zh')) : voices; if (filtered.length === 0) return voiceSelect.innerHTML = ''; filtered.forEach(v => { const opt = document.createElement('option'); opt.value = v.name; opt.textContent = `${v.name} (${v.lang})`; voiceSelect.appendChild(opt); }); const saved = localStorage.getItem('tts_voice_name'); if (saved) { const idx = [...voiceSelect.options].findIndex(o => o.value === saved); if (idx >= 0) voiceSelect.selectedIndex = idx; } } document.getElementById('tts-start').addEventListener('click', () => { const text = document.getElementById('textarea').value.trim(); if (!text) return showError('❌ 请在左侧文本区输入要朗读的内容'); const v = window.speechSynthesis.getVoices().find(v => v.name === voiceSelect.value); if (!v) return showError('❌ 请选择语音'); if (speaking) { window.speechSynthesis.cancel(); } speaking = true; switchTab('text'); const chunks = text.split(/\n+/).map(s=>s.trim()).filter(Boolean).flatMap(line => line.match(/[^。!?!?;;\.…]+[。!?!?;;\…]?/g) || [line]); const next = () => { if (!speaking || chunks.length === 0) { speaking = false; return; } const utt = new SpeechSynthesisUtterance(chunks.shift().trim()); utt.voice = v; utt.lang = v.lang; utt.rate = parseFloat(document.getElementById('tts-rate').value); utt.pitch = parseFloat(document.getElementById('tts-pitch').value); utt.volume = parseFloat(document.getElementById('tts-volume').value); utt.onend = next; utt.onerror = next; window.speechSynthesis.speak(utt); }; next(); localStorage.setItem('tts_voice_name', v.name); }); document.getElementById('tts-stop').addEventListener('click', () => { speaking = false; window.speechSynthesis.cancel(); }); filterChineseBox.addEventListener('change', listVoices); voiceSelect.addEventListener('change', () => { localStorage.setItem('tts_voice_name', voiceSelect.value); }); document.getElementById('tts-rate').addEventListener('input', (e) => document.getElementById('tts-rate-val').textContent = e.target.value + 'x'); document.getElementById('tts-pitch').addEventListener('input', (e) => document.getElementById('tts-pitch-val').textContent = e.target.value); document.getElementById('tts-volume').addEventListener('input', (e) => document.getElementById('tts-volume-val').textContent = e.target.value); listVoices(); if (typeof window.speechSynthesis !== 'undefined') window.speechSynthesis.onvoiceschanged = listVoices; })();