Files
LittleWhiteBox/modules/tts/tts-overlay.html
2026-01-18 02:55:49 +08:00

2407 lines
87 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<title>TTS ÓïÒôÉèÖÃ</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
¼«¼ò¿Æ¼¼·çÅäÉ« - ºÚ°×»Ò + µ¥É«µã׺
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
/* ±³¾°²ã´Î */
--bg-primary: #0a0a0c;
--bg-secondary: #111114;
--bg-tertiary: #18181c;
--bg-elevated: #1e1e24;
--bg-input: rgba(255, 255, 255, 0.04);
/* ÎÄ×Ö²ã´Î */
--text-primary: #f0f0f2;
--text-secondary: #a0a0a8;
--text-muted: #606068;
--text-dim: #404048;
/* ±ß¿ò */
--border: rgba(255, 255, 255, 0.08);
--border-light: rgba(255, 255, 255, 0.12);
--border-focus: rgba(140, 200, 255, 0.4);
/* Ψһǿµ÷É« - µ­ÇàÀ¶£¨¿Æ¼¼¸Ð£© */
--accent: #8cc8ff;
--accent-soft: rgba(140, 200, 255, 0.1);
--accent-glow: rgba(140, 200, 255, 0.15);
/* ¹¦ÄÜÉ« - ¼«µÍ±¥ºÍ¶È */
--success: #90d4a0;
--success-soft: rgba(144, 212, 160, 0.08);
--error: #e08080;
--error-soft: rgba(224, 128, 128, 0.08);
/* ÊÔÓÃ/¼øÈ¨ - ²»ÓòÊÉ«£¬ÓÃÃ÷°µÇø·Ö */
--tag-free: rgba(255, 255, 255, 0.7);
--tag-free-bg: rgba(255, 255, 255, 0.06);
--tag-auth: var(--accent);
--tag-auth-bg: var(--accent-soft);
/* Safe Area */
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-left: env(safe-area-inset-left, 0px);
--safe-area-right: env(safe-area-inset-right, 0px);
}
html {
height: 100%;
height: -webkit-fill-available;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
line-height: 1.5;
min-height: 100%;
min-height: -webkit-fill-available;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: 100dvh;
min-height: -webkit-fill-available;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Header
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.app-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
padding-top: calc(12px + var(--safe-area-top));
padding-left: calc(20px + var(--safe-area-left));
padding-right: calc(20px + var(--safe-area-right));
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
flex-shrink: 0;
}
.header-logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
white-space: nowrap;
color: var(--text-primary);
}
.header-logo i {
color: var(--accent);
opacity: 0.9;
}
.header-status {
display: flex;
align-items: center;
gap: 8px;
}
.header-badge {
display: flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 12px;
font-size: 10px;
color: var(--text-muted);
transition: all 0.2s;
}
.header-badge i {
font-size: 5px;
opacity: 0.5;
}
.header-badge.active {
color: var(--text-secondary);
border-color: var(--border-light);
}
.header-badge.active i {
color: var(--accent);
opacity: 1;
}
.header-spacer { flex: 1; min-width: 10px; }
.header-close {
width: 36px; height: 36px; min-width: 36px;
border: 1px solid var(--border);
border-radius: 8px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s;
}
.header-close:hover {
background: rgba(255,255,255,0.05);
color: var(--text-primary);
border-color: var(--border-light);
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Layout
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.app-body {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
.app-sidebar {
width: 200px;
min-width: 200px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
padding: 16px 8px;
padding-left: calc(8px + var(--safe-area-left));
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
flex-shrink: 0;
}
.app-main {
flex: 1;
padding: 24px;
padding-right: calc(24px + var(--safe-area-right));
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Navigation
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 8px;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
font-size: 13px;
}
.nav-item:hover {
background: rgba(255,255,255,0.03);
color: var(--text-secondary);
}
.nav-item.active {
background: var(--accent-soft);
color: var(--accent);
font-weight: 500;
}
.nav-item i { width: 18px; text-align: center; }
.nav-divider {
height: 1px;
background: var(--border);
margin: 8px 0;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Views
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.view {
display: none;
max-width: 800px;
margin: 0 auto;
padding-bottom: 24px;
}
.view.active {
display: block;
animation: viewIn 0.2s ease;
}
@keyframes viewIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; }
}
.view-header { margin-bottom: 20px; }
.view-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 4px;
color: var(--text-primary);
}
.view-desc {
font-size: 13px;
color: var(--text-muted);
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Cards
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.card-title {
font-size: 11px;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Forms
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.form-group { margin-bottom: 16px; }
.form-group:last-child { margin-bottom: 0; }
.form-label {
display: block;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 6px;
font-weight: 500;
}
.form-hint {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
}
.input {
width: 100%;
padding: 10px 12px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 13px;
transition: all 0.15s;
}
.input:focus {
outline: none;
border-color: var(--border-focus);
background: rgba(255,255,255,0.06);
}
.input::placeholder { color: var(--text-dim); }
textarea.input {
min-height: 80px;
resize: vertical;
font-family: inherit;
}
select.input { cursor: pointer; }
.input-row { display: flex; gap: 8px; }
.input-row .input { flex: 1; min-width: 0; }
.checkbox-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.checkbox-row input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent);
}
.checkbox-row label {
font-size: 13px;
cursor: pointer;
color: var(--text-secondary);
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Buttons
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 16px;
min-height: 40px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.btn:hover {
background: var(--bg-elevated);
color: var(--text-primary);
border-color: var(--border-light);
}
.btn:active { transform: scale(0.98); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary {
background: var(--accent-soft);
border-color: rgba(140, 200, 255, 0.2);
color: var(--accent);
font-weight: 500;
}
.btn-primary:hover {
background: rgba(140, 200, 255, 0.15);
border-color: rgba(140, 200, 255, 0.3);
}
.btn-danger {
color: var(--error);
border-color: rgba(224, 128, 128, 0.2);
}
.btn-danger:hover {
background: var(--error-soft);
}
.btn-icon { width: 40px; padding: 0; }
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-sm { padding: 6px 10px; min-height: 32px; font-size: 12px; }
.btn-xs { padding: 4px 8px; min-height: 28px; font-size: 11px; }
.btn.saving { pointer-events: none; opacity: 0.7; }
.btn.save-success {
background: var(--success-soft) !important;
border-color: rgba(144, 212, 160, 0.3) !important;
color: var(--success) !important;
pointer-events: none;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Slider
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.slider-row { display: flex; align-items: center; gap: 12px; }
.slider-row input[type="range"] {
flex: 1;
height: 4px;
accent-color: var(--accent);
cursor: pointer;
opacity: 0.8;
}
.slider-row input[type="range"]:hover { opacity: 1; }
.slider-row .slider-val {
min-width: 50px;
text-align: right;
font-size: 13px;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Rules Editor
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.rules-editor {
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.rules-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border);
}
.rules-header-title {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.rules-list { max-height: 200px; overflow-y: auto; }
.rules-empty {
padding: 20px;
text-align: center;
color: var(--text-dim);
font-size: 12px;
}
.rule-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
}
.rule-item:last-child { border-bottom: none; }
.rule-item:hover { background: rgba(255,255,255,0.02); }
.rule-input {
flex: 1;
padding: 6px 10px;
min-width: 0;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 12px;
}
.rule-input:focus {
outline: none;
border-color: var(--border-focus);
}
.rule-input::placeholder { color: var(--text-dim); }
.rule-arrow {
color: var(--text-dim);
font-size: 11px;
flex-shrink: 0;
}
.rule-delete {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--text-dim);
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.rule-delete:hover {
background: var(--error-soft);
color: var(--error);
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Current Voice Card - ¼«¼ò°æ
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.current-voice-card {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
transition: all 0.2s;
}
.current-voice-card:hover {
border-color: var(--border-light);
}
.current-voice-label {
font-size: 10px;
color: var(--text-dim);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.current-voice-display {
display: flex;
align-items: center;
gap: 14px;
}
.current-voice-icon {
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--bg-elevated);
border: 1px solid var(--border);
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.2s;
}
.current-voice-card:hover .current-voice-icon {
color: var(--accent);
border-color: rgba(140, 200, 255, 0.2);
}
.current-voice-info { flex: 1; }
.current-voice-name {
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
}
.current-voice-source {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Source Badge - ¼«¼òºÚ°×°æ
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.source-badge {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 7px;
border-radius: 4px;
font-size: 9px;
font-weight: 600;
letter-spacing: 0.02em;
}
/* ÊÔÓà - °×É«/dzɫµ÷ */
.source-badge.trial {
background: var(--tag-free-bg);
color: var(--tag-free);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* ¼øÈ¨ - À¶É«µã׺ */
.source-badge.auth {
background: var(--tag-auth-bg);
color: var(--tag-auth);
border: 1px solid rgba(140, 200, 255, 0.15);
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Voice Tabs - ¼«¼ò°æ
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.voice-tabs {
display: flex;
gap: 2px;
margin-bottom: 16px;
background: var(--bg-tertiary);
padding: 3px;
border-radius: 10px;
border: 1px solid var(--border);
}
.voice-tab {
flex: 1;
padding: 10px 12px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--text-muted);
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.voice-tab:hover {
color: var(--text-secondary);
}
.voice-tab.active {
background: var(--bg-elevated);
color: var(--text-primary);
font-weight: 500;
}
.voice-tab-count {
background: var(--bg-input);
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
color: var(--text-dim);
}
.voice-tab.active .voice-tab-count {
background: var(--accent-soft);
color: var(--accent);
}
.voice-panel { display: none; }
.voice-panel.active { display: block; }
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Test Voice Box
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.test-voice-box {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
margin-bottom: 16px;
}
.test-voice-row {
display: flex;
gap: 8px;
align-items: center;
}
.test-voice-row .input { flex: 1; }
.test-voice-status {
font-size: 11px;
color: var(--text-dim);
margin-top: 6px;
min-height: 16px;
}
.test-voice-status.playing { color: var(--accent); }
.test-voice-status.error { color: var(--error); }
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Voice Filters
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.voice-filters {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.voice-filters select {
flex: 1;
min-width: 80px;
padding: 8px 10px;
font-size: 12px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
}
.voice-filters select:focus {
outline: none;
border-color: var(--border-focus);
}
.voice-search {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.voice-search .input { flex: 1; }
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Voice List - ¼«¼ò°æ
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.voice-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 320px;
overflow-y: auto;
}
.voice-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
}
.voice-item:hover {
border-color: var(--border-light);
background: var(--bg-elevated);
}
.voice-item.selected {
border-color: var(--accent);
background: var(--accent-soft);
}
.voice-item.in-my-list {
opacity: 0.5;
}
.voice-item.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.voice-item.disabled:hover {
border-color: var(--border);
background: var(--bg-tertiary);
}
.voice-item-radio {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid var(--text-dim);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.voice-item:hover .voice-item-radio {
border-color: var(--text-muted);
}
.voice-item.selected .voice-item-radio {
border-color: var(--accent);
background: var(--accent);
}
.voice-item.selected .voice-item-radio::after {
content: '?';
color: var(--bg-primary);
font-size: 9px;
font-weight: bold;
}
.voice-item-info { flex: 1; min-width: 0; }
.voice-item-name {
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
color: var(--text-primary);
}
.voice-item-meta {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
.voice-item-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
/* Editing State */
.voice-item.editing {
background: var(--accent-soft);
border-color: var(--accent);
}
.voice-item-edit-form {
display: none;
width: 100%;
margin-top: 8px;
}
.voice-item.editing .voice-item-edit-form {
display: flex;
gap: 8px;
}
.voice-item.editing .voice-item-info > *:not(.voice-item-edit-form) {
display: none;
}
.voice-item-edit-form input {
flex: 1;
padding: 6px 10px;
font-size: 12px;
}
/* Add Form */
.voice-add-form {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
.voice-add-row {
display: flex;
gap: 8px;
align-items: flex-end;
}
.voice-add-row .form-group {
flex: 1;
margin-bottom: 0;
}
.voice-add-row .form-label {
font-size: 11px;
margin-bottom: 4px;
}
.preset-save-row {
display: flex;
gap: 8px;
align-items: center;
margin-top: 12px;
}
.preset-save-row input {
flex: 1;
padding: 10px 12px;
font-size: 13px;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
API Status Box
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.api-status-box {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 16px;
}
.api-status-icon {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
background: var(--bg-elevated);
border: 1px solid var(--border);
}
.api-status-box.configured .api-status-icon {
color: var(--accent);
border-color: rgba(140, 200, 255, 0.2);
}
.api-status-box.not-configured .api-status-icon {
color: var(--text-dim);
}
.api-status-info { flex: 1; }
.api-status-title {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
}
.api-status-desc {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Stats Card
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.stats-card {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.stats-group {
display: flex;
gap: 32px;
}
.stats-item { text-align: center; }
.stats-value {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
.stats-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Tip Box - ¼«¼ò°æ
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.tip-box {
display: flex;
gap: 10px;
padding: 12px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.6;
}
.tip-box i {
color: var(--accent);
flex-shrink: 0;
margin-top: 2px;
opacity: 0.7;
}
.tip-box.warning {
border-left: 3px solid var(--accent);
}
.tip-box.warning i {
color: var(--accent);
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Guide Box
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.guide-box {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
margin-bottom: 16px;
}
.guide-box h3 {
font-size: 14px;
color: var(--text-primary);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.guide-box h3 i {
color: var(--accent);
opacity: 0.8;
}
.guide-box p {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 10px;
line-height: 1.6;
}
.guide-box ol, .guide-box ul {
margin-left: 18px;
font-size: 13px;
color: var(--text-secondary);
}
.guide-box li {
margin-bottom: 8px;
line-height: 1.6;
}
.guide-box a { color: var(--accent); }
.guide-box code {
background: var(--bg-input);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: var(--accent);
font-family: monospace;
border: 1px solid var(--border);
}
.guide-box pre {
background: var(--bg-primary);
padding: 12px;
border-radius: 6px;
font-size: 11px;
color: var(--text-secondary);
font-family: monospace;
overflow-x: auto;
margin: 10px 0;
line-height: 1.5;
border: 1px solid var(--border);
}
.guide-image {
margin-top: 12px;
width: 100%;
height: auto;
border: 1px solid var(--border);
border-radius: 8px;
display: block;
opacity: 0.9;
}
.guide-image:hover { opacity: 1; }
.guide-link {
display: block;
padding: 10px 12px;
margin: 10px 0;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 12px;
word-break: break-all;
}
.guide-link a { color: var(--accent); }
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Mobile Navigation
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
.mobile-nav {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: calc(60px + var(--safe-area-bottom));
padding-bottom: var(--safe-area-bottom);
background: var(--bg-secondary);
border-top: 1px solid var(--border);
z-index: 100;
}
.mobile-nav-inner {
display: flex;
height: 60px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-left: var(--safe-area-left);
padding-right: var(--safe-area-right);
}
.mobile-nav-inner::-webkit-scrollbar { display: none; }
.mobile-nav-item {
flex: 1;
min-width: 60px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--text-dim);
font-size: 10px;
cursor: pointer;
padding: 8px 4px;
transition: color 0.15s;
}
.mobile-nav-item i { font-size: 18px; }
.mobile-nav-item:hover { color: var(--text-muted); }
.mobile-nav-item.active { color: var(--accent); }
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Responsive
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
@media (max-width: 768px) {
.app-sidebar { display: none; }
.mobile-nav { display: block; }
.app-body {
padding-bottom: calc(60px + var(--safe-area-bottom));
}
.app-main {
padding: 16px;
padding-left: calc(16px + var(--safe-area-left));
padding-right: calc(16px + var(--safe-area-right));
padding-bottom: calc(80px + var(--safe-area-bottom));
}
.view { padding-bottom: 20px; }
.app-header {
padding: 10px 12px;
padding-top: calc(10px + var(--safe-area-top));
padding-left: calc(12px + var(--safe-area-left));
padding-right: calc(12px + var(--safe-area-right));
gap: 8px;
}
.header-logo span { display: none; }
.header-badge span { display: none; }
.header-close { width: 32px; height: 32px; min-width: 32px; font-size: 14px; }
.view-title { font-size: 18px; }
.card { padding: 16px; }
.form-row { grid-template-columns: 1fr; }
.voice-filters select { min-width: calc(50% - 4px); }
.stats-card { flex-direction: column; align-items: stretch; }
.stats-group { justify-content: space-around; }
.voice-add-row { flex-wrap: wrap; }
.voice-add-row .form-group { min-width: calc(50% - 4px); }
.preset-save-row { flex-wrap: wrap; }
.preset-save-row input { min-width: 100%; margin-bottom: 8px; }
.voice-tabs { flex-wrap: wrap; }
.voice-tab { min-width: calc(33% - 3px); font-size: 11px; padding: 8px 6px; }
}
@media (max-width: 400px) {
.app-header { padding: 8px 10px; }
.mobile-nav-item { min-width: 48px; font-size: 9px; }
.mobile-nav-item i { font-size: 16px; }
}
@media (hover: none) and (pointer: coarse) {
.btn { min-height: 44px; }
.input { min-height: 44px; padding: 12px; }
.nav-item { min-height: 44px; }
.header-close { width: 44px; height: 44px; min-width: 44px; }
.mobile-nav-item { min-height: 44px; }
}
/* ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
Scrollbar
¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.08);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.15);
}
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="app-container">
<header class="app-header">
<div class="header-logo"><i class="fa-solid fa-microphone"></i><span>TTS ÓïÒô</span></div>
<div class="header-status">
<div id="badge_trial" class="header-badge active"><i class="fa-solid fa-circle"></i><span>ÊÔÓÃ</span></div>
<div id="badge_auth" class="header-badge"><i class="fa-solid fa-circle"></i><span>¼øÈ¨</span></div>
</div>
<div class="header-spacer"></div>
<button id="tts_close" class="header-close">?</button>
</header>
<div class="app-body">
<nav class="app-sidebar">
<div class="nav-item active" data-view="config"><i class="fa-solid fa-sliders"></i>»ù´¡ÅäÖÃ</div>
<div class="nav-item" data-view="voice"><i class="fa-solid fa-user-astronaut"></i>ÒôÉ«¹ÜÀí</div>
<div class="nav-divider"></div>
<div class="nav-item" data-view="advanced"><i class="fa-solid fa-gear"></i>¸ß¼¶ÉèÖÃ</div>
<div class="nav-item" data-view="cache"><i class="fa-solid fa-database"></i>»º´æ¹ÜÀí</div>
<div class="nav-divider"></div>
<div class="nav-item" data-view="guide"><i class="fa-solid fa-circle-question"></i>ʹÓÃ˵Ã÷</div>
</nav>
<main class="app-main">
<!-- »ù´¡ÅäÖÃ -->
<div id="view-config" class="view active">
<div class="view-header">
<h2 class="view-title">»ù´¡ÅäÖÃ</h2>
<p class="view-desc">TTS ·þÎñÁ¬½ÓÓëÀʶÁÉèÖÃ</p>
</div>
<div class="tip-box" style="margin-bottom: 16px;">
<i class="fa-solid fa-info-circle"></i>
<div>
<strong>ÊÔÓÃÒôÉ«</strong> ¡ª ÎÞÐèÅäÖã¬Ê¹Óòå¼þ·þÎñÆ÷£¨11¸öÒôÉ«£©<br>
<strong>¼øÈ¨ÒôÉ«</strong> ¡ª ÐèÅäÖûðɽÒýÇæ API£¨200+ ÒôÉ« + ¸´¿Ì£©
</div>
</div>
<div class="card">
<div class="card-title">¼øÈ¨ÅäÖ㨿ÉÑ¡£©</div>
<div id="apiStatusBox" class="api-status-box not-configured">
<div class="api-status-icon"><i class="fa-solid fa-link-slash"></i></div>
<div class="api-status-info">
<div class="api-status-title">δÅäÖÃ</div>
<div class="api-status-desc">ÅäÖúó¿ÉʹÓÃÔ¤ÉèÒôÉ«¿âºÍ¸´¿ÌÒôÉ«</div>
</div>
</div>
<div class="form-group">
<label class="form-label">AppID</label>
<input type="text" id="appId" class="input" placeholder="»ðɽÒýÇæ AppID">
</div>
<div class="form-group">
<label class="form-label">Access Token</label>
<div class="input-row">
<input type="password" id="accessKey" class="input" placeholder="»ðɽÒýÇæ Access Token">
<button id="toggleKey" class="btn btn-icon"><i class="fa-solid fa-eye"></i></button>
</div>
<p class="form-hint">»ñÈ¡·½Ê½¼û¡¸Ê¹ÓÃ˵Ã÷¡¹Ò³</p>
</div>
</div>
<div class="card">
<div class="card-title">ÀʶÁÉèÖÃ</div>
<div class="checkbox-row">
<input type="checkbox" id="autoSpeak" checked>
<label for="autoSpeak">AI »Ø¸´ºó×Ô¶¯ÀʶÁ</label>
</div>
<div class="form-group">
<label class="form-label">ÓïËÙ</label>
<div class="slider-row">
<input type="range" id="speechRate" min="0.5" max="2.0" step="0.1" value="1.0">
<span class="slider-val" id="speechRateValue">1.0x</span>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Îı¾¹ýÂË</div>
<div class="form-group">
<label class="form-label">Ìø¹ýÇø¼ä</label>
<p class="form-hint" style="margin-bottom: 8px;">Óöµ½¡¸Æðʼ¡¹ºóÌø¹ý£¬Ö±µ½¡¸½áÊø¡¹¡£Æðʼ»ò½áÊø¿Éµ¥¶ÀÁô¿Õ¡£</p>
<div class="rules-editor">
<div class="rules-header">
<span class="rules-header-title">µ±Ç°¹æÔò</span>
<button class="btn btn-sm" id="addSkipRule"><i class="fa-solid fa-plus"></i> Ìí¼Ó</button>
</div>
<div class="rules-list" id="skipRulesList"></div>
</div>
</div>
<div class="form-group">
<div class="checkbox-row" style="margin-bottom: 8px;">
<input type="checkbox" id="readRangesEnabled">
<label for="readRangesEnabled">ÆôÓÃÖ»¶ÁÇø¼ä£¨½öÀʶÁÆ¥ÅäÄÚÈÝ£©</label>
</div>
<p class="form-hint" style="margin-bottom: 8px;">Æðʼ»ò½áÊø¿Éµ¥¶ÀÁô¿Õ¡£</p>
<div class="rules-editor">
<div class="rules-header">
<span class="rules-header-title">Ö»¶Á¹æÔò</span>
<button class="btn btn-sm" id="addReadRule"><i class="fa-solid fa-plus"></i> Ìí¼Ó</button>
</div>
<div class="rules-list" id="readRulesList"></div>
</div>
</div>
</div>
<button id="saveConfigBtn" class="btn btn-primary"><i class="fa-solid fa-floppy-disk"></i> ±£´æÅäÖÃ</button>
</div>
<!-- ÒôÉ«¹ÜÀí -->
<div id="view-voice" class="view">
<div class="view-header">
<h2 class="view-title">ÒôÉ«¹ÜÀí</h2>
<p class="view-desc">½«Ï²»¶µÄÒôɫ֨ÃüÃû¼ÓÈ롾ÎÒµÄÒôÉ«¡¿</p>
</div>
<div class="current-voice-card" id="currentVoiceCard">
<div class="current-voice-label">µ±Ç°Ä¬ÈÏÒôÉ«</div>
<div class="current-voice-display">
<div class="current-voice-icon"><i class="fa-solid fa-microphone-lines"></i></div>
<div class="current-voice-info">
<div class="current-voice-name" id="currentVoiceName">δѡÔñ</div>
<div class="current-voice-source" id="currentVoiceSource">ÇëÔÚÏ·½Ñ¡ÔñÒôÉ«</div>
</div>
</div>
</div>
<div class="voice-tabs">
<button class="voice-tab active" data-panel="myVoice">
<i class="fa-solid fa-star"></i> ÎÒµÄ
<span class="voice-tab-count" id="myVoiceCount">0</span>
</button>
<button class="voice-tab" data-panel="trialVoice">
<i class="fa-solid fa-flask"></i> ÊÔÓÃ
</button>
<button class="voice-tab" data-panel="authVoice">
<i class="fa-solid fa-list"></i> Ô¤Éè¿â
</button>
</div>
<!-- ÎÒµÄÒôÉ«Ãæ°å -->
<div class="voice-panel active" id="panel-myVoice">
<div class="card">
<div class="test-voice-box">
<div class="test-voice-row">
<input type="text" id="testTextMy" class="input" value="&#9834; ÎÒÄÜÏëµ½×îÀËÂþµÄÊÂ&#65374;&#9834;" placeholder="ÊäÈë²âÊÔÎı¾">
<button class="btn btn-primary" id="testMyVoiceBtn"><i class="fa-solid fa-play"></i> ÊÔÌý</button>
</div>
<div class="test-voice-status" id="testMyStatus"></div>
</div>
<p class="form-hint" style="margin-bottom: 12px;">
µã»÷Ñ¡ÖÐÉèΪĬÈÏ¡£<span class="source-badge trial">ÊÔÓÃ</span> ÎÞÐèÅäÖã¬<span class="source-badge auth">¼øÈ¨</span> ÐèÅäÖà API
</p>
<div class="voice-list" id="myVoiceList"></div>
<div id="myVoiceEmpty" class="rules-empty">
<i class="fa-solid fa-inbox" style="font-size: 24px; margin-bottom: 8px; display: block; opacity: 0.5;"></i>
ÔÝÎÞÒôÉ«£¬Çë´Ó¡¸ÊÔÓá¹»ò¡¸Ô¤Éè¿â¡¹Ìí¼Ó
</div>
<div class="voice-add-form">
<div class="form-label" style="margin-bottom: 8px;">ÊÖ¶¯Ìí¼Ó¸´¿ÌÒôÉ« <span class="source-badge auth">¼øÈ¨</span></div>
<div class="voice-add-row">
<div class="form-group">
<label class="form-label">ÒôÉ« ID</label>
<input type="text" id="newVoiceId" class="input" placeholder="Èç S_xxx">
</div>
<div class="form-group">
<label class="form-label">Ãû³Æ</label>
<input type="text" id="newVoiceName" class="input" placeholder="ÏÔʾÃû³Æ">
</div>
<button class="btn btn-primary" id="addMySpeakerBtn" style="margin-top: 18px;"><i class="fa-solid fa-plus"></i></button>
</div>
</div>
</div>
</div>
<!-- ÊÔÓÃÒôÉ«Ãæ°å -->
<div class="voice-panel" id="panel-trialVoice">
<div class="card">
<div class="test-voice-box">
<div class="test-voice-row">
<input type="text" id="testTextTrial" class="input" value="¿È~&#9834;×£ÄãÉúÈÕ¿ìÀÖ&#65374;" placeholder="ÊäÈë²âÊÔÎı¾">
<button class="btn btn-primary" id="testTrialVoiceBtn"><i class="fa-solid fa-play"></i> ÊÔÌý</button>
</div>
<div class="test-voice-status" id="testTrialStatus"></div>
</div>
<div class="voice-list" id="trialVoiceList"></div>
<div class="preset-save-row">
<input type="text" id="saveAsNameTrial" class="input" placeholder="±£´æÃû³Æ£¨¿ÉÑ¡£©">
<button class="btn btn-primary" id="saveToMyVoiceTrialBtn"><i class="fa-solid fa-plus"></i> Ìí¼Óµ½ÎÒµÄÒôÉ«</button>
</div>
</div>
</div>
<!-- Ô¤ÉèÒôÉ«¿âÃæ°å -->
<div class="voice-panel" id="panel-authVoice">
<div class="card">
<div id="authVoiceNotice" class="tip-box warning" style="margin-bottom: 16px;">
<i class="fa-solid fa-exclamation-triangle"></i>
<div>ʹÓÃÔ¤ÉèÒôÉ«¿âÐèÒªÏÈÅäÖüøÈ¨ API£¬ÇëǰÍù¡¸»ù´¡ÅäÖá¹Ò³ÃæÉèÖá£</div>
</div>
<div class="test-voice-box">
<div class="test-voice-row">
<input type="text" id="testTextAuth" class="input" value="Ô¤±¸£¬³ª£ºÁ½Ö»ÀÏ»¢£¬Á½Ö»ÀÏ»¢..." placeholder="ÊäÈë²âÊÔÎı¾">
<button class="btn btn-primary" id="testAuthVoiceBtn"><i class="fa-solid fa-play"></i> ÊÔÌý</button>
</div>
<div class="test-voice-status" id="testAuthStatus"></div>
</div>
<div class="voice-search">
<input type="text" id="voiceSearchInput" class="input" placeholder="ËÑË÷ÒôÉ«Ãû³Æ...">
</div>
<div class="voice-filters">
<select id="voiceGenderFilter">
<option value="all">È«²¿ÐÔ±ð</option>
<option value="female">Å®Éù</option>
<option value="male">ÄÐÉù</option>
<option value="other">ÆäËû</option>
</select>
<select id="voiceModelFilter">
<option value="all">È«²¿Ä£ÐÍ</option>
<option value="tts2">2.0</option>
<option value="tts1">1.0</option>
</select>
<select id="voiceLangFilter">
<option value="all">È«²¿ÓïÖÖ</option>
<option value="zh">ÖÐÎÄ</option>
<option value="en">Ó¢ÎÄ</option>
<option value="other">ÈÕÓï¡¢Î÷°àÑÀ</option>
<option value="multi">¶àÓï</option>
</select>
<select id="voiceSceneFilter">
<option value="all">È«²¿³¡¾°</option>
</select>
</div>
<div class="voice-list" id="authVoiceList"></div>
<div class="preset-save-row">
<input type="text" id="saveAsNameAuth" class="input" placeholder="±£´æÃû³Æ£¨¿ÉÑ¡£©">
<button class="btn btn-primary" id="saveToMyVoiceAuthBtn"><i class="fa-solid fa-plus"></i> Ìí¼Óµ½ÎÒµÄÒôÉ«</button>
</div>
</div>
</div>
<button class="btn btn-primary" id="saveVoiceBtn" style="margin-top: 8px;"><i class="fa-solid fa-floppy-disk"></i> ±£´æÒôÉ«ÉèÖÃ</button>
</div>
<!-- ¸ß¼¶ÉèÖà -->
<div id="view-advanced" class="view">
<div class="view-header">
<h2 class="view-title">¸ß¼¶ÉèÖÃ</h2>
<p class="view-desc">¼Æ·Ñ¡¢»º´æÓë¹ýÂËÑ¡Ï¼øÈ¨Ä£Ê½£©</p>
</div>
<div class="card">
<div class="card-title">¼Æ·ÑÓ뻺´æ</div>
<div class="checkbox-row">
<input type="checkbox" id="usageReturn">
<label for="usageReturn">·µ»Ø¼Æ·ÑÓÃÁ¿£¨text_words£©</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="serverCacheEnabled">
<label for="serverCacheEnabled">ÆôÓûðɽ·þÎñ¶Ë»º´æ</label>
</div>
</div>
<div class="card">
<div class="card-title">¹ýÂËÓëʶ±ð</div>
<div class="checkbox-row">
<input type="checkbox" id="disableMarkdownFilter">
<label for="disableMarkdownFilter">ÆôÓÃ Markdown ¹ýÂË</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="useTts11" checked>
<label for="useTts11">ÆôÓà 1.1 Ä£ÐÍ£¨½ö¶Ô seed-tts-1.0 ÉúЧ£©</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="disableEmojiFilter">
<label for="disableEmojiFilter">²»¹ýÂË Emoji</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="enableLanguageDetector">
<label for="enableLanguageDetector">ÆôÓÃ×Ô¶¯ÓïÖÖʶ±ð</label>
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="form-label">Ö¸¶¨ÓïÖÖ</label>
<input type="text" id="explicitLanguage" class="input" placeholder="È磺zh-cn / en / crosslingual">
</div>
<div class="form-row" style="margin-top: 12px;">
<div class="form-group">
<label class="form-label">À¨ºÅ¹ýÂ˳¤¶È</label>
<input type="number" id="maxLengthToFilterParenthesis" class="input" min="0" max="500">
</div>
<div class="form-group">
<label class="form-label">Òô¸ßµ÷Õû</label>
<input type="number" id="postProcessPitch" class="input" min="-12" max="12">
</div>
</div>
</div>
<button class="btn btn-primary" id="saveAdvancedBtn"><i class="fa-solid fa-floppy-disk"></i> ±£´æ¸ß¼¶ÉèÖÃ</button>
</div>
<!-- »º´æ¹ÜÀí -->
<div id="view-cache" class="view">
<div class="view-header">
<h2 class="view-title">»º´æ¹ÜÀí</h2>
<p class="view-desc">±¾µØÒôƵ»º´æÍ³¼ÆÓëÇåÀí</p>
</div>
<div class="card">
<div class="stats-card">
<div class="stats-group">
<div class="stats-item">
<div class="stats-value" id="cacheCount">0</div>
<div class="stats-label">»º´æÌõÊý</div>
</div>
<div class="stats-item">
<div class="stats-value" id="cacheSize">0 MB</div>
<div class="stats-label">Õ¼Óÿռä</div>
</div>
<div class="stats-item">
<div class="stats-value" id="cacheHits">0</div>
<div class="stats-label">ÃüÖÐ</div>
</div>
<div class="stats-item">
<div class="stats-value" id="cacheMisses">0</div>
<div class="stats-label">δÃüÖÐ</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">»º´æÅäÖÃ</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">»º´æÌìÊý</label>
<input type="number" id="cacheDays" class="input" min="1" max="30">
</div>
<div class="form-group">
<label class="form-label">×î´óÌõÊý</label>
<input type="number" id="cacheMaxEntries" class="input" min="10" max="5000">
</div>
<div class="form-group">
<label class="form-label">×î´óÈÝÁ¿ (MB)</label>
<input type="number" id="cacheMaxMB" class="input" min="10" max="5000">
</div>
</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" id="saveCacheBtn"><i class="fa-solid fa-floppy-disk"></i> ±£´æ</button>
<button class="btn" id="cacheRefreshBtn"><i class="fa-solid fa-arrows-rotate"></i> Ë¢ÐÂ</button>
<button class="btn" id="cacheClearExpiredBtn"><i class="fa-solid fa-broom"></i> ÇåÀí¹ýÆÚ</button>
<button class="btn btn-danger" id="cacheClearAllBtn"><i class="fa-solid fa-trash"></i> Çå¿ÕÈ«²¿</button>
</div>
</div>
<!-- ʹÓÃ˵Ã÷ -->
<div id="view-guide" class="view">
<div class="view-header">
<h2 class="view-title">ʹÓÃ˵Ã÷</h2>
<p class="view-desc">ÅäÒôÖ¸ÁîÓ뿪ͨÁ÷³Ì</p>
</div>
<div class="guide-box">
<h3><i class="fa-solid fa-terminal"></i> ÅäÒôÖ¸Áî</h3>
<p>¸ñʽ£º<code>[tts:speaker=ÒôÉ«Ãû;emotion=ÇéÐ÷;context=ÓïÆøÌáʾ]</code> ·ÅÔÚÕýÎÄǰһÐÐ</p>
<p>speaker¡¢emotion¡¢context Èý¸ö²ÎÊý¿ÉÈÎÒâ×éºÏ¡¢ÈÎÒâ˳Ðò£¬Ó÷ֺŷָô</p>
<p>ÿÓöµ½Ò»¸öР<code>[tts:...]</code> ¿é»á·Ö¶ÎÀʶÁ£¬°´Ë³Ðò²¥·Å</p>
<p>δд <code>speaker=</code> µÄ¿éʹÓõ±Ç°Ñ¡ÖеÄĬÈÏÒôÉ«</p>
<p style="margin-top: 12px;"><strong>ÒôÉ«£¨speaker£©</strong></p>
<p>Ö»ÄÜÖ¸¶¨"ÎÒµÄÒôÉ«"Öб£´æµÄÃû³Æ¡£ÀýÈç±£´æÁËÃûΪ"С°×"µÄÒôÉ«£¬Ôò¿ÉÓà <code>speaker=С°×</code>¡£</p>
<p style="margin-top: 12px;"><strong>Çé¸Ð£¨emotion£©¿ÉÓÃÖµ£º</strong></p>
<pre>ÖÐÎÄ£º¿ªÐÄ¡¢±¯ÉË¡¢ÉúÆø¡¢¾ªÑÈ¡¢¿Ö¾å¡¢Ñá¶ñ¡¢¼¤¶¯¡¢ÀäÄ®¡¢ÖÐÐÔ¡¢¾ÚÉ¥¡¢Èö½¿¡¢º¦Ðß¡¢°²Î¿¡¢¹ÄÀø¡¢ÅØÏø¡¢½¹¼±¡¢ÎÂÈá¡¢½²¹ÊÊ¡¢×ÔÈ»½²Êö¡¢Çé¸Ðµç̨¡¢´ÅÐÔ¡¢¹ã¸æÓªÏú¡¢ÆøÅÝÒô¡¢µÍÓï¡¢ÐÂÎŲ¥±¨¡¢ÓéÀÖ°ËØÔ¡¢·½ÑÔ¡¢¶Ô»°¡¢ÏÐÁÄ¡¢ÎÂů¡¢ÉîÇ顢ȨÍþ
Ó¢ÎÄ£ºhappy, sad, angry, surprised, fear, hate, excited, coldness, neutral, depressed, lovey-dovey, shy, comfort, tension, tender, storytelling, radio, magnetic, advertising, vocal-fry, asmr, news, entertainment, dialect, chat, warm, affectionate, authoritative</pre>
<p style="margin-top: 12px;"><strong>ÓïÆøÌáʾ£¨context£©</strong>½ö¶Ô seed-tts-2.0 ÉúЧ£º</p>
<p>ÀýÈ磺"ÓøüίÇüµÄÓïÆø"¡¢"·ÅÂýÒ»µã£¬Ñ¹µÍÒôÁ¿"</p>
</div>
<div class="guide-box">
<h3><i class="fa-solid fa-user-plus"></i> ¸´¿ÌÒôɫʹÓÃ</h3>
<ol>
<li>ÔÚ»ðɽ¹ÙÍø¸´¿ÌÒôÉ«</li>
<li>»ñÈ¡ÒôÉ«ID£¨¸ñʽ <code>S_xxxxxxxx</code>£©</li>
<li>ÔÚ"ÒôÉ«¹ÜÀí" ¡ú "ÎÒµÄÒôÉ«"ÖÐÌí¼Ó</li>
</ol>
</div>
<div class="tip-box warning" style="margin-bottom: 16px;">
<i class="fa-solid fa-exclamation-triangle"></i>
<div><strong>ÒÔÏÂÊǼøÈ¨Ä£Ê½µÄ¿ªÍ¨½Ì³Ì</strong>£¬ÊÔÓÃÒôÉ«ÎÞÐèÅäÖá£</div>
</div>
<div class="guide-box">
<h3><i class="fa-solid fa-server"></i> ¿ªÆô CORS ´úÀí</h3>
<ol>
<li>´ò¿ª¾Æ¹ÝĿ¼µÄ config.yaml</li>
<li>½« enableCorsProxy ¸ÄΪ true ²¢±£´æ</li>
<li>ÖØÆô¾Æ¹Ý£¨ÖØÆôÈÝÆ÷/½ø³Ì£¬²»ÊÇ F5 ˢУ©</li>
</ol>
</div>
<div class="guide-box">
<h3><i class="fa-solid fa-check-circle"></i> ¿ªÍ¨·þÎñ£¨ÍƼöÒ»´ÎÐÔ¿ªÍ¨È«²¿£©</h3>
<div class="guide-link">
<a href="https://console.volcengine.com/speech/new/setting/activate" target="_blank">https://console.volcengine.com/speech/new/setting/activate</a>
</div>
<img class="guide-image" src="¿ªÍ¨¹ÜÀí.png" alt="¿ªÍ¨¹ÜÀí">
</div>
<div class="guide-box">
<h3><i class="fa-solid fa-key"></i> »ñÈ¡ Access Token / AppID</h3>
<div class="guide-link">
<a href="https://console.volcengine.com/speech/service/8" target="_blank">https://console.volcengine.com/speech/service/8</a>
</div>
<img class="guide-image" src="»ñÈ¡IDºÍKEY.png" alt="»ñÈ¡IDºÍKEY">
</div>
<div class="guide-box">
<h3><i class="fa-solid fa-microphone-lines"></i> ÉùÒô¸´¿ÌÈë¿Ú(¸´¿ÌºóÈ¥ÒôÉ«¿âÄÃID)</h3>
<div class="guide-link">
<a href="https://console.volcengine.com/speech/new/experience/clone" target="_blank">https://console.volcengine.com/speech/new/experience/clone</a>
</div>
<img class="guide-image" src="ÉùÒô¸´¿Ì.png" alt="ÉùÒô¸´¿Ì">
</div>
</div>
</main>
</div>
<nav class="mobile-nav">
<div class="mobile-nav-inner">
<div class="mobile-nav-item active" data-view="config"><i class="fa-solid fa-sliders"></i><span>ÅäÖÃ</span></div>
<div class="mobile-nav-item" data-view="voice"><i class="fa-solid fa-user-astronaut"></i><span>ÒôÉ«</span></div>
<div class="mobile-nav-item" data-view="advanced"><i class="fa-solid fa-gear"></i><span>¸ß¼¶</span></div>
<div class="mobile-nav-item" data-view="cache"><i class="fa-solid fa-database"></i><span>»º´æ</span></div>
<div class="mobile-nav-item" data-view="guide"><i class="fa-solid fa-circle-question"></i><span>˵Ã÷</span></div>
</div>
</nav>
</div>
<script src="tts-voices.js"></script>
<script>
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// ³£Á¿Óë״̬
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
const PARENT_ORIGIN = (() => {
try { return new URL(document.referrer).origin; } catch { return window.location.origin; }
})();
const post = (type, payload) => parent.postMessage({ type, payload }, PARENT_ORIGIN);
let config = {};
let mySpeakers = [];
let selectedVoiceValue = '';
let selectedTrialVoiceValue = '';
let selectedAuthVoiceValue = '';
let editingVoiceValue = null;
let activeSaveBtn = null;
const TRIAL_VOICES = [
{ key: 'female_1', name: 'ÌÒØ²', tag: 'ÌðÃÛÏÉ×Ó', gender: 'female' },
{ key: 'female_2', name: '˪»ª', tag: 'ÇåÀäÏÉ×Ó', gender: 'female' },
{ key: 'female_3', name: '¹Ë½ã', tag: 'Óù½ãÑÌɤ', gender: 'female' },
{ key: 'female_4', name: 'ËÕ·Æ', tag: 'ÓÅÑÅÖªÐÔ', gender: 'female' },
{ key: 'female_5', name: '¼ÎÐÀ', tag: '¸Û·çÌðÐÄ', gender: 'female' },
{ key: 'female_6', name: 'Çà÷', tag: 'ÇåÐãÉÙÄêÒô', gender: 'female' },
{ key: 'female_7', name: '¿ÉÀò', tag: 'ÄÌÒôÂÜÀò', gender: 'female' },
{ key: 'male_1', name: 'Ò¹èÉ', tag: '´ÅÐÔµÍÒô', gender: 'male' },
{ key: 'male_2', name: '¾ýÔó', tag: 'ÎÂÈó¹«×Ó', gender: 'male' },
{ key: 'male_3', name: 'ãåÑô', tag: '³ÁÎÈůÄÐ', gender: 'male' },
{ key: 'male_4', name: 'è÷ÐÁ', tag: 'Çà´ºÉÙÄê', gender: 'male' },
];
const TRIAL_VOICE_KEYS = new Set(TRIAL_VOICES.map(v => v.key));
const AUTH_VOICE_DATA = Array.isArray(window.XB_TTS_VOICE_DATA) ? window.XB_TTS_VOICE_DATA : [];
const TTS2_VOICES = new Set((window.XB_TTS_TTS2_VOICE_INFO || []).map(item => item.value));
let authVoiceList = [];
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// ¹¤¾ßº¯Êý
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
const $ = id => document.getElementById(id);
const $$ = sel => document.querySelectorAll(sel);
function escapeHtml(str) {
return String(str || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function switchView(viewId) {
$$('.view').forEach(v => v.classList.remove('active'));
$$('.nav-item, .mobile-nav-item').forEach(n => n.classList.remove('active'));
$(`view-${viewId}`)?.classList.add('active');
$$(`[data-view="${viewId}"]`).forEach(n => n.classList.add('active'));
}
function setSavingState(btn) {
if (!btn) return;
activeSaveBtn = btn;
const i = btn.querySelector('i');
if (i) { btn._origIcon = i.className; i.className = 'fa-solid fa-spinner fa-spin'; }
btn.classList.add('saving');
}
function handleSaveResult(success) {
if (!activeSaveBtn) return;
const btn = activeSaveBtn;
activeSaveBtn = null;
btn.classList.remove('saving');
const i = btn.querySelector('i');
if (success && i) {
i.className = 'fa-solid fa-check';
btn.classList.add('save-success');
setTimeout(() => { btn.classList.remove('save-success'); i.className = btn._origIcon || 'fa-solid fa-floppy-disk'; }, 1500);
} else if (i) {
i.className = btn._origIcon || 'fa-solid fa-floppy-disk';
}
}
function setTestStatus(elId, status, text) {
const el = $(elId);
if (!el) return;
el.textContent = text;
el.className = 'test-voice-status' + (status ? ' ' + status : '');
}
function getVoiceSource(value) {
if (!value) return 'free';
if (TRIAL_VOICE_KEYS.has(value)) return 'free';
return 'auth';
}
function isAuthConfigured() {
return !!(config?.volc?.appId && config?.volc?.accessKey);
}
function isInMyList(value) {
return mySpeakers.some(s => s.value === value);
}
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// ¹æÔò±à¼­Æ÷
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
function renderRulesList(listEl, rules, type) {
if (!rules?.length) {
listEl.innerHTML = `<div class="rules-empty">ÔÝÎÞ¹æÔò</div>`;
return;
}
listEl.innerHTML = rules.map((rule, idx) => `
<div class="rule-item" data-idx="${idx}">
<input type="text" class="rule-input rule-start" value="${escapeHtml(rule.start || '')}" placeholder="Æðʼ£¨¿ÉΪ¿Õ£©">
<span class="rule-arrow">¡ú</span>
<input type="text" class="rule-input rule-end" value="${escapeHtml(rule.end || '')}" placeholder="½áÊø£¨¿ÉΪ¿Õ£©">
<button class="rule-delete" data-type="${type}" data-idx="${idx}"><i class="fa-solid fa-times"></i></button>
</div>
`).join('');
}
function collectRules(listEl) {
const rules = [];
listEl.querySelectorAll('.rule-item').forEach(item => {
const start = item.querySelector('.rule-start')?.value?.trim() || '';
const end = item.querySelector('.rule-end')?.value?.trim() || '';
if (start || end) rules.push({ start, end });
});
return rules;
}
function initRulesEditors() {
$('addSkipRule').addEventListener('click', () => {
const rules = collectRules($('skipRulesList'));
rules.push({ start: '', end: '' });
renderRulesList($('skipRulesList'), rules, 'skip');
});
$('addReadRule').addEventListener('click', () => {
const rules = collectRules($('readRulesList'));
rules.push({ start: '', end: '' });
renderRulesList($('readRulesList'), rules, 'read');
});
document.addEventListener('click', e => {
const deleteBtn = e.target.closest('.rule-delete');
if (!deleteBtn) return;
const type = deleteBtn.dataset.type;
const idx = parseInt(deleteBtn.dataset.idx);
const listEl = type === 'skip' ? $('skipRulesList') : $('readRulesList');
const rules = collectRules(listEl);
rules.splice(idx, 1);
renderRulesList(listEl, rules, type);
});
}
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// ÒôÉ«´¦Àí
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
function detectGenderByValue(value) {
const v = String(value || '').toLowerCase();
if (v.includes('female')) return 'female';
if (v.includes('male')) return 'male';
return 'other';
}
function detectModel(value) { return TTS2_VOICES.has(String(value).trim()) ? 'tts2' : 'tts1'; }
function detectTags(value, model) {
const tags = [];
if (model === 'tts2') tags.push('2.0');
if (model === 'tts1') tags.push('1.0');
if (String(value).includes('emo')) tags.push('¶àÇé¸Ð');
return tags;
}
function buildAuthVoiceList() {
authVoiceList = AUTH_VOICE_DATA.map(item => {
const value = item.value;
const name = item.name || value;
const gender = detectGenderByValue(value);
const genderLabel = gender === 'female' ? 'Å®' : gender === 'male' ? 'ÄÐ' : 'ÆäËû';
const model = detectModel(value);
const scene = String(item.scene || '').trim();
const tags = detectTags(value, model);
if (scene && !tags.includes(scene)) tags.push(scene);
let language;
if (model === 'tts2') {
language = 'multi';
} else if (value.startsWith('en_')) {
language = 'en';
} else if (value.startsWith('multi_')) {
language = 'other';
} else {
language = 'zh';
}
return { value, name, gender, genderLabel, model, language, scene, tags };
});
}
function formatTagLabel(tags) { return tags.length ? ` [${tags.join('/')}]` : ''; }
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// UI ¸üÐÂ
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
function updateApiStatus() {
const configured = isAuthConfigured();
const box = $('apiStatusBox');
const icon = box.querySelector('.api-status-icon i');
const title = box.querySelector('.api-status-title');
const desc = box.querySelector('.api-status-desc');
const badge = $('badge_auth');
if (configured) {
box.className = 'api-status-box configured';
icon.className = 'fa-solid fa-link';
title.textContent = 'ÒÑÅäÖÃ';
desc.textContent = '¿ÉʹÓÃÔ¤ÉèÒôÉ«¿âºÍ¸´¿ÌÒôÉ«';
badge.className = 'header-badge active';
$('authVoiceNotice').style.display = 'none';
} else {
box.className = 'api-status-box not-configured';
icon.className = 'fa-solid fa-link-slash';
title.textContent = 'δÅäÖÃ';
desc.textContent = 'ÅäÖúó¿ÉʹÓÃÔ¤ÉèÒôÉ«¿âºÍ¸´¿ÌÒôÉ«';
badge.className = 'header-badge';
$('authVoiceNotice').style.display = 'flex';
}
}
function updateCurrentVoiceDisplay() {
const nameEl = $('currentVoiceName');
const sourceEl = $('currentVoiceSource');
if (!selectedVoiceValue) {
nameEl.innerHTML = 'δѡÔñ';
sourceEl.textContent = 'ÇëÔÚÏ·½Ñ¡ÔñÒôÉ«';
return;
}
const myVoice = mySpeakers.find(s => s.value === selectedVoiceValue);
const source = myVoice?.source || getVoiceSource(selectedVoiceValue);
const sourceBadge = source === 'free'
? '<span class="source-badge trial">ÊÔÓÃ</span>'
: '<span class="source-badge auth">¼øÈ¨</span>';
if (myVoice) {
nameEl.innerHTML = `${escapeHtml(myVoice.name)} ${sourceBadge}`;
sourceEl.textContent = 'ÎÒµÄÒôÉ«';
} else if (source === 'free') {
const tv = TRIAL_VOICES.find(v => v.key === selectedVoiceValue);
nameEl.innerHTML = `${escapeHtml(tv?.name || selectedVoiceValue)} ${sourceBadge}`;
sourceEl.textContent = tv?.tag || 'ÊÔÓÃÒôÉ«';
} else {
const av = authVoiceList.find(v => v.value === selectedVoiceValue);
nameEl.innerHTML = `${escapeHtml(av?.name || selectedVoiceValue)} ${sourceBadge}`;
sourceEl.textContent = 'Ô¤ÉèÒôÉ«£¨½¨ÒéÏÈÌí¼Óµ½ÎÒµÄÒôÉ«£©';
}
}
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// äÖȾÒôÉ«Áбí
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
function renderMyVoiceList() {
const listEl = $('myVoiceList');
const emptyEl = $('myVoiceEmpty');
$('myVoiceCount').textContent = mySpeakers.length;
if (!mySpeakers.length) {
listEl.innerHTML = '';
listEl.style.display = 'none';
emptyEl.style.display = 'block';
return;
}
listEl.style.display = 'flex';
emptyEl.style.display = 'none';
const authOk = isAuthConfigured();
listEl.innerHTML = mySpeakers.map(s => {
const isSelected = s.value === selectedVoiceValue;
const isEditing = s.value === editingVoiceValue;
const source = s.source || getVoiceSource(s.value);
const canPlay = source === 'free' || authOk;
const sourceBadge = source === 'free'
? '<span class="source-badge trial">ÊÔÓÃ</span>'
: '<span class="source-badge auth">¼øÈ¨</span>';
return `
<div class="voice-item${isSelected ? ' selected' : ''}${isEditing ? ' editing' : ''}${!canPlay ? ' disabled' : ''}"
data-value="${escapeHtml(s.value)}" data-source="${source}">
<div class="voice-item-radio"></div>
<div class="voice-item-info">
<div class="voice-item-name">${escapeHtml(s.name || s.value)} ${sourceBadge}</div>
<div class="voice-item-meta">${!canPlay ? 'ÐèÅäÖüøÈ¨' : escapeHtml(s.value.slice(0, 25))}</div>
<div class="voice-item-edit-form">
<input type="text" class="input voice-edit-input" value="${escapeHtml(s.name || '')}" placeholder="ÊäÈëÐÂÃû³Æ">
<button class="btn btn-xs btn-primary voice-edit-save"><i class="fa-solid fa-check"></i></button>
<button class="btn btn-xs voice-edit-cancel"><i class="fa-solid fa-times"></i></button>
</div>
</div>
<div class="voice-item-actions">
<button class="btn btn-xs voice-rename-btn" data-value="${escapeHtml(s.value)}" title="¸ÄÃû">
<i class="fa-solid fa-pen"></i>
</button>
<button class="btn btn-xs btn-danger voice-delete-btn" data-value="${escapeHtml(s.value)}" title="ɾ³ý">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>`;
}).join('');
bindMyVoiceEvents(listEl);
}
function bindMyVoiceEvents(listEl) {
listEl.querySelectorAll('.voice-item').forEach(item => {
item.addEventListener('click', e => {
if (e.target.closest('button') || e.target.closest('input')) return;
if (item.classList.contains('disabled')) {
post('xb-tts:toast', { type: 'error', message: 'ÇëÏÈÅäÖüøÈ¨ API' });
return;
}
if (editingVoiceValue) return;
selectedVoiceValue = item.dataset.value;
renderMyVoiceList();
updateCurrentVoiceDisplay();
});
});
listEl.querySelectorAll('.voice-rename-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
editingVoiceValue = btn.dataset.value;
renderMyVoiceList();
listEl.querySelector('.voice-edit-input')?.focus();
});
});
listEl.querySelectorAll('.voice-edit-save').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const item = mySpeakers.find(s => s.value === editingVoiceValue);
const input = btn.closest('.voice-item').querySelector('.voice-edit-input');
if (item && input?.value?.trim()) {
item.name = input.value.trim();
post('xb-tts:save-config', collectForm());
}
editingVoiceValue = null;
renderMyVoiceList();
updateCurrentVoiceDisplay();
});
});
listEl.querySelectorAll('.voice-edit-cancel').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
editingVoiceValue = null;
renderMyVoiceList();
});
});
listEl.querySelectorAll('.voice-delete-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const value = btn.dataset.value;
const item = mySpeakers.find(s => s.value === value);
if (confirm(`ɾ³ý¡¸${item?.name || value}¡¹£¿`)) {
mySpeakers = mySpeakers.filter(s => s.value !== value);
if (selectedVoiceValue === value) {
selectedVoiceValue = mySpeakers[0]?.value || '';
}
renderMyVoiceList();
renderTrialVoiceList();
renderAuthVoiceList();
updateCurrentVoiceDisplay();
post('xb-tts:save-config', collectForm());
}
});
});
}
function renderTrialVoiceList() {
const listEl = $('trialVoiceList');
listEl.innerHTML = TRIAL_VOICES.map(v => {
const isSelected = v.key === selectedTrialVoiceValue;
const inMy = isInMyList(v.key);
return `
<div class="voice-item${isSelected ? ' selected' : ''}${inMy ? ' in-my-list' : ''}" data-value="${v.key}">
<div class="voice-item-radio"></div>
<div class="voice-item-info">
<div class="voice-item-name">${escapeHtml(v.name)}${inMy ? ' <span class="source-badge trial">ÒÑÌí¼Ó</span>' : ''}</div>
<div class="voice-item-meta">${escapeHtml(v.tag)} ¡¤ ${v.gender === 'female' ? 'Å®' : 'ÄÐ'}</div>
</div>
</div>`;
}).join('');
listEl.querySelectorAll('.voice-item').forEach(item => {
item.addEventListener('click', () => {
selectedTrialVoiceValue = item.dataset.value;
renderTrialVoiceList();
});
});
}
function renderAuthVoiceList() {
const gender = $('voiceGenderFilter')?.value || 'all';
const model = $('voiceModelFilter')?.value || 'all';
const language = $('voiceLangFilter')?.value || 'all';
const scene = $('voiceSceneFilter')?.value || 'all';
const search = ($('voiceSearchInput')?.value || '').toLowerCase().trim();
const list = authVoiceList.filter(item => {
if (gender !== 'all' && item.gender !== gender) return false;
if (model !== 'all' && item.model !== model) return false;
if (language !== 'all' && item.language !== language) return false;
if (scene !== 'all' && item.scene !== scene) return false;
if (search && !item.name.toLowerCase().includes(search) && !item.value.toLowerCase().includes(search)) return false;
return true;
}).sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN'));
const listEl = $('authVoiceList');
if (!list.length) {
listEl.innerHTML = '<div class="rules-empty">ûÓÐÆ¥ÅäµÄÒôÉ«</div>';
return;
}
listEl.innerHTML = list.map(item => {
const isSelected = item.value === selectedAuthVoiceValue;
const inMy = isInMyList(item.value);
return `
<div class="voice-item${isSelected ? ' selected' : ''}${inMy ? ' in-my-list' : ''}" data-value="${escapeHtml(item.value)}">
<div class="voice-item-radio"></div>
<div class="voice-item-info">
<div class="voice-item-name">${escapeHtml(item.name)}${inMy ? ' <span class="source-badge auth">ÒÑÌí¼Ó</span>' : ''}</div>
<div class="voice-item-meta">${escapeHtml(item.genderLabel)}${formatTagLabel(item.tags)}</div>
</div>
</div>`;
}).join('');
listEl.querySelectorAll('.voice-item').forEach(item => {
item.addEventListener('click', () => {
selectedAuthVoiceValue = item.dataset.value;
renderAuthVoiceList();
});
});
}
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// Êý¾Ý´¦Àí
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
function normalizeMySpeakers(list) {
if (!Array.isArray(list)) return [];
return list.map(item => ({
name: String(item?.name || '').trim(),
value: String(item?.value || '').trim(),
source: item?.source || getVoiceSource(item?.value || ''),
})).filter(item => item.value);
}
function applyCacheStats(stats = {}) {
$('cacheCount').textContent = stats.count ?? 0;
$('cacheSize').textContent = `${stats.sizeMB ?? 0} MB`;
$('cacheHits').textContent = stats.hits ?? 0;
$('cacheMisses').textContent = stats.misses ?? 0;
}
function fillForm(cfg) {
config = cfg || {};
mySpeakers = normalizeMySpeakers(cfg.volc?.mySpeakers);
selectedVoiceValue = cfg.volc?.defaultSpeaker || '';
$('appId').value = cfg.volc?.appId || '';
$('accessKey').value = cfg.volc?.accessKey || '';
$('autoSpeak').checked = cfg.autoSpeak !== false;
const speechRate = Number.isFinite(cfg.volc?.speechRate) ? cfg.volc.speechRate : 1.0;
$('speechRate').value = speechRate;
$('speechRateValue').textContent = `${speechRate.toFixed(1)}x`;
renderRulesList($('skipRulesList'), cfg.skipRanges || [], 'skip');
renderRulesList($('readRulesList'), cfg.readRanges || [], 'read');
$('readRangesEnabled').checked = cfg.readRangesEnabled === true;
$('usageReturn').checked = cfg.volc?.usageReturn === true;
$('serverCacheEnabled').checked = cfg.volc?.serverCacheEnabled === true;
$('disableMarkdownFilter').checked = cfg.volc?.disableMarkdownFilter !== false;
$('useTts11').checked = cfg.volc?.useTts11 !== false;
$('disableEmojiFilter').checked = cfg.volc?.disableEmojiFilter === true;
$('enableLanguageDetector').checked = cfg.volc?.enableLanguageDetector === true;
$('explicitLanguage').value = cfg.volc?.explicitLanguage || '';
$('maxLengthToFilterParenthesis').value = cfg.volc?.maxLengthToFilterParenthesis ?? 100;
$('postProcessPitch').value = cfg.volc?.postProcessPitch ?? 0;
$('cacheDays').value = cfg.volc?.cacheDays ?? 7;
$('cacheMaxEntries').value = cfg.volc?.cacheMaxEntries ?? 200;
$('cacheMaxMB').value = cfg.volc?.cacheMaxMB ?? 200;
applyCacheStats(cfg.cacheStats || {});
updateApiStatus();
renderMyVoiceList();
renderTrialVoiceList();
renderAuthVoiceList();
updateCurrentVoiceDisplay();
}
function inferResourceIdBySpeaker(value) {
const v = (value || '').trim().toLowerCase();
if (v.startsWith('icl_') || v.startsWith('s_')) return 'seed-icl-2.0';
if (TTS2_VOICES.has(value)) return 'seed-tts-2.0';
return 'seed-tts-1.0';
}
function collectForm() {
const speaker = selectedVoiceValue;
const source = getVoiceSource(speaker);
return {
volc: {
appId: $('appId').value.trim(),
accessKey: $('accessKey').value.trim(),
defaultResourceId: source === 'auth' ? inferResourceIdBySpeaker(speaker) : '',
defaultSpeaker: speaker,
mySpeakers: mySpeakers,
usageReturn: $('usageReturn').checked,
serverCacheEnabled: $('serverCacheEnabled').checked,
disableMarkdownFilter: $('disableMarkdownFilter').checked,
useTts11: $('useTts11').checked,
disableEmojiFilter: $('disableEmojiFilter').checked,
enableLanguageDetector: $('enableLanguageDetector').checked,
explicitLanguage: $('explicitLanguage').value.trim(),
maxLengthToFilterParenthesis: Number($('maxLengthToFilterParenthesis').value) || 0,
postProcessPitch: Number($('postProcessPitch').value) || 0,
speechRate: Number($('speechRate').value) || 1.0,
localCacheEnabled: true,
cacheDays: Math.max(1, Number($('cacheDays').value) || 7),
cacheMaxEntries: Math.max(10, Number($('cacheMaxEntries').value) || 200),
cacheMaxMB: Math.max(10, Number($('cacheMaxMB').value) || 200),
},
autoSpeak: $('autoSpeak').checked,
skipRanges: collectRules($('skipRulesList')),
readRanges: collectRules($('readRulesList')),
readRangesEnabled: $('readRangesEnabled').checked,
};
}
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// ÊÔÌý¹¦ÄÜ
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
function doTestVoice(speaker, source, textElId, statusElId) {
const text = $(textElId)?.value?.trim() || 'ÄãºÃ£¬ÕâÊÇÒ»¶Î²âÊÔÓïÒô¡£';
if (!speaker) {
setTestStatus(statusElId, 'error', 'ÇëÏÈÑ¡ÔñÒ»¸öÒôÉ«');
return;
}
if (source === 'auth' && !isAuthConfigured()) {
setTestStatus(statusElId, 'error', 'ÇëÏÈÅäÖüøÈ¨ API');
return;
}
setTestStatus(statusElId, 'playing', 'ÕýÔںϳÉ...');
post('xb-tts:test-speak', {
text,
speaker,
source,
resourceId: source === 'auth' ? inferResourceIdBySpeaker(speaker) : '',
});
}
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// ÏûÏ¢´¦Àí
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
window.addEventListener('message', ev => {
if (ev.origin !== PARENT_ORIGIN || ev.source !== parent) return;
if (!ev.data?.type?.startsWith('xb-tts:')) return;
const { type, payload } = ev.data;
switch (type) {
case 'xb-tts:config':
fillForm(payload);
break;
case 'xb-tts:config-saved':
fillForm(payload);
handleSaveResult(true);
post('xb-tts:toast', { type: 'success', message: 'ÅäÖÃÒѱ£´æ' });
break;
case 'xb-tts:config-save-error':
handleSaveResult(false);
post('xb-tts:toast', { type: 'error', message: payload?.message || '±£´æÊ§°Ü' });
break;
case 'xb-tts:test-done':
['testMyStatus', 'testTrialStatus', 'testAuthStatus'].forEach(id => setTestStatus(id, 'playing', '²¥·ÅÖÐ...'));
setTimeout(() => ['testMyStatus', 'testTrialStatus', 'testAuthStatus'].forEach(id => setTestStatus(id, '', '')), 3000);
break;
case 'xb-tts:test-error':
const errMsg = 'ʧ°Ü: ' + (payload || 'δ֪´íÎó');
['testMyStatus', 'testTrialStatus', 'testAuthStatus'].forEach(id => setTestStatus(id, 'error', errMsg));
break;
case 'xb-tts:cache-stats':
applyCacheStats(payload || {});
break;
}
});
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
// ³õʼ»¯
// ¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T¨T
document.addEventListener('DOMContentLoaded', () => {
buildAuthVoiceList();
initRulesEditors();
const scenes = new Set();
authVoiceList.forEach(item => { if (item.scene) scenes.add(item.scene); });
$('voiceSceneFilter').innerHTML = '<option value="all">È«²¿³¡¾°</option>' +
Array.from(scenes).sort((a, b) => a.localeCompare(b, 'zh-Hans-CN')).map(s => `<option value="${s}">${s}</option>`).join('');
$$('.nav-item, .mobile-nav-item').forEach(item => item.addEventListener('click', () => switchView(item.dataset.view)));
$('tts_close').addEventListener('click', () => post('xb-tts:close'));
$('toggleKey').addEventListener('click', () => {
const input = $('accessKey');
const icon = $('toggleKey').querySelector('i');
input.type = input.type === 'password' ? 'text' : 'password';
icon.className = input.type === 'password' ? 'fa-solid fa-eye' : 'fa-solid fa-eye-slash';
});
['appId', 'accessKey'].forEach(id => {
$(id).addEventListener('input', () => {
config.volc = config.volc || {};
config.volc.appId = $('appId').value.trim();
config.volc.accessKey = $('accessKey').value.trim();
updateApiStatus();
renderMyVoiceList();
});
});
$('speechRate').addEventListener('input', e => {
$('speechRateValue').textContent = `${Number(e.target.value).toFixed(1)}x`;
});
['voiceGenderFilter', 'voiceModelFilter', 'voiceLangFilter', 'voiceSceneFilter'].forEach(id => {
$(id)?.addEventListener('change', renderAuthVoiceList);
});
$('voiceSearchInput')?.addEventListener('input', renderAuthVoiceList);
$$('.voice-tab').forEach(tab => {
tab.addEventListener('click', () => {
$$('.voice-tab').forEach(t => t.classList.remove('active'));
$$('.voice-panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
$(`panel-${tab.dataset.panel}`)?.classList.add('active');
});
});
$('testMyVoiceBtn').addEventListener('click', () => {
const my = mySpeakers.find(s => s.value === selectedVoiceValue);
const source = my?.source || getVoiceSource(selectedVoiceValue);
doTestVoice(selectedVoiceValue, source, 'testTextMy', 'testMyStatus');
});
$('testTrialVoiceBtn').addEventListener('click', () => {
doTestVoice(selectedTrialVoiceValue, 'free', 'testTextTrial', 'testTrialStatus');
});
$('testAuthVoiceBtn').addEventListener('click', () => {
doTestVoice(selectedAuthVoiceValue, 'auth', 'testTextAuth', 'testAuthStatus');
});
$('saveToMyVoiceTrialBtn').addEventListener('click', () => {
if (!selectedTrialVoiceValue) { post('xb-tts:toast', { type: 'error', message: 'ÇëÏÈÑ¡ÔñÒ»¸öÒôÉ«' }); return; }
const tv = TRIAL_VOICES.find(v => v.key === selectedTrialVoiceValue);
const name = $('saveAsNameTrial').value.trim() || tv?.name || selectedTrialVoiceValue;
if (!isInMyList(selectedTrialVoiceValue)) {
mySpeakers.push({ name, value: selectedTrialVoiceValue, source: 'free' });
}
selectedVoiceValue = selectedTrialVoiceValue;
$('saveAsNameTrial').value = '';
renderMyVoiceList();
renderTrialVoiceList();
updateCurrentVoiceDisplay();
$$('.voice-tab').forEach(t => t.classList.remove('active'));
$$('.voice-panel').forEach(p => p.classList.remove('active'));
$$('.voice-tab')[0].classList.add('active');
$('panel-myVoice').classList.add('active');
post('xb-tts:save-config', collectForm());
post('xb-tts:toast', { type: 'success', message: `ÒÑÌí¼Ó£º${name}` });
});
$('saveToMyVoiceAuthBtn').addEventListener('click', () => {
if (!selectedAuthVoiceValue) { post('xb-tts:toast', { type: 'error', message: 'ÇëÏÈÑ¡ÔñÒ»¸öÒôÉ«' }); return; }
const av = authVoiceList.find(v => v.value === selectedAuthVoiceValue);
const name = $('saveAsNameAuth').value.trim() || av?.name || selectedAuthVoiceValue;
if (!isInMyList(selectedAuthVoiceValue)) {
mySpeakers.push({ name, value: selectedAuthVoiceValue, source: 'auth' });
}
selectedVoiceValue = selectedAuthVoiceValue;
$('saveAsNameAuth').value = '';
renderMyVoiceList();
renderAuthVoiceList();
updateCurrentVoiceDisplay();
$$('.voice-tab').forEach(t => t.classList.remove('active'));
$$('.voice-panel').forEach(p => p.classList.remove('active'));
$$('.voice-tab')[0].classList.add('active');
$('panel-myVoice').classList.add('active');
post('xb-tts:save-config', collectForm());
post('xb-tts:toast', { type: 'success', message: `ÒÑÌí¼Ó£º${name}` });
});
$('addMySpeakerBtn').addEventListener('click', () => {
const id = $('newVoiceId').value.trim();
const name = $('newVoiceName').value.trim();
if (!id) { post('xb-tts:toast', { type: 'error', message: 'ÇëÊäÈëÒôÉ«ID' }); return; }
if (!isInMyList(id)) {
mySpeakers.push({ name: name || id, value: id, source: 'auth' });
}
selectedVoiceValue = id;
$('newVoiceId').value = '';
$('newVoiceName').value = '';
renderMyVoiceList();
updateCurrentVoiceDisplay();
post('xb-tts:save-config', collectForm());
post('xb-tts:toast', { type: 'success', message: `ÒÑÌí¼Ó£º${name || id}` });
});
['saveConfigBtn', 'saveVoiceBtn', 'saveAdvancedBtn', 'saveCacheBtn'].forEach(id => {
$(id)?.addEventListener('click', () => { setSavingState($(id)); post('xb-tts:save-config', collectForm()); });
});
$('cacheRefreshBtn').addEventListener('click', () => post('xb-tts:cache-refresh'));
$('cacheClearExpiredBtn').addEventListener('click', () => post('xb-tts:cache-clear-expired'));
$('cacheClearAllBtn').addEventListener('click', () => post('xb-tts:cache-clear-all'));
post('xb-tts:ready');
});
</script>
</body>
</html>