2407 lines
87 KiB
HTML
2407 lines
87 KiB
HTML
<!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="♪ ÎÒÄÜÏëµ½×îÀËÂþµÄÊÂ~♪" 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="¿È~♪×£ÄãÉúÈÕ¿ìÀÖ~" 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
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> |