Commit ·
fc967af
1
Parent(s): e33d11d
fixed stt and added whisper and elevenlabs stt + updated ++ done
Browse files- .env +1 -1
- frontend/index.html +69 -67
- frontend/script.js +273 -121
- frontend/style.css +952 -459
- services/streaming.py +2 -1
- services/tts.py +12 -5
.env
CHANGED
|
@@ -9,7 +9,7 @@ GOOGLE_API_KEY="AIzaSyA9sqz4YKQHKXR9TU1imw0DPOghzHOMiBo"
|
|
| 9 |
|
| 10 |
|
| 11 |
ELEVENLABS_API_KEY="b3af3a938c8e15d5eae700ea47eea7d88dfe397f34fbd4b0c75c24f143b032b8"
|
| 12 |
-
ELEVENLABS_VOICE_ID="
|
| 13 |
ELEVENLABS_MODEL_ID="eleven_v3"
|
| 14 |
|
| 15 |
# TWILIO_ACCOUNT_SID="ACfafc0d2d007bdf14b21bb3e14a7a7b31"
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
ELEVENLABS_API_KEY="b3af3a938c8e15d5eae700ea47eea7d88dfe397f34fbd4b0c75c24f143b032b8"
|
| 12 |
+
ELEVENLABS_VOICE_ID="rxvktZTNrsQlsGIpOQGz"
|
| 13 |
ELEVENLABS_MODEL_ID="eleven_v3"
|
| 14 |
|
| 15 |
# TWILIO_ACCOUNT_SID="ACfafc0d2d007bdf14b21bb3e14a7a7b31"
|
frontend/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
-
<title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@300;400&family=Hind+Siliguri:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
@@ -11,69 +11,6 @@
|
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
|
| 14 |
-
<!-- ── Ambient background ── -->
|
| 15 |
-
<div class="bg-orb orb-1"></div>
|
| 16 |
-
<div class="bg-orb orb-2"></div>
|
| 17 |
-
<div class="bg-orb orb-3"></div>
|
| 18 |
-
|
| 19 |
-
<!-- ══════════════════════════════════════════════════════════════
|
| 20 |
-
INIT OVERLAY — shown until WS ready + animations done
|
| 21 |
-
Hard 8 s failsafe closes overlay if backend is slow.
|
| 22 |
-
══════════════════════════════════════════════════════════════ -->
|
| 23 |
-
<div id="init-overlay" class="init-overlay">
|
| 24 |
-
<div class="init-card">
|
| 25 |
-
<div class="init-logo">
|
| 26 |
-
<svg width="56" height="56" viewBox="0 0 56 56" fill="none">
|
| 27 |
-
<circle cx="28" cy="28" r="26" stroke="url(#g1)" stroke-width="2"/>
|
| 28 |
-
<path d="M18 28 Q28 16 38 28 Q28 40 18 28Z" fill="url(#g2)" opacity="0.9"/>
|
| 29 |
-
<defs>
|
| 30 |
-
<linearGradient id="g1" x1="0" y1="0" x2="56" y2="56">
|
| 31 |
-
<stop offset="0%" stop-color="#22d3ee"/><stop offset="100%" stop-color="#818cf8"/>
|
| 32 |
-
</linearGradient>
|
| 33 |
-
<linearGradient id="g2" x1="0" y1="0" x2="56" y2="56">
|
| 34 |
-
<stop offset="0%" stop-color="#22d3ee"/><stop offset="100%" stop-color="#818cf8"/>
|
| 35 |
-
</linearGradient>
|
| 36 |
-
</defs>
|
| 37 |
-
</svg>
|
| 38 |
-
</div>
|
| 39 |
-
<h2 class="init-title">AI Voice Assistant</h2>
|
| 40 |
-
<p class="init-subtitle">বাংলা ভয়েস সহকারী</p>
|
| 41 |
-
|
| 42 |
-
<div class="init-stages">
|
| 43 |
-
<div class="stage" id="stage-1">
|
| 44 |
-
<div class="stage-dot"></div>
|
| 45 |
-
<span>AI Engine শুরু হচ্ছে…</span>
|
| 46 |
-
<div class="stage-check">✓</div>
|
| 47 |
-
</div>
|
| 48 |
-
<div class="stage" id="stage-2">
|
| 49 |
-
<div class="stage-dot"></div>
|
| 50 |
-
<span>Speech Recognition মডেল লোড হচ্ছে…</span>
|
| 51 |
-
<div class="stage-check">✓</div>
|
| 52 |
-
</div>
|
| 53 |
-
<div class="stage" id="stage-3">
|
| 54 |
-
<div class="stage-dot"></div>
|
| 55 |
-
<span>GPU Warmup চলছে…</span>
|
| 56 |
-
<div class="stage-check">✓</div>
|
| 57 |
-
</div>
|
| 58 |
-
<div class="stage" id="stage-4">
|
| 59 |
-
<div class="stage-dot"></div>
|
| 60 |
-
<span>Voice Pipeline প্রস্তুত হচ্ছে…</span>
|
| 61 |
-
<div class="stage-check">✓</div>
|
| 62 |
-
</div>
|
| 63 |
-
</div>
|
| 64 |
-
|
| 65 |
-
<div class="init-bar-wrap">
|
| 66 |
-
<div class="init-bar" id="init-bar"></div>
|
| 67 |
-
</div>
|
| 68 |
-
<p class="init-status" id="init-status">সংযোগ স্থাপন করা হচ্ছে…</p>
|
| 69 |
-
</div>
|
| 70 |
-
</div>
|
| 71 |
-
|
| 72 |
-
<!-- ══════════════════════════════════════════════════════════════
|
| 73 |
-
MAIN APP
|
| 74 |
-
FIX-7: Hidden via .app CSS class (not inline opacity:0) to
|
| 75 |
-
prevent FOUC. JS adds class "visible" after init closes.
|
| 76 |
-
══════════════════════════════════════════════════════════════ -->
|
| 77 |
<div class="app" id="app">
|
| 78 |
|
| 79 |
<!-- ── Sidebar ── -->
|
|
@@ -92,7 +29,7 @@
|
|
| 92 |
</linearGradient>
|
| 93 |
</defs>
|
| 94 |
</svg>
|
| 95 |
-
<span>
|
| 96 |
</div>
|
| 97 |
<button class="sidebar-toggle" id="sidebar-toggle" title="Toggle sidebar">‹</button>
|
| 98 |
</div>
|
|
@@ -161,7 +98,7 @@
|
|
| 161 |
<span id="s-timeout-val">900 ms</span>
|
| 162 |
</div>
|
| 163 |
</div>
|
| 164 |
-
<div class="setting-row">
|
| 165 |
<label>TTS Voice</label>
|
| 166 |
<select id="s-voice" class="setting-select">
|
| 167 |
<option value="bn-BD-NabanitaNeural">Nabanita (Female)</option>
|
|
@@ -169,7 +106,7 @@
|
|
| 169 |
<option value="bn-IN-BashkarNeural">Bashkar (IN Male)</option>
|
| 170 |
<option value="bn-IN-TanishaaNeural">Tanishaa (IN Female)</option>
|
| 171 |
</select>
|
| 172 |
-
</div>
|
| 173 |
</div>
|
| 174 |
|
| 175 |
<div class="sidebar-divider"></div>
|
|
@@ -207,13 +144,78 @@
|
|
| 207 |
<span class="topbar-title">🏥 ডাক্তার অ্যাপয়েন্টমেন্ট সহকারী</span>
|
| 208 |
</div>
|
| 209 |
<div class="topbar-right">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
<button class="clear-btn" id="clear-btn" title="Clear conversation">↺ Clear</button>
|
| 211 |
</div>
|
| 212 |
</header>
|
| 213 |
|
|
|
|
|
|
|
| 214 |
<!-- Chat -->
|
| 215 |
<div id="chat-box"></div>
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
<!-- Voice visualizer — shown only while mic is active -->
|
| 218 |
<div class="voice-visualizer" id="voice-viz">
|
| 219 |
<div class="viz-bar"></div><div class="viz-bar"></div><div class="viz-bar"></div>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>Hospital Assistant</title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@300;400&family=Hind+Siliguri:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
|
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
<div class="app" id="app">
|
| 15 |
|
| 16 |
<!-- ── Sidebar ── -->
|
|
|
|
| 29 |
</linearGradient>
|
| 30 |
</defs>
|
| 31 |
</svg>
|
| 32 |
+
<span>Hospital Assistant</span>
|
| 33 |
</div>
|
| 34 |
<button class="sidebar-toggle" id="sidebar-toggle" title="Toggle sidebar">‹</button>
|
| 35 |
</div>
|
|
|
|
| 98 |
<span id="s-timeout-val">900 ms</span>
|
| 99 |
</div>
|
| 100 |
</div>
|
| 101 |
+
<!-- <div class="setting-row">
|
| 102 |
<label>TTS Voice</label>
|
| 103 |
<select id="s-voice" class="setting-select">
|
| 104 |
<option value="bn-BD-NabanitaNeural">Nabanita (Female)</option>
|
|
|
|
| 106 |
<option value="bn-IN-BashkarNeural">Bashkar (IN Male)</option>
|
| 107 |
<option value="bn-IN-TanishaaNeural">Tanishaa (IN Female)</option>
|
| 108 |
</select>
|
| 109 |
+
</div> -->
|
| 110 |
</div>
|
| 111 |
|
| 112 |
<div class="sidebar-divider"></div>
|
|
|
|
| 144 |
<span class="topbar-title">🏥 ডাক্তার অ্যাপয়েন্টমেন্ট সহকারী</span>
|
| 145 |
</div>
|
| 146 |
<div class="topbar-right">
|
| 147 |
+
<button class="brain-btn" id="brain-mode-btn" title="Brain mode" aria-pressed="false" aria-label="Brain mode">
|
| 148 |
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
| 149 |
+
<path d="M8.5 5.5c-1.7 0-3 1.4-3 3.1 0 .8.3 1.5.8 2.1-1.1.5-1.8 1.5-1.8 2.8 0 1.7 1.4 3.1 3.1 3.1h.3c.2 1.3 1.4 2.3 2.8 2.3.9 0 1.8-.4 2.3-1.1.5.7 1.4 1.1 2.3 1.1 1.4 0 2.6-1 2.8-2.3h.3c1.7 0 3.1-1.4 3.1-3.1 0-1.2-.7-2.3-1.8-2.8.5-.6.8-1.3.8-2.1 0-1.7-1.3-3.1-3-3.1-.6 0-1.2.2-1.7.5-.5-1.1-1.5-1.8-2.7-1.8-1.2 0-2.2.7-2.7 1.8-.5-.3-1.1-.5-1.7-.5Z"/>
|
| 150 |
+
<path d="M7.6 8.4 9.8 11M16.4 8.4 14.2 11M6.8 13.4 9.4 13.8M17.2 13.4 14.6 13.8M10.1 15.8 12 17.5 13.9 15.8"/>
|
| 151 |
+
<circle cx="10" cy="11" r="0.8"/>
|
| 152 |
+
<circle cx="14" cy="11" r="0.8"/>
|
| 153 |
+
<circle cx="12" cy="13.8" r="0.9"/>
|
| 154 |
+
</svg>
|
| 155 |
+
</button>
|
| 156 |
<button class="clear-btn" id="clear-btn" title="Clear conversation">↺ Clear</button>
|
| 157 |
</div>
|
| 158 |
</header>
|
| 159 |
|
| 160 |
+
<div class="voice-caption" id="voice-caption" aria-live="polite"></div>
|
| 161 |
+
|
| 162 |
<!-- Chat -->
|
| 163 |
<div id="chat-box"></div>
|
| 164 |
|
| 165 |
+
<!-- Brain mode -->
|
| 166 |
+
<section class="brain-stage" id="brain-stage" aria-hidden="true">
|
| 167 |
+
<div class="brain-shell">
|
| 168 |
+
<div class="brain-bubbles" aria-hidden="true">
|
| 169 |
+
<div class="brain-bubble brain-bubble-stt" id="brain-bubble-stt">
|
| 170 |
+
<div class="brain-bubble-label">You</div>
|
| 171 |
+
<div class="brain-bubble-text" id="brain-bubble-stt-text">Listening…</div>
|
| 172 |
+
</div>
|
| 173 |
+
<div class="brain-bubble brain-bubble-tts" id="brain-bubble-tts">
|
| 174 |
+
<div class="brain-bubble-label">AI</div>
|
| 175 |
+
<div class="brain-bubble-text" id="brain-bubble-tts-text">Waiting…</div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="brain-scan" aria-hidden="true"></div>
|
| 179 |
+
<div class="brain-pulse pulse-a" aria-hidden="true"></div>
|
| 180 |
+
<div class="brain-pulse pulse-b" aria-hidden="true"></div>
|
| 181 |
+
<div class="brain-pulse pulse-c" aria-hidden="true"></div>
|
| 182 |
+
<svg class="brain-svg" viewBox="0 0 960 620" role="presentation" aria-hidden="true">
|
| 183 |
+
<defs>
|
| 184 |
+
<linearGradient id="brainGlow" x1="0" y1="0" x2="1" y2="1">
|
| 185 |
+
<stop offset="0%" stop-color="#0ea5e9"/>
|
| 186 |
+
<stop offset="50%" stop-color="#8b5cf6"/>
|
| 187 |
+
<stop offset="100%" stop-color="#22c55e"/>
|
| 188 |
+
</linearGradient>
|
| 189 |
+
<filter id="brainBlur" x="-20%" y="-20%" width="140%" height="140%">
|
| 190 |
+
<feGaussianBlur stdDeviation="8"/>
|
| 191 |
+
</filter>
|
| 192 |
+
</defs>
|
| 193 |
+
<g class="brain-net">
|
| 194 |
+
<path class="brain-wire wire-a" d="M190 330C280 190 380 150 480 150s200 40 290 180"/>
|
| 195 |
+
<path class="brain-wire wire-b" d="M160 250C250 180 350 160 480 160s230 20 340 130"/>
|
| 196 |
+
<path class="brain-wire wire-c" d="M160 390C250 460 360 480 480 480s220-20 340-140"/>
|
| 197 |
+
<path class="brain-wire wire-d" d="M220 180C290 240 360 260 480 260s190-20 260-80"/>
|
| 198 |
+
<path class="brain-wire wire-e" d="M220 440C300 380 390 350 480 350s170 30 260 100"/>
|
| 199 |
+
<circle class="brain-node node-1" cx="190" cy="330" r="9"/>
|
| 200 |
+
<circle class="brain-node node-2" cx="300" cy="220" r="7"/>
|
| 201 |
+
<circle class="brain-node node-3" cx="410" cy="180" r="8"/>
|
| 202 |
+
<circle class="brain-node node-4" cx="480" cy="150" r="10"/>
|
| 203 |
+
<circle class="brain-node node-5" cx="560" cy="185" r="7"/>
|
| 204 |
+
<circle class="brain-node node-6" cx="670" cy="240" r="9"/>
|
| 205 |
+
<circle class="brain-node node-7" cx="760" cy="330" r="10"/>
|
| 206 |
+
<circle class="brain-node node-8" cx="660" cy="430" r="8"/>
|
| 207 |
+
<circle class="brain-node node-9" cx="520" cy="485" r="9"/>
|
| 208 |
+
<circle class="brain-node node-10" cx="360" cy="455" r="7"/>
|
| 209 |
+
<circle class="brain-node node-11" cx="260" cy="390" r="8"/>
|
| 210 |
+
<circle class="brain-node node-12" cx="300" cy="280" r="6"/>
|
| 211 |
+
</g>
|
| 212 |
+
<path class="brain-outline" d="M332 144c33-44 87-70 148-70 52 0 99 18 135 48 31 26 50 57 58 91 61 18 105 72 105 135 0 60-37 111-90 131-11 67-73 117-146 117-41 0-78-15-108-40-25 12-53 18-83 18-85 0-154-58-166-139-46-15-79-56-79-105 0-60 42-110 99-127 10-20 23-39 37-59 25-37 61-63 90-70z"/>
|
| 213 |
+
<circle class="brain-core" cx="480" cy="310" r="62" filter="url(#brainBlur)"/>
|
| 214 |
+
<circle class="brain-core ring" cx="480" cy="310" r="88"/>
|
| 215 |
+
</svg>
|
| 216 |
+
</div>
|
| 217 |
+
</section>
|
| 218 |
+
|
| 219 |
<!-- Voice visualizer — shown only while mic is active -->
|
| 220 |
<div class="voice-visualizer" id="voice-viz">
|
| 221 |
<div class="viz-bar"></div><div class="viz-bar"></div><div class="viz-bar"></div>
|
frontend/script.js
CHANGED
|
@@ -12,13 +12,17 @@ const stopBtn = document.getElementById('stop-btn');
|
|
| 12 |
const stateLabel = document.getElementById('state-label');
|
| 13 |
const stateDot = document.getElementById('state-dot');
|
| 14 |
const clearBtn = document.getElementById('clear-btn');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
const voiceViz = document.getElementById('voice-viz');
|
| 16 |
const vizBars = Array.from(voiceViz.querySelectorAll('.viz-bar'));
|
| 17 |
const queueBars = Array.from(document.querySelectorAll('.queue-bar'));
|
| 18 |
const chunksCount = document.getElementById('chunks-count');
|
| 19 |
-
const initOverlay = document.getElementById('init-overlay');
|
| 20 |
-
const initBar = document.getElementById('init-bar');
|
| 21 |
-
const initStatus = document.getElementById('init-status');
|
| 22 |
const sidebarEl = document.getElementById('sidebar');
|
| 23 |
const sidebarToggle = document.getElementById('sidebar-toggle');
|
| 24 |
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
|
@@ -51,8 +55,24 @@ const USER_ID = (() => {
|
|
| 51 |
})();
|
| 52 |
|
| 53 |
// ─── WebSocket base URL ────────────────────────────────────────────────────────
|
| 54 |
-
const
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
// ─── WS handles ───────────────────────────────────────────────────────────────
|
| 58 |
let chatWS = null;
|
|
@@ -75,6 +95,12 @@ let _endTimer = null;
|
|
| 75 |
let _cancelled = false;
|
| 76 |
let _inFlight = 0;
|
| 77 |
let _ttsPlaying = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
// ─── Recording state ──────────────────────────────────────────────────────────
|
| 80 |
let micStream = null;
|
|
@@ -96,6 +122,8 @@ let _recorderMime = 'audio/webm';
|
|
| 96 |
let aiEl = null;
|
| 97 |
let aiTxt = '';
|
| 98 |
let thinkingEl = null;
|
|
|
|
|
|
|
| 99 |
|
| 100 |
// ─── Latency timestamps ───────────────────────────────────────────────────────
|
| 101 |
let tSend = 0,
|
|
@@ -103,72 +131,10 @@ let tSend = 0,
|
|
| 103 |
tLlm = 0,
|
| 104 |
tTts = 0;
|
| 105 |
|
| 106 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 107 |
-
// INIT OVERLAY
|
| 108 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 109 |
-
|
| 110 |
-
const STAGES = [
|
| 111 |
-
{ id: 'stage-1', text: 'AI Engine শুরু হচ্ছে…', at: 400, pct: 20 },
|
| 112 |
-
{
|
| 113 |
-
id: 'stage-2',
|
| 114 |
-
text: 'Speech Recognition মডেল লোড হচ্ছে…',
|
| 115 |
-
at: 1100,
|
| 116 |
-
pct: 50,
|
| 117 |
-
},
|
| 118 |
-
{ id: 'stage-3', text: 'GPU Warmup চলছে…', at: 1900, pct: 75 },
|
| 119 |
-
{ id: 'stage-4', text: 'Voice Pipeline প্রস্তুত হচ্ছে…', at: 2700, pct: 90 },
|
| 120 |
-
];
|
| 121 |
-
|
| 122 |
-
let _wsGate = false;
|
| 123 |
-
let _stageGate = false;
|
| 124 |
-
let _initClosed = false;
|
| 125 |
-
|
| 126 |
-
function _tryClose() {
|
| 127 |
-
if (_initClosed || !_wsGate || !_stageGate) return;
|
| 128 |
-
_initClosed = true;
|
| 129 |
-
initBar.style.width = '100%';
|
| 130 |
-
initStatus.textContent = 'সিস্টেম প্রস্তুত ✓';
|
| 131 |
-
setTimeout(() => {
|
| 132 |
-
initOverlay.classList.add('hidden');
|
| 133 |
-
appEl.style.opacity = '1';
|
| 134 |
-
appEl.style.pointerEvents = 'auto';
|
| 135 |
-
setState('ready');
|
| 136 |
-
}, 450);
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
function boot() {
|
| 140 |
initWebSockets();
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
if (i > 0) _stageDone(STAGES[i - 1].id);
|
| 144 |
-
const el = document.getElementById(id);
|
| 145 |
-
if (el) el.classList.add('active');
|
| 146 |
-
initStatus.textContent = text;
|
| 147 |
-
initBar.style.width = pct + '%';
|
| 148 |
-
}, at);
|
| 149 |
-
});
|
| 150 |
-
setTimeout(
|
| 151 |
-
() => {
|
| 152 |
-
_stageDone(STAGES[STAGES.length - 1].id);
|
| 153 |
-
_stageGate = true;
|
| 154 |
-
_tryClose();
|
| 155 |
-
},
|
| 156 |
-
STAGES[STAGES.length - 1].at + 650,
|
| 157 |
-
);
|
| 158 |
-
setTimeout(() => {
|
| 159 |
-
if (!_initClosed) {
|
| 160 |
-
_wsGate = _stageGate = true;
|
| 161 |
-
_tryClose();
|
| 162 |
-
}
|
| 163 |
-
}, 8000);
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
function _stageDone(id) {
|
| 167 |
-
const el = document.getElementById(id);
|
| 168 |
-
if (el) {
|
| 169 |
-
el.classList.remove('active');
|
| 170 |
-
el.classList.add('done');
|
| 171 |
-
}
|
| 172 |
}
|
| 173 |
|
| 174 |
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -179,6 +145,17 @@ function _backoff(r) {
|
|
| 179 |
return Math.min(1000 * Math.pow(2, r), 16000);
|
| 180 |
}
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
function _setSysStatus(online) {
|
| 183 |
if (!sysStat) return;
|
| 184 |
sysStat.textContent = online ? 'Ready' : 'Reconnecting';
|
|
@@ -188,7 +165,7 @@ function _setSysStatus(online) {
|
|
| 188 |
|
| 189 |
function _connectChat() {
|
| 190 |
if (chatWS && chatWS.readyState <= WebSocket.OPEN) return;
|
| 191 |
-
chatWS = new WebSocket(`${
|
| 192 |
chatWS.onopen = () => {
|
| 193 |
_chatRetry = 0;
|
| 194 |
console.log('[Chat WS] connected');
|
|
@@ -197,6 +174,7 @@ function _connectChat() {
|
|
| 197 |
chatWS.onerror = (e) => console.error('[Chat WS] error:', e);
|
| 198 |
chatWS.onclose = (ev) => {
|
| 199 |
console.log(`[Chat WS] closed (${ev.code})`);
|
|
|
|
| 200 |
clearTimeout(_chatRetryTimer);
|
| 201 |
_chatRetryTimer = setTimeout(() => {
|
| 202 |
_chatRetry++;
|
|
@@ -208,7 +186,7 @@ function _connectChat() {
|
|
| 208 |
|
| 209 |
function _connectVoice() {
|
| 210 |
if (voiceWS && voiceWS.readyState <= WebSocket.OPEN) return;
|
| 211 |
-
voiceWS = new WebSocket(`${
|
| 212 |
voiceWS.binaryType = 'arraybuffer';
|
| 213 |
|
| 214 |
voiceWS.onopen = () => {
|
|
@@ -216,17 +194,13 @@ function _connectVoice() {
|
|
| 216 |
console.log('[Voice WS] connected, uid:', USER_ID);
|
| 217 |
voiceWS.send(JSON.stringify({ type: 'init', user_id: USER_ID }));
|
| 218 |
_setSysStatus(true);
|
| 219 |
-
|
| 220 |
-
|
| 221 |
};
|
| 222 |
voiceWS.onerror = (e) => console.error('[Voice WS] error:', e);
|
| 223 |
voiceWS.onclose = (ev) => {
|
| 224 |
console.log(`[Voice WS] closed (${ev.code})`);
|
| 225 |
_setSysStatus(false);
|
| 226 |
-
if (!_initClosed) {
|
| 227 |
-
_wsGate = true;
|
| 228 |
-
_tryClose();
|
| 229 |
-
}
|
| 230 |
if (isListening || isSpeaking || isProcessing) {
|
| 231 |
_teardownMicHardware();
|
| 232 |
_resetVoiceState();
|
|
@@ -235,10 +209,14 @@ function _connectVoice() {
|
|
| 235 |
micBtn.disabled = false;
|
| 236 |
}
|
| 237 |
clearTimeout(_voiceRetryTimer);
|
|
|
|
| 238 |
_voiceRetryTimer = setTimeout(() => {
|
| 239 |
_voiceRetry++;
|
| 240 |
_connectVoice();
|
| 241 |
}, _backoff(_voiceRetry));
|
|
|
|
|
|
|
|
|
|
| 242 |
};
|
| 243 |
voiceWS.onmessage = onVoiceMsg;
|
| 244 |
}
|
|
@@ -272,11 +250,7 @@ function onChatMsg(ev) {
|
|
| 272 |
chatBox.appendChild(aiEl);
|
| 273 |
}
|
| 274 |
aiTxt += msg.token;
|
| 275 |
-
|
| 276 |
-
typeof marked !== 'undefined'
|
| 277 |
-
? marked.parse(aiTxt)
|
| 278 |
-
: aiTxt.replace(/\n/g, '<br>');
|
| 279 |
-
chatBox.scrollTop = chatBox.scrollHeight;
|
| 280 |
break;
|
| 281 |
|
| 282 |
case 'chat':
|
|
@@ -288,24 +262,15 @@ function onChatMsg(ev) {
|
|
| 288 |
chatBox.appendChild(aiEl);
|
| 289 |
}
|
| 290 |
aiTxt = msg.text;
|
| 291 |
-
|
| 292 |
-
typeof marked !== 'undefined'
|
| 293 |
-
? marked.parse(aiTxt)
|
| 294 |
-
: aiTxt.replace(/\n/g, '<br>');
|
| 295 |
-
chatBox.scrollTop = chatBox.scrollHeight;
|
| 296 |
break;
|
| 297 |
|
| 298 |
case 'end':
|
| 299 |
_removeThinking();
|
| 300 |
-
|
| 301 |
-
aiEl.innerHTML =
|
| 302 |
-
typeof marked !== 'undefined'
|
| 303 |
-
? marked.parse(aiTxt)
|
| 304 |
-
: aiTxt.replace(/\n/g, '<br>');
|
| 305 |
-
chatBox.scrollTop = chatBox.scrollHeight;
|
| 306 |
-
}
|
| 307 |
aiEl = null;
|
| 308 |
aiTxt = '';
|
|
|
|
| 309 |
if (tSend > 0) mTotal.textContent = Date.now() - tSend + ' ms';
|
| 310 |
tSend = tStt = tLlm = tTts = 0;
|
| 311 |
isProcessing = false;
|
|
@@ -317,6 +282,7 @@ function onChatMsg(ev) {
|
|
| 317 |
appendMsg('⚠️ ' + msg.text, 'system');
|
| 318 |
aiEl = null;
|
| 319 |
aiTxt = '';
|
|
|
|
| 320 |
isProcessing = false;
|
| 321 |
setState('ready');
|
| 322 |
break;
|
|
@@ -348,9 +314,12 @@ function onVoiceMsg(ev) {
|
|
| 348 |
tStt = Date.now();
|
| 349 |
if (tSend > 0) mStt.textContent = tStt - tSend + ' ms';
|
| 350 |
_removeThinking();
|
| 351 |
-
appendMsg('🎤 ' + msg.text, 'user');
|
| 352 |
aiEl = null;
|
| 353 |
aiTxt = '';
|
|
|
|
|
|
|
|
|
|
| 354 |
appendThinking();
|
| 355 |
setState('processing');
|
| 356 |
break;
|
|
@@ -362,30 +331,28 @@ function onVoiceMsg(ev) {
|
|
| 362 |
if (tStt > 0) mLlm.textContent = tLlm - tStt + ' ms';
|
| 363 |
}
|
| 364 |
_removeThinking();
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
}
|
| 370 |
-
aiTxt += msg.token;
|
| 371 |
-
aiEl.innerHTML =
|
| 372 |
-
typeof marked !== 'undefined'
|
| 373 |
-
? marked.parse(aiTxt)
|
| 374 |
-
: aiTxt.replace(/\n/g, '<br>');
|
| 375 |
-
chatBox.scrollTop = chatBox.scrollHeight;
|
| 376 |
break;
|
| 377 |
|
| 378 |
case 'end':
|
| 379 |
-
|
| 380 |
-
aiEl.innerHTML =
|
| 381 |
-
typeof marked !== 'undefined'
|
| 382 |
-
? marked.parse(aiTxt)
|
| 383 |
-
: aiTxt.replace(/\n/g, '<br>');
|
| 384 |
-
chatBox.scrollTop = chatBox.scrollHeight;
|
| 385 |
-
}
|
| 386 |
_removeThinking();
|
| 387 |
aiEl = null;
|
| 388 |
aiTxt = '';
|
|
|
|
| 389 |
if (tSend > 0) mTotal.textContent = Date.now() - tSend + ' ms';
|
| 390 |
tSend = tStt = tLlm = tTts = 0;
|
| 391 |
isProcessing = false;
|
|
@@ -399,6 +366,9 @@ function onVoiceMsg(ev) {
|
|
| 399 |
appendMsg('⚠️ ' + msg.text, 'system');
|
| 400 |
aiEl = null;
|
| 401 |
aiTxt = '';
|
|
|
|
|
|
|
|
|
|
| 402 |
isProcessing = false;
|
| 403 |
// BUG-FIX-C: unconditionally unlock on error
|
| 404 |
_done();
|
|
@@ -414,6 +384,7 @@ function onVoiceMsg(ev) {
|
|
| 414 |
|
| 415 |
// ─── Thinking bubble ──────────────────────────────────────────────────────────
|
| 416 |
function appendThinking() {
|
|
|
|
| 417 |
if (thinkingEl) return;
|
| 418 |
thinkingEl = document.createElement('div');
|
| 419 |
thinkingEl.className = 'message ai thinking';
|
|
@@ -429,6 +400,28 @@ function _removeThinking() {
|
|
| 429 |
}
|
| 430 |
}
|
| 431 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 433 |
// AUDIO PLAYBACK
|
| 434 |
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -515,11 +508,22 @@ function _done() {
|
|
| 515 |
_ttsPlaying = false;
|
| 516 |
isProcessing = false;
|
| 517 |
isRecordingLocked = false;
|
|
|
|
|
|
|
| 518 |
_inFlight = 0;
|
| 519 |
_vizQ();
|
| 520 |
micBtn.disabled = false;
|
| 521 |
setState('ready');
|
| 522 |
setMic('off');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
console.log('[Voice] Idle — ready for next manual press');
|
| 524 |
}
|
| 525 |
|
|
@@ -585,6 +589,10 @@ micBtn.onclick = async () => {
|
|
| 585 |
return;
|
| 586 |
}
|
| 587 |
if (isListening) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
_teardownMicHardware();
|
| 589 |
_resetVoiceState();
|
| 590 |
setState('ready');
|
|
@@ -595,6 +603,10 @@ micBtn.onclick = async () => {
|
|
| 595 |
};
|
| 596 |
|
| 597 |
stopBtn.onclick = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 598 |
stopAllAudio();
|
| 599 |
if (isListening || isSpeaking) _teardownMicHardware();
|
| 600 |
_resetVoiceState();
|
|
@@ -833,9 +845,16 @@ function startRecorder() {
|
|
| 833 |
const captured = audioChunks.slice();
|
| 834 |
audioChunks = [];
|
| 835 |
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
|
| 840 |
console.log(
|
| 841 |
`[Recorder] onstop: ${captured.length} chunk(s), ${captured
|
|
@@ -851,8 +870,9 @@ function startRecorder() {
|
|
| 851 |
'system',
|
| 852 |
);
|
| 853 |
_resetVoiceState();
|
| 854 |
-
setState('ready');
|
| 855 |
micBtn.disabled = false;
|
|
|
|
| 856 |
return;
|
| 857 |
}
|
| 858 |
|
|
@@ -864,9 +884,10 @@ function startRecorder() {
|
|
| 864 |
} catch (err) {
|
| 865 |
console.error('[Recorder] arrayBuffer() error:', err);
|
| 866 |
_resetVoiceState();
|
| 867 |
-
setState('ready');
|
| 868 |
setMic('off');
|
| 869 |
micBtn.disabled = false;
|
|
|
|
| 870 |
return;
|
| 871 |
}
|
| 872 |
|
|
@@ -878,12 +899,14 @@ function startRecorder() {
|
|
| 878 |
voiceWS.send(buf);
|
| 879 |
// isProcessing + isRecordingLocked stay true until _done() fires
|
| 880 |
} else {
|
| 881 |
-
console.warn('[VAD] Voice WS not open —
|
| 882 |
-
|
|
|
|
| 883 |
_resetVoiceState();
|
| 884 |
-
setState('ready');
|
| 885 |
setMic('off');
|
| 886 |
micBtn.disabled = false;
|
|
|
|
| 887 |
}
|
| 888 |
};
|
| 889 |
|
|
@@ -927,6 +950,7 @@ function setState(s) {
|
|
| 927 |
const cfg = STATE_MAP[s] || STATE_MAP.ready;
|
| 928 |
stateLabel.textContent = cfg.label;
|
| 929 |
stateDot.className = 'state-dot' + (cfg.cls ? ' ' + cfg.cls : '');
|
|
|
|
| 930 |
}
|
| 931 |
|
| 932 |
const MIC_MAP = {
|
|
@@ -948,6 +972,7 @@ function setMic(s) {
|
|
| 948 |
}
|
| 949 |
|
| 950 |
function appendMsg(text, who) {
|
|
|
|
| 951 |
const d = document.createElement('div');
|
| 952 |
d.className = 'message ' + who;
|
| 953 |
if (who === 'ai' && typeof marked !== 'undefined') {
|
|
@@ -963,7 +988,11 @@ function appendMsg(text, who) {
|
|
| 963 |
clearBtn.onclick = () => {
|
| 964 |
chatBox.innerHTML = '';
|
| 965 |
thinkingEl = null;
|
| 966 |
-
appendMsg('চ্যাট পরিষ্কার করা হয়েছে।', 'system');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 967 |
};
|
| 968 |
|
| 969 |
sidebarToggle.onclick = () => {
|
|
@@ -974,6 +1003,129 @@ sidebarToggle.onclick = () => {
|
|
| 974 |
};
|
| 975 |
mobileMenuBtn.onclick = () => sidebarEl.classList.toggle('mobile-open');
|
| 976 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 977 |
sThreshold.value = SILENCE_DB;
|
| 978 |
sThresholdVal.textContent = SILENCE_DB + ' dB';
|
| 979 |
sThreshold.oninput = () => {
|
|
|
|
| 12 |
const stateLabel = document.getElementById('state-label');
|
| 13 |
const stateDot = document.getElementById('state-dot');
|
| 14 |
const clearBtn = document.getElementById('clear-btn');
|
| 15 |
+
const brainBtn = document.getElementById('brain-mode-btn');
|
| 16 |
+
const voiceCaption = document.getElementById('voice-caption');
|
| 17 |
+
const brainStage = document.getElementById('brain-stage');
|
| 18 |
+
const brainBubbleStt = document.getElementById('brain-bubble-stt');
|
| 19 |
+
const brainBubbleTts = document.getElementById('brain-bubble-tts');
|
| 20 |
+
const brainBubbleSttText = document.getElementById('brain-bubble-stt-text');
|
| 21 |
+
const brainBubbleTtsText = document.getElementById('brain-bubble-tts-text');
|
| 22 |
const voiceViz = document.getElementById('voice-viz');
|
| 23 |
const vizBars = Array.from(voiceViz.querySelectorAll('.viz-bar'));
|
| 24 |
const queueBars = Array.from(document.querySelectorAll('.queue-bar'));
|
| 25 |
const chunksCount = document.getElementById('chunks-count');
|
|
|
|
|
|
|
|
|
|
| 26 |
const sidebarEl = document.getElementById('sidebar');
|
| 27 |
const sidebarToggle = document.getElementById('sidebar-toggle');
|
| 28 |
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
|
|
|
| 55 |
})();
|
| 56 |
|
| 57 |
// ─── WebSocket base URL ────────────────────────────────────────────────────────
|
| 58 |
+
const WS_BASES = (() => {
|
| 59 |
+
const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 60 |
+
const bases = [];
|
| 61 |
+
const host = window.location.host && window.location.host !== 'null'
|
| 62 |
+
? `${scheme}//${window.location.host}`
|
| 63 |
+
: '';
|
| 64 |
+
const push = (base) => {
|
| 65 |
+
if (base && !bases.includes(base)) bases.push(base);
|
| 66 |
+
};
|
| 67 |
+
push(host);
|
| 68 |
+
push(`${scheme}//127.0.0.1:8000`);
|
| 69 |
+
push(`${scheme}//127.0.0.1:8679`);
|
| 70 |
+
push(`${scheme}//localhost:8000`);
|
| 71 |
+
push(`${scheme}//localhost:8679`);
|
| 72 |
+
return bases;
|
| 73 |
+
})();
|
| 74 |
+
let _wsBaseIndex = 0;
|
| 75 |
+
console.log('[Boot] WS bases:', WS_BASES.join(', '));
|
| 76 |
|
| 77 |
// ─── WS handles ───────────────────────────────────────────────────────────────
|
| 78 |
let chatWS = null;
|
|
|
|
| 95 |
let _cancelled = false;
|
| 96 |
let _inFlight = 0;
|
| 97 |
let _ttsPlaying = false;
|
| 98 |
+
let brainMode = false;
|
| 99 |
+
let brainVoiceActive = false;
|
| 100 |
+
let brainRestartTimer = null;
|
| 101 |
+
let brainAutoRestartTimer = null;
|
| 102 |
+
let brainPendingAudio = null;
|
| 103 |
+
let voicePendingPackets = [];
|
| 104 |
|
| 105 |
// ─── Recording state ──────────────────────────────────────────────────────────
|
| 106 |
let micStream = null;
|
|
|
|
| 122 |
let aiEl = null;
|
| 123 |
let aiTxt = '';
|
| 124 |
let thinkingEl = null;
|
| 125 |
+
let _captionRaf = 0;
|
| 126 |
+
let _captionText = '';
|
| 127 |
|
| 128 |
// ─── Latency timestamps ───────────────────────────────────────────────────────
|
| 129 |
let tSend = 0,
|
|
|
|
| 131 |
tLlm = 0,
|
| 132 |
tTts = 0;
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
function boot() {
|
| 135 |
initWebSockets();
|
| 136 |
+
appEl.classList.add('visible');
|
| 137 |
+
setState('ready');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
| 145 |
return Math.min(1000 * Math.pow(2, r), 16000);
|
| 146 |
}
|
| 147 |
|
| 148 |
+
function _wsBase() {
|
| 149 |
+
return WS_BASES[Math.min(_wsBaseIndex, WS_BASES.length - 1)] || WS_BASES[0];
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
function _advanceWsBase() {
|
| 153 |
+
if (WS_BASES.length <= 1) return _wsBase();
|
| 154 |
+
_wsBaseIndex = (_wsBaseIndex + 1) % WS_BASES.length;
|
| 155 |
+
console.log('[WS] Switching base to:', _wsBase());
|
| 156 |
+
return _wsBase();
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
function _setSysStatus(online) {
|
| 160 |
if (!sysStat) return;
|
| 161 |
sysStat.textContent = online ? 'Ready' : 'Reconnecting';
|
|
|
|
| 165 |
|
| 166 |
function _connectChat() {
|
| 167 |
if (chatWS && chatWS.readyState <= WebSocket.OPEN) return;
|
| 168 |
+
chatWS = new WebSocket(`${_wsBase()}/ws/chat`);
|
| 169 |
chatWS.onopen = () => {
|
| 170 |
_chatRetry = 0;
|
| 171 |
console.log('[Chat WS] connected');
|
|
|
|
| 174 |
chatWS.onerror = (e) => console.error('[Chat WS] error:', e);
|
| 175 |
chatWS.onclose = (ev) => {
|
| 176 |
console.log(`[Chat WS] closed (${ev.code})`);
|
| 177 |
+
_advanceWsBase();
|
| 178 |
clearTimeout(_chatRetryTimer);
|
| 179 |
_chatRetryTimer = setTimeout(() => {
|
| 180 |
_chatRetry++;
|
|
|
|
| 186 |
|
| 187 |
function _connectVoice() {
|
| 188 |
if (voiceWS && voiceWS.readyState <= WebSocket.OPEN) return;
|
| 189 |
+
voiceWS = new WebSocket(`${_wsBase()}/ws/voice`);
|
| 190 |
voiceWS.binaryType = 'arraybuffer';
|
| 191 |
|
| 192 |
voiceWS.onopen = () => {
|
|
|
|
| 194 |
console.log('[Voice WS] connected, uid:', USER_ID);
|
| 195 |
voiceWS.send(JSON.stringify({ type: 'init', user_id: USER_ID }));
|
| 196 |
_setSysStatus(true);
|
| 197 |
+
_flushVoicePendingPackets();
|
| 198 |
+
_flushBrainPendingAudio();
|
| 199 |
};
|
| 200 |
voiceWS.onerror = (e) => console.error('[Voice WS] error:', e);
|
| 201 |
voiceWS.onclose = (ev) => {
|
| 202 |
console.log(`[Voice WS] closed (${ev.code})`);
|
| 203 |
_setSysStatus(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
if (isListening || isSpeaking || isProcessing) {
|
| 205 |
_teardownMicHardware();
|
| 206 |
_resetVoiceState();
|
|
|
|
| 209 |
micBtn.disabled = false;
|
| 210 |
}
|
| 211 |
clearTimeout(_voiceRetryTimer);
|
| 212 |
+
_advanceWsBase();
|
| 213 |
_voiceRetryTimer = setTimeout(() => {
|
| 214 |
_voiceRetry++;
|
| 215 |
_connectVoice();
|
| 216 |
}, _backoff(_voiceRetry));
|
| 217 |
+
if (brainMode && brainVoiceActive) {
|
| 218 |
+
_queueBrainReconnect();
|
| 219 |
+
}
|
| 220 |
};
|
| 221 |
voiceWS.onmessage = onVoiceMsg;
|
| 222 |
}
|
|
|
|
| 250 |
chatBox.appendChild(aiEl);
|
| 251 |
}
|
| 252 |
aiTxt += msg.token;
|
| 253 |
+
_renderAiText();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
break;
|
| 255 |
|
| 256 |
case 'chat':
|
|
|
|
| 262 |
chatBox.appendChild(aiEl);
|
| 263 |
}
|
| 264 |
aiTxt = msg.text;
|
| 265 |
+
_renderAiText();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
break;
|
| 267 |
|
| 268 |
case 'end':
|
| 269 |
_removeThinking();
|
| 270 |
+
_renderAiText(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
aiEl = null;
|
| 272 |
aiTxt = '';
|
| 273 |
+
_setCaption('');
|
| 274 |
if (tSend > 0) mTotal.textContent = Date.now() - tSend + ' ms';
|
| 275 |
tSend = tStt = tLlm = tTts = 0;
|
| 276 |
isProcessing = false;
|
|
|
|
| 282 |
appendMsg('⚠️ ' + msg.text, 'system');
|
| 283 |
aiEl = null;
|
| 284 |
aiTxt = '';
|
| 285 |
+
_setCaption('');
|
| 286 |
isProcessing = false;
|
| 287 |
setState('ready');
|
| 288 |
break;
|
|
|
|
| 314 |
tStt = Date.now();
|
| 315 |
if (tSend > 0) mStt.textContent = tStt - tSend + ' ms';
|
| 316 |
_removeThinking();
|
| 317 |
+
if (!brainMode) appendMsg('🎤 ' + msg.text, 'user');
|
| 318 |
aiEl = null;
|
| 319 |
aiTxt = '';
|
| 320 |
+
_setCaption('');
|
| 321 |
+
_brainSetSttBubble(msg.text);
|
| 322 |
+
_brainModeSetSearch(true);
|
| 323 |
appendThinking();
|
| 324 |
setState('processing');
|
| 325 |
break;
|
|
|
|
| 331 |
if (tStt > 0) mLlm.textContent = tLlm - tStt + ' ms';
|
| 332 |
}
|
| 333 |
_removeThinking();
|
| 334 |
+
_setCaption(aiTxt + msg.token);
|
| 335 |
+
_brainSetTtsBubble(aiTxt + msg.token);
|
| 336 |
+
_brainModeSetSearch(true);
|
| 337 |
+
if (!brainMode) {
|
| 338 |
+
if (!aiEl) {
|
| 339 |
+
aiEl = document.createElement('div');
|
| 340 |
+
aiEl.className = 'message ai';
|
| 341 |
+
chatBox.appendChild(aiEl);
|
| 342 |
+
}
|
| 343 |
+
aiTxt += msg.token;
|
| 344 |
+
_renderAiText();
|
| 345 |
+
} else {
|
| 346 |
+
aiTxt += msg.token;
|
| 347 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
break;
|
| 349 |
|
| 350 |
case 'end':
|
| 351 |
+
_renderAiText(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
_removeThinking();
|
| 353 |
aiEl = null;
|
| 354 |
aiTxt = '';
|
| 355 |
+
_setCaption('');
|
| 356 |
if (tSend > 0) mTotal.textContent = Date.now() - tSend + ' ms';
|
| 357 |
tSend = tStt = tLlm = tTts = 0;
|
| 358 |
isProcessing = false;
|
|
|
|
| 366 |
appendMsg('⚠️ ' + msg.text, 'system');
|
| 367 |
aiEl = null;
|
| 368 |
aiTxt = '';
|
| 369 |
+
_setCaption('');
|
| 370 |
+
_brainSetTtsBubble('', false);
|
| 371 |
+
_brainModeSetSearch(false);
|
| 372 |
isProcessing = false;
|
| 373 |
// BUG-FIX-C: unconditionally unlock on error
|
| 374 |
_done();
|
|
|
|
| 384 |
|
| 385 |
// ─── Thinking bubble ──────────────────────────────────────────────────────────
|
| 386 |
function appendThinking() {
|
| 387 |
+
if (brainMode) return;
|
| 388 |
if (thinkingEl) return;
|
| 389 |
thinkingEl = document.createElement('div');
|
| 390 |
thinkingEl.className = 'message ai thinking';
|
|
|
|
| 400 |
}
|
| 401 |
}
|
| 402 |
|
| 403 |
+
function _renderAiText(force = false) {
|
| 404 |
+
if (!aiEl || !aiTxt) {
|
| 405 |
+
if (force && aiEl) aiEl.innerHTML = '';
|
| 406 |
+
return;
|
| 407 |
+
}
|
| 408 |
+
aiEl.innerHTML =
|
| 409 |
+
typeof marked !== 'undefined'
|
| 410 |
+
? marked.parse(aiTxt)
|
| 411 |
+
: aiTxt.replace(/\n/g, '<br>');
|
| 412 |
+
chatBox.scrollTop = chatBox.scrollHeight;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
function _setCaption(text) {
|
| 416 |
+
_captionText = text || '';
|
| 417 |
+
if (_captionRaf) return;
|
| 418 |
+
_captionRaf = requestAnimationFrame(() => {
|
| 419 |
+
_captionRaf = 0;
|
| 420 |
+
if (!voiceCaption) return;
|
| 421 |
+
voiceCaption.textContent = brainMode ? '' : _captionText;
|
| 422 |
+
});
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
// ═══════════════════════════════════════════════════════════════════════════════
|
| 426 |
// AUDIO PLAYBACK
|
| 427 |
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
| 508 |
_ttsPlaying = false;
|
| 509 |
isProcessing = false;
|
| 510 |
isRecordingLocked = false;
|
| 511 |
+
_brainModeSetSearch(false);
|
| 512 |
+
_brainSetTtsBubble('', false);
|
| 513 |
_inFlight = 0;
|
| 514 |
_vizQ();
|
| 515 |
micBtn.disabled = false;
|
| 516 |
setState('ready');
|
| 517 |
setMic('off');
|
| 518 |
+
if (brainMode && brainVoiceActive) {
|
| 519 |
+
clearTimeout(brainAutoRestartTimer);
|
| 520 |
+
brainAutoRestartTimer = setTimeout(() => {
|
| 521 |
+
if (!brainMode || !brainVoiceActive || isListening || isProcessing || isRecordingLocked) {
|
| 522 |
+
return;
|
| 523 |
+
}
|
| 524 |
+
_brainResumeListening();
|
| 525 |
+
}, 180);
|
| 526 |
+
}
|
| 527 |
console.log('[Voice] Idle — ready for next manual press');
|
| 528 |
}
|
| 529 |
|
|
|
|
| 589 |
return;
|
| 590 |
}
|
| 591 |
if (isListening) {
|
| 592 |
+
if (brainMode && brainVoiceActive) {
|
| 593 |
+
console.log('[Brain] Continuous mode active — use Stop to exit');
|
| 594 |
+
return;
|
| 595 |
+
}
|
| 596 |
_teardownMicHardware();
|
| 597 |
_resetVoiceState();
|
| 598 |
setState('ready');
|
|
|
|
| 603 |
};
|
| 604 |
|
| 605 |
stopBtn.onclick = () => {
|
| 606 |
+
brainVoiceActive = false;
|
| 607 |
+
clearTimeout(brainAutoRestartTimer);
|
| 608 |
+
clearTimeout(brainRestartTimer);
|
| 609 |
+
brainPendingAudio = null;
|
| 610 |
stopAllAudio();
|
| 611 |
if (isListening || isSpeaking) _teardownMicHardware();
|
| 612 |
_resetVoiceState();
|
|
|
|
| 845 |
const captured = audioChunks.slice();
|
| 846 |
audioChunks = [];
|
| 847 |
|
| 848 |
+
const keepBrainMicWarm = brainMode && brainVoiceActive;
|
| 849 |
+
|
| 850 |
+
// ── 2. Tear down mic hardware unless brain mode wants a live loop ─────
|
| 851 |
+
if (keepBrainMicWarm) {
|
| 852 |
+
mediaRecorder = null;
|
| 853 |
+
setMic('off');
|
| 854 |
+
} else {
|
| 855 |
+
_teardownMicHardware();
|
| 856 |
+
setMic('off');
|
| 857 |
+
}
|
| 858 |
|
| 859 |
console.log(
|
| 860 |
`[Recorder] onstop: ${captured.length} chunk(s), ${captured
|
|
|
|
| 870 |
'system',
|
| 871 |
);
|
| 872 |
_resetVoiceState();
|
| 873 |
+
setState(keepBrainMicWarm ? 'listening' : 'ready');
|
| 874 |
micBtn.disabled = false;
|
| 875 |
+
if (keepBrainMicWarm) _brainResumeListening();
|
| 876 |
return;
|
| 877 |
}
|
| 878 |
|
|
|
|
| 884 |
} catch (err) {
|
| 885 |
console.error('[Recorder] arrayBuffer() error:', err);
|
| 886 |
_resetVoiceState();
|
| 887 |
+
setState(keepBrainMicWarm ? 'listening' : 'ready');
|
| 888 |
setMic('off');
|
| 889 |
micBtn.disabled = false;
|
| 890 |
+
if (keepBrainMicWarm) _brainResumeListening();
|
| 891 |
return;
|
| 892 |
}
|
| 893 |
|
|
|
|
| 899 |
voiceWS.send(buf);
|
| 900 |
// isProcessing + isRecordingLocked stay true until _done() fires
|
| 901 |
} else {
|
| 902 |
+
console.warn('[VAD] Voice WS not open — queueing utterance');
|
| 903 |
+
voicePendingPackets.push(buf);
|
| 904 |
+
_connectVoice();
|
| 905 |
_resetVoiceState();
|
| 906 |
+
setState(keepBrainMicWarm ? 'listening' : 'ready');
|
| 907 |
setMic('off');
|
| 908 |
micBtn.disabled = false;
|
| 909 |
+
if (keepBrainMicWarm) _brainResumeListening();
|
| 910 |
}
|
| 911 |
};
|
| 912 |
|
|
|
|
| 950 |
const cfg = STATE_MAP[s] || STATE_MAP.ready;
|
| 951 |
stateLabel.textContent = cfg.label;
|
| 952 |
stateDot.className = 'state-dot' + (cfg.cls ? ' ' + cfg.cls : '');
|
| 953 |
+
if (brainStage) brainStage.dataset.state = s;
|
| 954 |
}
|
| 955 |
|
| 956 |
const MIC_MAP = {
|
|
|
|
| 972 |
}
|
| 973 |
|
| 974 |
function appendMsg(text, who) {
|
| 975 |
+
if (brainMode && who !== 'system') return null;
|
| 976 |
const d = document.createElement('div');
|
| 977 |
d.className = 'message ' + who;
|
| 978 |
if (who === 'ai' && typeof marked !== 'undefined') {
|
|
|
|
| 988 |
clearBtn.onclick = () => {
|
| 989 |
chatBox.innerHTML = '';
|
| 990 |
thinkingEl = null;
|
| 991 |
+
if (!brainMode) appendMsg('চ্যাট পরিষ্কার করা হয়েছে।', 'system');
|
| 992 |
+
};
|
| 993 |
+
|
| 994 |
+
brainBtn.onclick = () => {
|
| 995 |
+
setBrainMode(!brainMode);
|
| 996 |
};
|
| 997 |
|
| 998 |
sidebarToggle.onclick = () => {
|
|
|
|
| 1003 |
};
|
| 1004 |
mobileMenuBtn.onclick = () => sidebarEl.classList.toggle('mobile-open');
|
| 1005 |
|
| 1006 |
+
function setBrainMode(on) {
|
| 1007 |
+
brainMode = !!on;
|
| 1008 |
+
document.body.classList.toggle('brain-mode', brainMode);
|
| 1009 |
+
brainBtn.classList.toggle('active', brainMode);
|
| 1010 |
+
brainBtn.setAttribute('aria-pressed', String(brainMode));
|
| 1011 |
+
if (brainStage) brainStage.setAttribute('aria-hidden', String(!brainMode));
|
| 1012 |
+
if (voiceCaption) voiceCaption.textContent = '';
|
| 1013 |
+
if (brainMode) {
|
| 1014 |
+
brainBubbleSttText.textContent = 'Listening…';
|
| 1015 |
+
brainBubbleTtsText.textContent = 'Waiting…';
|
| 1016 |
+
brainVoiceActive = true;
|
| 1017 |
+
sidebarEl.classList.add('collapsed');
|
| 1018 |
+
sidebarToggle.textContent = '›';
|
| 1019 |
+
chatBox.scrollTop = chatBox.scrollHeight;
|
| 1020 |
+
textInput.blur();
|
| 1021 |
+
_brainModeSetSearch(isProcessing || isListening || isSpeaking || _ttsPlaying);
|
| 1022 |
+
if (!isListening && !isProcessing && !isRecordingLocked) {
|
| 1023 |
+
setTimeout(() => {
|
| 1024 |
+
if (brainMode && brainVoiceActive && !isListening && !isProcessing && !isRecordingLocked) {
|
| 1025 |
+
_brainResumeListening();
|
| 1026 |
+
}
|
| 1027 |
+
}, 180);
|
| 1028 |
+
}
|
| 1029 |
+
} else {
|
| 1030 |
+
brainVoiceActive = false;
|
| 1031 |
+
clearTimeout(brainAutoRestartTimer);
|
| 1032 |
+
clearTimeout(brainRestartTimer);
|
| 1033 |
+
brainPendingAudio = null;
|
| 1034 |
+
sidebarEl.classList.remove('collapsed');
|
| 1035 |
+
sidebarToggle.textContent = '‹';
|
| 1036 |
+
_brainModeSetSearch(false);
|
| 1037 |
+
_brainSetSttBubble('');
|
| 1038 |
+
_brainSetTtsBubble('');
|
| 1039 |
+
}
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
function _brainModeSetSearch(active) {
|
| 1043 |
+
if (!brainStage) return;
|
| 1044 |
+
brainStage.classList.toggle('searching', !!active);
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
+
function _brainSetSttBubble(text) {
|
| 1048 |
+
if (!brainBubbleStt || !brainBubbleSttText) return;
|
| 1049 |
+
const value = (text || '').trim();
|
| 1050 |
+
brainBubbleSttText.textContent = value || 'Listening…';
|
| 1051 |
+
brainBubbleStt.classList.toggle('active', !!value);
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
function _brainSetTtsBubble(text, active = true) {
|
| 1055 |
+
if (!brainBubbleTts || !brainBubbleTtsText) return;
|
| 1056 |
+
const value = (text || '').trim();
|
| 1057 |
+
brainBubbleTtsText.textContent = value || 'Waiting…';
|
| 1058 |
+
brainBubbleTts.classList.toggle('active', active || !!value);
|
| 1059 |
+
brainBubbleTts.classList.toggle('speaking', active || !!value);
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
function _brainResumeListening() {
|
| 1063 |
+
if (!brainMode || !brainVoiceActive || isListening || isProcessing || isRecordingLocked) {
|
| 1064 |
+
return;
|
| 1065 |
+
}
|
| 1066 |
+
if (micStream && analyserCtx && analyser) {
|
| 1067 |
+
isListening = true;
|
| 1068 |
+
setMic('listening');
|
| 1069 |
+
setState('listening');
|
| 1070 |
+
voiceViz.classList.add('active');
|
| 1071 |
+
vadInt = setInterval(vadTick, VAD_MS);
|
| 1072 |
+
vizInt = setInterval(vizTick, 60);
|
| 1073 |
+
_brainModeSetSearch(false);
|
| 1074 |
+
console.log('[Brain] Mic re-armed');
|
| 1075 |
+
return;
|
| 1076 |
+
}
|
| 1077 |
+
startListening().catch((err) => {
|
| 1078 |
+
console.error('[Brain] resume failed:', err);
|
| 1079 |
+
});
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
function _queueBrainReconnect() {
|
| 1083 |
+
if (!brainMode || !brainVoiceActive) return;
|
| 1084 |
+
clearTimeout(brainRestartTimer);
|
| 1085 |
+
brainRestartTimer = setTimeout(() => {
|
| 1086 |
+
if (!brainMode || !brainVoiceActive) return;
|
| 1087 |
+
_flushBrainPendingAudio();
|
| 1088 |
+
}, 700);
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
function _flushVoicePendingPackets() {
|
| 1092 |
+
if (!voiceWS || voiceWS.readyState !== WebSocket.OPEN || !voicePendingPackets.length) {
|
| 1093 |
+
return;
|
| 1094 |
+
}
|
| 1095 |
+
const packets = voicePendingPackets.splice(0);
|
| 1096 |
+
for (const packet of packets) {
|
| 1097 |
+
try {
|
| 1098 |
+
voiceWS.send(packet);
|
| 1099 |
+
appendThinking();
|
| 1100 |
+
console.log('[Voice] queued packet flushed');
|
| 1101 |
+
} catch (err) {
|
| 1102 |
+
console.error('[Voice] flush failed:', err);
|
| 1103 |
+
voicePendingPackets.unshift(packet);
|
| 1104 |
+
_connectVoice();
|
| 1105 |
+
break;
|
| 1106 |
+
}
|
| 1107 |
+
}
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
function _flushBrainPendingAudio() {
|
| 1111 |
+
if (!brainPendingAudio) return;
|
| 1112 |
+
if (!voiceWS || voiceWS.readyState !== WebSocket.OPEN) {
|
| 1113 |
+
_queueBrainReconnect();
|
| 1114 |
+
return;
|
| 1115 |
+
}
|
| 1116 |
+
const buf = brainPendingAudio;
|
| 1117 |
+
brainPendingAudio = null;
|
| 1118 |
+
try {
|
| 1119 |
+
appendThinking();
|
| 1120 |
+
voiceWS.send(buf);
|
| 1121 |
+
console.log('[Brain] queued utterance flushed');
|
| 1122 |
+
} catch (err) {
|
| 1123 |
+
console.error('[Brain] flush failed:', err);
|
| 1124 |
+
brainPendingAudio = buf;
|
| 1125 |
+
_queueBrainReconnect();
|
| 1126 |
+
}
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
sThreshold.value = SILENCE_DB;
|
| 1130 |
sThresholdVal.textContent = SILENCE_DB + ' dB';
|
| 1131 |
sThreshold.oninput = () => {
|
frontend/style.css
CHANGED
|
@@ -1,235 +1,97 @@
|
|
| 1 |
-
/* ── Reset & base ── */
|
| 2 |
*,
|
| 3 |
*::before,
|
| 4 |
*::after {
|
| 5 |
-
margin: 0;
|
| 6 |
-
padding: 0;
|
| 7 |
box-sizing: border-box;
|
| 8 |
}
|
| 9 |
|
| 10 |
:root {
|
| 11 |
-
--bg: #
|
| 12 |
-
--
|
| 13 |
-
--
|
| 14 |
-
--
|
| 15 |
-
--
|
| 16 |
-
--
|
| 17 |
-
--
|
| 18 |
-
--
|
| 19 |
-
--
|
| 20 |
-
--
|
| 21 |
-
--
|
| 22 |
-
--
|
| 23 |
-
--
|
| 24 |
-
--
|
| 25 |
-
--
|
| 26 |
-
--
|
| 27 |
-
--
|
| 28 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
html,
|
| 32 |
body {
|
| 33 |
height: 100%;
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
color: var(--text);
|
| 36 |
font-family: 'Hind Siliguri', 'Syne', sans-serif;
|
| 37 |
overflow: hidden;
|
| 38 |
}
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
border-radius: 50%;
|
| 44 |
-
filter: blur(80px);
|
| 45 |
-
pointer-events: none;
|
| 46 |
-
z-index: 0;
|
| 47 |
-
opacity: 0.18;
|
| 48 |
-
animation: orb-float 12s ease-in-out infinite;
|
| 49 |
-
}
|
| 50 |
-
.orb-1 {
|
| 51 |
-
width: 500px; height: 500px;
|
| 52 |
-
background: radial-gradient(circle, #22d3ee, transparent);
|
| 53 |
-
top: -200px; left: -150px;
|
| 54 |
-
animation-delay: 0s;
|
| 55 |
-
}
|
| 56 |
-
.orb-2 {
|
| 57 |
-
width: 400px; height: 400px;
|
| 58 |
-
background: radial-gradient(circle, #818cf8, transparent);
|
| 59 |
-
bottom: -100px; right: -100px;
|
| 60 |
-
animation-delay: -4s;
|
| 61 |
-
}
|
| 62 |
-
.orb-3 {
|
| 63 |
-
width: 300px; height: 300px;
|
| 64 |
-
background: radial-gradient(circle, #f472b6, transparent);
|
| 65 |
-
top: 50%; left: 50%;
|
| 66 |
-
transform: translate(-50%, -50%);
|
| 67 |
-
animation-delay: -8s;
|
| 68 |
-
}
|
| 69 |
-
@keyframes orb-float {
|
| 70 |
-
0%, 100% { transform: translate(0, 0) scale(1); }
|
| 71 |
-
33% { transform: translate(30px, -20px) scale(1.05); }
|
| 72 |
-
66% { transform: translate(-20px, 15px) scale(0.97); }
|
| 73 |
}
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
z-index: 1000;
|
| 80 |
-
display: flex;
|
| 81 |
-
align-items: center;
|
| 82 |
-
justify-content: center;
|
| 83 |
-
background: var(--bg);
|
| 84 |
-
transition: opacity 0.6s ease, visibility 0.6s ease;
|
| 85 |
-
}
|
| 86 |
-
.init-overlay.hidden {
|
| 87 |
-
opacity: 0;
|
| 88 |
-
visibility: hidden;
|
| 89 |
-
pointer-events: none;
|
| 90 |
}
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
border: 1px solid var(--border2);
|
| 95 |
-
border-radius: 24px;
|
| 96 |
-
padding: 48px 56px;
|
| 97 |
-
width: 480px;
|
| 98 |
-
max-width: 95vw;
|
| 99 |
-
text-align: center;
|
| 100 |
-
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
|
| 101 |
-
}
|
| 102 |
-
.init-logo {
|
| 103 |
-
margin-bottom: 20px;
|
| 104 |
-
animation: logo-pulse 2s ease-in-out infinite;
|
| 105 |
-
}
|
| 106 |
-
@keyframes logo-pulse {
|
| 107 |
-
0%, 100% { filter: drop-shadow(0 0 12px rgba(34, 211, 238, 0.4)); transform: scale(1); }
|
| 108 |
-
50% { filter: drop-shadow(0 0 24px rgba(129, 140, 248, 0.6)); transform: scale(1.06); }
|
| 109 |
-
}
|
| 110 |
-
.init-title {
|
| 111 |
-
font-family: 'Syne', sans-serif;
|
| 112 |
-
font-size: 26px;
|
| 113 |
-
font-weight: 800;
|
| 114 |
-
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
| 115 |
-
-webkit-background-clip: text;
|
| 116 |
-
-webkit-text-fill-color: transparent;
|
| 117 |
-
background-clip: text;
|
| 118 |
-
margin-bottom: 6px;
|
| 119 |
-
}
|
| 120 |
-
.init-subtitle {
|
| 121 |
-
font-family: 'Hind Siliguri', sans-serif;
|
| 122 |
-
color: var(--text2);
|
| 123 |
-
font-size: 15px;
|
| 124 |
-
margin-bottom: 36px;
|
| 125 |
-
}
|
| 126 |
-
.init-stages {
|
| 127 |
-
text-align: left;
|
| 128 |
-
margin-bottom: 28px;
|
| 129 |
-
}
|
| 130 |
-
.stage {
|
| 131 |
-
display: flex;
|
| 132 |
-
align-items: center;
|
| 133 |
-
gap: 12px;
|
| 134 |
-
padding: 10px 0;
|
| 135 |
-
font-size: 13px;
|
| 136 |
-
color: var(--text3);
|
| 137 |
-
border-bottom: 1px solid var(--border);
|
| 138 |
-
transition: color 0.3s;
|
| 139 |
-
}
|
| 140 |
-
.stage.active { color: var(--accent); }
|
| 141 |
-
.stage.done { color: var(--green); }
|
| 142 |
-
.stage-dot {
|
| 143 |
-
width: 8px; height: 8px;
|
| 144 |
-
border-radius: 50%;
|
| 145 |
-
background: var(--text3);
|
| 146 |
-
flex-shrink: 0;
|
| 147 |
-
transition: background 0.3s, box-shadow 0.3s;
|
| 148 |
-
}
|
| 149 |
-
.stage.active .stage-dot {
|
| 150 |
-
background: var(--accent);
|
| 151 |
-
box-shadow: 0 0 8px var(--accent);
|
| 152 |
-
animation: blink-dot 0.8s ease-in-out infinite;
|
| 153 |
-
}
|
| 154 |
-
.stage.done .stage-dot { background: var(--green); }
|
| 155 |
-
@keyframes blink-dot {
|
| 156 |
-
0%, 100% { opacity: 1; }
|
| 157 |
-
50% { opacity: 0.3; }
|
| 158 |
-
}
|
| 159 |
-
.stage-check {
|
| 160 |
-
margin-left: auto;
|
| 161 |
-
opacity: 0;
|
| 162 |
-
transition: opacity 0.3s;
|
| 163 |
-
}
|
| 164 |
-
.stage.done .stage-check { opacity: 1; }
|
| 165 |
-
.stage span {
|
| 166 |
-
flex: 1;
|
| 167 |
-
font-family: 'Hind Siliguri', sans-serif;
|
| 168 |
-
}
|
| 169 |
-
.init-bar-wrap {
|
| 170 |
-
background: var(--bg3);
|
| 171 |
-
border-radius: 99px;
|
| 172 |
-
height: 6px;
|
| 173 |
-
overflow: hidden;
|
| 174 |
-
margin-bottom: 16px;
|
| 175 |
-
border: 1px solid var(--border);
|
| 176 |
-
}
|
| 177 |
-
.init-bar {
|
| 178 |
-
height: 100%;
|
| 179 |
-
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
| 180 |
-
border-radius: 99px;
|
| 181 |
-
width: 0%;
|
| 182 |
-
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
| 183 |
-
box-shadow: 0 0 12px rgba(34, 211, 238, 0.5);
|
| 184 |
-
}
|
| 185 |
-
.init-status {
|
| 186 |
-
font-size: 12px;
|
| 187 |
-
color: var(--text2);
|
| 188 |
-
font-family: 'JetBrains Mono', monospace;
|
| 189 |
}
|
| 190 |
|
| 191 |
-
/* ──
|
| 192 |
-
FIX-7: App is hidden by default via opacity/pointer-events.
|
| 193 |
-
JS adds class .visible after init overlay closes. This prevents
|
| 194 |
-
any flash of unstyled content (FOUC) during JS execution.
|
| 195 |
-
── */
|
| 196 |
.app {
|
| 197 |
position: fixed;
|
| 198 |
inset: 0;
|
| 199 |
-
z-index: 1;
|
| 200 |
display: flex;
|
| 201 |
-
opacity: 0;
|
| 202 |
-
pointer-events: none;
|
| 203 |
-
transition: opacity 0.5s ease;
|
| 204 |
-
}
|
| 205 |
-
.app.visible {
|
| 206 |
opacity: 1;
|
| 207 |
pointer-events: auto;
|
| 208 |
}
|
| 209 |
|
| 210 |
-
/* ── Sidebar ── */
|
| 211 |
.sidebar {
|
| 212 |
width: var(--sidebar-w);
|
| 213 |
-
|
| 214 |
-
border-right: 1px solid var(--border);
|
| 215 |
display: flex;
|
| 216 |
flex-direction: column;
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
| 218 |
overflow-y: auto;
|
| 219 |
-
transition: width var(--transition), transform var(--transition);
|
| 220 |
-
z-index: 10;
|
| 221 |
}
|
|
|
|
| 222 |
.sidebar.collapsed {
|
| 223 |
width: 0;
|
| 224 |
overflow: hidden;
|
| 225 |
}
|
|
|
|
| 226 |
.sidebar-header {
|
| 227 |
display: flex;
|
| 228 |
align-items: center;
|
| 229 |
justify-content: space-between;
|
| 230 |
-
padding:
|
| 231 |
-
border-bottom: 1px solid var(--border);
|
| 232 |
}
|
|
|
|
| 233 |
.brand {
|
| 234 |
display: flex;
|
| 235 |
align-items: center;
|
|
@@ -237,109 +99,130 @@ body {
|
|
| 237 |
font-family: 'Syne', sans-serif;
|
| 238 |
font-weight: 700;
|
| 239 |
font-size: 14px;
|
| 240 |
-
|
| 241 |
}
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
border: 1px solid var(--border);
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
cursor: pointer;
|
| 249 |
-
|
| 250 |
-
transition: all var(--transition);
|
| 251 |
}
|
|
|
|
| 252 |
.sidebar-toggle:hover {
|
| 253 |
-
|
| 254 |
-
|
| 255 |
}
|
| 256 |
|
| 257 |
-
.
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
justify-content: space-between;
|
| 262 |
-
padding: 6px 0;
|
| 263 |
-
}
|
| 264 |
-
.status-label { font-size: 12px; color: var(--text2); }
|
| 265 |
-
.status-badge {
|
| 266 |
-
font-size: 10px;
|
| 267 |
-
font-family: 'JetBrains Mono', monospace;
|
| 268 |
-
padding: 2px 8px;
|
| 269 |
-
border-radius: 99px;
|
| 270 |
-
font-weight: 600;
|
| 271 |
-
letter-spacing: 0.03em;
|
| 272 |
}
|
| 273 |
-
.badge-green { background: rgba(74, 222, 128, 0.12); color: var(--green); }
|
| 274 |
-
.badge-yellow { background: rgba(251, 191, 36, 0.12); color: var(--yellow); }
|
| 275 |
-
.badge-red { background: rgba(248, 113, 113, 0.12); color: var(--red); }
|
| 276 |
|
| 277 |
-
.
|
|
|
|
|
|
|
| 278 |
|
| 279 |
-
.dash-section { padding: 16px; }
|
| 280 |
.dash-title {
|
| 281 |
font-size: 11px;
|
| 282 |
-
|
| 283 |
text-transform: uppercase;
|
| 284 |
-
|
| 285 |
-
color: var(--text2);
|
| 286 |
margin-bottom: 12px;
|
| 287 |
}
|
|
|
|
| 288 |
.metric-grid {
|
| 289 |
display: grid;
|
| 290 |
-
grid-template-columns:
|
| 291 |
-
gap:
|
| 292 |
}
|
|
|
|
| 293 |
.metric-card {
|
| 294 |
-
background:
|
| 295 |
border: 1px solid var(--border);
|
| 296 |
-
border-radius:
|
| 297 |
-
padding: 10px;
|
|
|
|
| 298 |
text-align: center;
|
| 299 |
}
|
|
|
|
| 300 |
.metric-val {
|
| 301 |
font-family: 'JetBrains Mono', monospace;
|
| 302 |
font-size: 18px;
|
| 303 |
-
font-weight: 400;
|
| 304 |
color: var(--accent);
|
| 305 |
line-height: 1;
|
| 306 |
margin-bottom: 4px;
|
| 307 |
}
|
| 308 |
-
.metric-label { font-size: 10px; color: var(--text3); }
|
| 309 |
|
| 310 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
.setting-row label {
|
| 312 |
display: block;
|
| 313 |
font-size: 11px;
|
| 314 |
-
color: var(--
|
| 315 |
-
margin-bottom:
|
| 316 |
}
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
.slider-wrap input[type='range'] {
|
| 319 |
flex: 1;
|
| 320 |
-
accent-color: var(--accent);
|
| 321 |
-
height: 4px;
|
| 322 |
-
cursor: pointer;
|
| 323 |
}
|
|
|
|
| 324 |
.slider-wrap span {
|
|
|
|
|
|
|
| 325 |
font-size: 11px;
|
|
|
|
| 326 |
font-family: 'JetBrains Mono', monospace;
|
| 327 |
-
color: var(--accent);
|
| 328 |
-
min-width: 58px;
|
| 329 |
-
text-align: right;
|
| 330 |
}
|
|
|
|
| 331 |
.setting-select {
|
| 332 |
width: 100%;
|
| 333 |
-
|
| 334 |
-
border:
|
|
|
|
| 335 |
color: var(--text);
|
| 336 |
-
border-radius: 8px;
|
| 337 |
-
padding: 6px 10px;
|
| 338 |
-
font-size: 12px;
|
| 339 |
-
font-family: 'Hind Siliguri', sans-serif;
|
| 340 |
-
cursor: pointer;
|
| 341 |
}
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
.queue-vis {
|
| 345 |
display: flex;
|
|
@@ -348,342 +231,952 @@ body {
|
|
| 348 |
height: 48px;
|
| 349 |
margin-bottom: 8px;
|
| 350 |
}
|
|
|
|
| 351 |
.queue-bar {
|
| 352 |
flex: 1;
|
| 353 |
-
background: var(--accent);
|
| 354 |
-
border-radius: 3px;
|
| 355 |
-
opacity: 0.3;
|
| 356 |
-
transition: height 0.15s ease, opacity 0.15s ease;
|
| 357 |
min-height: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
}
|
| 359 |
-
|
| 360 |
-
.queue-
|
| 361 |
-
|
| 362 |
-
color: var(--text2);
|
| 363 |
-
font-family: 'JetBrains Mono', monospace;
|
| 364 |
}
|
| 365 |
|
| 366 |
-
/* ── Main ── */
|
| 367 |
.main {
|
| 368 |
flex: 1;
|
|
|
|
| 369 |
display: flex;
|
| 370 |
flex-direction: column;
|
| 371 |
overflow: hidden;
|
| 372 |
-
min-width: 0;
|
| 373 |
}
|
| 374 |
|
| 375 |
-
/* ── Topbar ── */
|
| 376 |
.topbar {
|
| 377 |
display: flex;
|
| 378 |
align-items: center;
|
| 379 |
justify-content: space-between;
|
| 380 |
-
padding: 14px
|
| 381 |
-
background: var(--bg2);
|
| 382 |
border-bottom: 1px solid var(--border);
|
|
|
|
|
|
|
| 383 |
flex-shrink: 0;
|
| 384 |
}
|
| 385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
.topbar-center {
|
| 387 |
-
font-family: 'Syne', sans-serif;
|
| 388 |
-
font-weight: 700;
|
| 389 |
-
font-size: 15px;
|
| 390 |
-
color: var(--text);
|
| 391 |
position: absolute;
|
| 392 |
left: 50%;
|
| 393 |
transform: translateX(-50%);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
}
|
| 395 |
-
|
| 396 |
-
.
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
border: 1px solid var(--border);
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
cursor: pointer;
|
| 404 |
-
font-size: 16px;
|
| 405 |
}
|
|
|
|
| 406 |
.state-dot {
|
| 407 |
-
width: 8px;
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
transition: background 0.3s, box-shadow 0.3s;
|
| 413 |
}
|
| 414 |
-
.state-dot.listening { background: var(--accent); box-shadow: 0 0 8px var(--accent); animation: blink-dot 0.8s infinite; }
|
| 415 |
-
.state-dot.recording { background: var(--red); box-shadow: 0 0 10px var(--red); animation: blink-dot 0.4s infinite; }
|
| 416 |
-
.state-dot.processing { background: var(--yellow); box-shadow: 0 0 8px var(--yellow); animation: blink-dot 1s infinite; }
|
| 417 |
-
.state-dot.speaking { background: var(--accent2); box-shadow: 0 0 10px var(--accent2);animation: blink-dot 0.6s infinite; }
|
| 418 |
-
#state-label { font-size: 13px; color: var(--text2); font-family: 'JetBrains Mono', monospace; }
|
| 419 |
|
| 420 |
-
.
|
| 421 |
-
background:
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
font-size: 12px;
|
| 428 |
-
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
}
|
| 431 |
-
.clear-btn:hover { border-color: var(--accent); color: var(--accent); }
|
| 432 |
|
| 433 |
-
/* ── Chat ── */
|
| 434 |
#chat-box {
|
|
|
|
|
|
|
|
|
|
| 435 |
flex: 1;
|
| 436 |
overflow-y: auto;
|
| 437 |
-
padding: 24px 20px 12px;
|
| 438 |
display: flex;
|
| 439 |
flex-direction: column;
|
| 440 |
gap: 12px;
|
| 441 |
scroll-behavior: smooth;
|
| 442 |
}
|
| 443 |
-
|
| 444 |
-
#chat-box::-webkit-scrollbar
|
| 445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
|
| 447 |
.message {
|
| 448 |
-
max-width:
|
| 449 |
-
padding: 14px
|
| 450 |
-
border-radius:
|
| 451 |
line-height: 1.65;
|
| 452 |
-
font-size:
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
|
|
|
| 457 |
}
|
|
|
|
| 458 |
@keyframes msg-in {
|
| 459 |
-
from {
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
}
|
|
|
|
| 462 |
.message.user {
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
border-bottom-right-radius: 4px;
|
| 467 |
}
|
|
|
|
| 468 |
.message.ai {
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
border-bottom-left-radius:
|
| 472 |
}
|
|
|
|
| 473 |
.message.system {
|
| 474 |
-
background: rgba(251, 191, 36, 0.08);
|
| 475 |
-
border: 1px solid rgba(251, 191, 36, 0.2);
|
| 476 |
-
color: var(--yellow);
|
| 477 |
-
font-size: 12px;
|
| 478 |
-
font-family: 'JetBrains Mono', monospace;
|
| 479 |
align-self: center;
|
| 480 |
-
max-width:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
}
|
| 482 |
-
|
| 483 |
-
.message li { margin-bottom: 4px; }
|
| 484 |
-
.message p { margin: 6px 0; }
|
| 485 |
.message code {
|
| 486 |
-
background: rgba(0, 0, 0, 0.3);
|
| 487 |
-
border-radius: 4px;
|
| 488 |
padding: 1px 6px;
|
|
|
|
|
|
|
| 489 |
font-family: 'JetBrains Mono', monospace;
|
| 490 |
font-size: 13px;
|
| 491 |
}
|
|
|
|
| 492 |
.message pre {
|
| 493 |
-
background: rgba(0, 0, 0, 0.3);
|
| 494 |
-
border-radius: 8px;
|
| 495 |
-
padding: 12px;
|
| 496 |
overflow-x: auto;
|
| 497 |
-
|
|
|
|
|
|
|
| 498 |
}
|
| 499 |
|
| 500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
.voice-visualizer {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
display: flex;
|
| 503 |
align-items: center;
|
| 504 |
justify-content: center;
|
| 505 |
gap: 4px;
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
|
|
|
| 510 |
}
|
| 511 |
-
|
| 512 |
.viz-bar {
|
| 513 |
width: 4px;
|
| 514 |
-
border-radius: 99px;
|
| 515 |
-
background: linear-gradient(180deg, var(--accent), var(--accent2));
|
| 516 |
height: 6px;
|
|
|
|
|
|
|
| 517 |
transition: height 0.08s ease;
|
| 518 |
-
flex-shrink: 0;
|
| 519 |
}
|
| 520 |
|
| 521 |
-
/* ── Controls ── */
|
| 522 |
.controls {
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
flex-shrink: 0;
|
| 527 |
}
|
|
|
|
| 528 |
.text-row {
|
| 529 |
display: flex;
|
|
|
|
| 530 |
gap: 10px;
|
| 531 |
-
margin-bottom:
|
| 532 |
-
align-items: flex-end; /* FIX-4: align send button to bottom when textarea grows */
|
| 533 |
}
|
| 534 |
|
| 535 |
-
/* ── FIX-4: Auto-growing textarea replaces <input type="text"> ── */
|
| 536 |
#text-input {
|
| 537 |
flex: 1;
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
|
|
|
|
|
|
|
|
|
| 542 |
color: var(--text);
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
overflow-y: hidden; /* hidden until 10 lines exceeded; JS manages */
|
| 549 |
-
line-height: 1.57; /* ~22px per line at font-size 14px */
|
| 550 |
-
min-height: 44px; /* single line min */
|
| 551 |
-
max-height: 226px; /* 10 lines × 22px + 16px padding */
|
| 552 |
-
display: block;
|
| 553 |
-
/* smooth height animation */
|
| 554 |
-
transition: border-color var(--transition), height 0.1s ease;
|
| 555 |
}
|
| 556 |
-
#text-input::placeholder { color: var(--text3); }
|
| 557 |
-
#text-input:focus { border-color: var(--accent); }
|
| 558 |
-
/* Custom scrollbar inside textarea once > 10 lines */
|
| 559 |
-
#text-input::-webkit-scrollbar { width: 4px; }
|
| 560 |
-
#text-input::-webkit-scrollbar-track { background: transparent; }
|
| 561 |
-
#text-input::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 99px; }
|
| 562 |
|
| 563 |
#send-btn {
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
border-radius:
|
| 567 |
-
|
|
|
|
| 568 |
cursor: pointer;
|
| 569 |
-
|
| 570 |
-
display: flex;
|
| 571 |
align-items: center;
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
| 576 |
}
|
| 577 |
-
#send-btn:hover { opacity: 0.88; }
|
| 578 |
-
#send-btn:active { transform: scale(0.95); }
|
| 579 |
|
| 580 |
-
.voice-row { display: flex; gap: 10px; }
|
| 581 |
.mic-btn {
|
| 582 |
flex: 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
display: flex;
|
| 584 |
align-items: center;
|
| 585 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
gap: 8px;
|
| 587 |
-
padding: 13px 20px;
|
| 588 |
-
border-radius: 14px;
|
| 589 |
-
border: 1.5px solid var(--border2);
|
| 590 |
-
background: var(--bg3);
|
| 591 |
-
color: var(--text);
|
| 592 |
cursor: pointer;
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
overflow: hidden;
|
| 598 |
}
|
| 599 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
content: '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
position: absolute;
|
| 602 |
inset: 0;
|
| 603 |
-
background:
|
| 604 |
-
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
}
|
| 607 |
-
|
| 608 |
-
.
|
| 609 |
-
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
}
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
animation:
|
|
|
|
| 615 |
}
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
}
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
|
|
|
| 623 |
}
|
| 624 |
-
.mic-icon { font-size: 18px; position: relative; z-index: 1; }
|
| 625 |
-
.mic-label { position: relative; z-index: 1; }
|
| 626 |
|
| 627 |
-
.
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
display: flex;
|
| 637 |
-
align-items: center;
|
| 638 |
-
gap: 6px;
|
| 639 |
-
transition: all var(--transition);
|
| 640 |
}
|
| 641 |
-
.stop-btn:hover { background: rgba(248, 113, 113, 0.2); border-color: var(--red); }
|
| 642 |
-
.stop-btn:active { transform: scale(0.95); }
|
| 643 |
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
-
|
| 650 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
.sidebar {
|
| 652 |
position: fixed;
|
| 653 |
-
|
| 654 |
transform: translateX(-100%);
|
| 655 |
-
z-index:
|
|
|
|
|
|
|
| 656 |
}
|
| 657 |
-
.sidebar.mobile-open { transform: translateX(0); }
|
| 658 |
-
.mobile-menu-btn { display: flex; }
|
| 659 |
-
.topbar-center { font-size: 13px; }
|
| 660 |
-
.message { max-width: 90%; font-size: 14px; }
|
| 661 |
-
}
|
| 662 |
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
}
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
}
|
|
|
|
| 1 |
+
/* ── Reset & base ─────────────────────────────────────────────────────────── */
|
| 2 |
*,
|
| 3 |
*::before,
|
| 4 |
*::after {
|
|
|
|
|
|
|
| 5 |
box-sizing: border-box;
|
| 6 |
}
|
| 7 |
|
| 8 |
:root {
|
| 9 |
+
--bg: #05070d;
|
| 10 |
+
--panel: rgba(10, 14, 24, 0.76);
|
| 11 |
+
--panel-strong: rgba(13, 18, 30, 0.94);
|
| 12 |
+
--panel-soft: rgba(255, 255, 255, 0.03);
|
| 13 |
+
--border: rgba(255, 255, 255, 0.08);
|
| 14 |
+
--border-strong: rgba(255, 255, 255, 0.14);
|
| 15 |
+
--text: #e5eefb;
|
| 16 |
+
--text-2: #a9b7cc;
|
| 17 |
+
--text-3: #6f7d92;
|
| 18 |
+
--accent: #f5f7fb;
|
| 19 |
+
--accent-2: #64b5ff;
|
| 20 |
+
--good: #16a34a;
|
| 21 |
+
--warn: #f59e0b;
|
| 22 |
+
--danger: #ff5e7b;
|
| 23 |
+
--shadow: 0 22px 60px rgba(0, 0, 0, 0.45);
|
| 24 |
+
--shadow-soft: 0 10px 26px rgba(0, 0, 0, 0.32);
|
| 25 |
+
--radius-xl: 28px;
|
| 26 |
+
--radius-lg: 20px;
|
| 27 |
+
--radius-md: 14px;
|
| 28 |
+
--radius-sm: 10px;
|
| 29 |
+
--sidebar-w: 304px;
|
| 30 |
+
--max-chat: 860px;
|
| 31 |
}
|
| 32 |
|
| 33 |
html,
|
| 34 |
body {
|
| 35 |
height: 100%;
|
| 36 |
+
margin: 0;
|
| 37 |
+
background:
|
| 38 |
+
radial-gradient(circle at top left, rgba(100, 181, 255, 0.14), transparent 20%),
|
| 39 |
+
radial-gradient(circle at 75% 25%, rgba(133, 95, 255, 0.12), transparent 18%),
|
| 40 |
+
radial-gradient(circle at bottom right, rgba(22, 163, 74, 0.08), transparent 22%),
|
| 41 |
+
var(--bg);
|
| 42 |
color: var(--text);
|
| 43 |
font-family: 'Hind Siliguri', 'Syne', sans-serif;
|
| 44 |
overflow: hidden;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
body {
|
| 48 |
+
-webkit-font-smoothing: antialiased;
|
| 49 |
+
text-rendering: optimizeLegibility;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
+
button,
|
| 53 |
+
input,
|
| 54 |
+
select {
|
| 55 |
+
font: inherit;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
+
button {
|
| 59 |
+
appearance: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
+
/* ── Layout ───────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
.app {
|
| 64 |
position: fixed;
|
| 65 |
inset: 0;
|
|
|
|
| 66 |
display: flex;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
opacity: 1;
|
| 68 |
pointer-events: auto;
|
| 69 |
}
|
| 70 |
|
|
|
|
| 71 |
.sidebar {
|
| 72 |
width: var(--sidebar-w);
|
| 73 |
+
flex-shrink: 0;
|
|
|
|
| 74 |
display: flex;
|
| 75 |
flex-direction: column;
|
| 76 |
+
background: rgba(8, 12, 20, 0.82);
|
| 77 |
+
backdrop-filter: blur(20px);
|
| 78 |
+
border-right: 1px solid var(--border);
|
| 79 |
+
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.03);
|
| 80 |
overflow-y: auto;
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
+
|
| 83 |
.sidebar.collapsed {
|
| 84 |
width: 0;
|
| 85 |
overflow: hidden;
|
| 86 |
}
|
| 87 |
+
|
| 88 |
.sidebar-header {
|
| 89 |
display: flex;
|
| 90 |
align-items: center;
|
| 91 |
justify-content: space-between;
|
| 92 |
+
padding: 18px 16px 14px;
|
|
|
|
| 93 |
}
|
| 94 |
+
|
| 95 |
.brand {
|
| 96 |
display: flex;
|
| 97 |
align-items: center;
|
|
|
|
| 99 |
font-family: 'Syne', sans-serif;
|
| 100 |
font-weight: 700;
|
| 101 |
font-size: 14px;
|
| 102 |
+
letter-spacing: 0.02em;
|
| 103 |
}
|
| 104 |
+
|
| 105 |
+
.sidebar-toggle,
|
| 106 |
+
.brain-btn,
|
| 107 |
+
.clear-btn,
|
| 108 |
+
.mobile-menu-btn,
|
| 109 |
+
.stop-btn,
|
| 110 |
+
#send-btn,
|
| 111 |
+
.mic-btn,
|
| 112 |
+
.setting-select,
|
| 113 |
+
#text-input {
|
| 114 |
border: 1px solid var(--border);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.sidebar-toggle {
|
| 118 |
+
width: 34px;
|
| 119 |
+
height: 34px;
|
| 120 |
+
border-radius: 999px;
|
| 121 |
+
background: rgba(255, 255, 255, 0.04);
|
| 122 |
+
color: var(--text-2);
|
| 123 |
+
display: grid;
|
| 124 |
+
place-items: center;
|
| 125 |
cursor: pointer;
|
| 126 |
+
transition: transform 0.15s ease, border-color 0.15s ease;
|
|
|
|
| 127 |
}
|
| 128 |
+
|
| 129 |
.sidebar-toggle:hover {
|
| 130 |
+
border-color: var(--border-strong);
|
| 131 |
+
transform: translateY(-1px);
|
| 132 |
}
|
| 133 |
|
| 134 |
+
.sidebar-divider {
|
| 135 |
+
height: 1px;
|
| 136 |
+
margin: 4px 16px;
|
| 137 |
+
background: var(--border);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
+
.dash-section {
|
| 141 |
+
padding: 16px;
|
| 142 |
+
}
|
| 143 |
|
|
|
|
| 144 |
.dash-title {
|
| 145 |
font-size: 11px;
|
| 146 |
+
letter-spacing: 0.12em;
|
| 147 |
text-transform: uppercase;
|
| 148 |
+
color: var(--text-3);
|
|
|
|
| 149 |
margin-bottom: 12px;
|
| 150 |
}
|
| 151 |
+
|
| 152 |
.metric-grid {
|
| 153 |
display: grid;
|
| 154 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 155 |
+
gap: 10px;
|
| 156 |
}
|
| 157 |
+
|
| 158 |
.metric-card {
|
| 159 |
+
background: rgba(255, 255, 255, 0.04);
|
| 160 |
border: 1px solid var(--border);
|
| 161 |
+
border-radius: 16px;
|
| 162 |
+
padding: 12px 10px;
|
| 163 |
+
box-shadow: var(--shadow-soft);
|
| 164 |
text-align: center;
|
| 165 |
}
|
| 166 |
+
|
| 167 |
.metric-val {
|
| 168 |
font-family: 'JetBrains Mono', monospace;
|
| 169 |
font-size: 18px;
|
|
|
|
| 170 |
color: var(--accent);
|
| 171 |
line-height: 1;
|
| 172 |
margin-bottom: 4px;
|
| 173 |
}
|
|
|
|
| 174 |
|
| 175 |
+
.metric-label,
|
| 176 |
+
.queue-label,
|
| 177 |
+
.status-label {
|
| 178 |
+
color: var(--text-3);
|
| 179 |
+
font-size: 11px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.setting-row {
|
| 183 |
+
margin-bottom: 14px;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
.setting-row label {
|
| 187 |
display: block;
|
| 188 |
font-size: 11px;
|
| 189 |
+
color: var(--text-2);
|
| 190 |
+
margin-bottom: 8px;
|
| 191 |
}
|
| 192 |
+
|
| 193 |
+
.slider-wrap {
|
| 194 |
+
display: flex;
|
| 195 |
+
align-items: center;
|
| 196 |
+
gap: 10px;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
.slider-wrap input[type='range'] {
|
| 200 |
flex: 1;
|
| 201 |
+
accent-color: var(--accent-2);
|
|
|
|
|
|
|
| 202 |
}
|
| 203 |
+
|
| 204 |
.slider-wrap span {
|
| 205 |
+
min-width: 64px;
|
| 206 |
+
text-align: right;
|
| 207 |
font-size: 11px;
|
| 208 |
+
color: var(--text-2);
|
| 209 |
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
+
|
| 212 |
.setting-select {
|
| 213 |
width: 100%;
|
| 214 |
+
padding: 11px 12px;
|
| 215 |
+
border-radius: 14px;
|
| 216 |
+
background: rgba(255, 255, 255, 0.04);
|
| 217 |
color: var(--text);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
}
|
| 219 |
+
|
| 220 |
+
.setting-select:focus,
|
| 221 |
+
#text-input:focus {
|
| 222 |
+
outline: none;
|
| 223 |
+
border-color: rgba(37, 99, 235, 0.4);
|
| 224 |
+
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.08);
|
| 225 |
+
}
|
| 226 |
|
| 227 |
.queue-vis {
|
| 228 |
display: flex;
|
|
|
|
| 231 |
height: 48px;
|
| 232 |
margin-bottom: 8px;
|
| 233 |
}
|
| 234 |
+
|
| 235 |
.queue-bar {
|
| 236 |
flex: 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
min-height: 4px;
|
| 238 |
+
border-radius: 999px;
|
| 239 |
+
background: linear-gradient(180deg, rgba(37, 99, 235, 0.85), rgba(15, 23, 42, 0.55));
|
| 240 |
+
opacity: 0.2;
|
| 241 |
+
transition: height 0.14s ease, opacity 0.14s ease;
|
| 242 |
}
|
| 243 |
+
|
| 244 |
+
.queue-bar.active {
|
| 245 |
+
opacity: 0.95;
|
|
|
|
|
|
|
| 246 |
}
|
| 247 |
|
| 248 |
+
/* ── Main column ──────────────────────────────────────────────────────────── */
|
| 249 |
.main {
|
| 250 |
flex: 1;
|
| 251 |
+
min-width: 0;
|
| 252 |
display: flex;
|
| 253 |
flex-direction: column;
|
| 254 |
overflow: hidden;
|
|
|
|
| 255 |
}
|
| 256 |
|
|
|
|
| 257 |
.topbar {
|
| 258 |
display: flex;
|
| 259 |
align-items: center;
|
| 260 |
justify-content: space-between;
|
| 261 |
+
padding: 14px 18px;
|
|
|
|
| 262 |
border-bottom: 1px solid var(--border);
|
| 263 |
+
background: rgba(8, 12, 20, 0.72);
|
| 264 |
+
backdrop-filter: blur(18px);
|
| 265 |
flex-shrink: 0;
|
| 266 |
}
|
| 267 |
+
|
| 268 |
+
.topbar-left,
|
| 269 |
+
.topbar-right {
|
| 270 |
+
display: flex;
|
| 271 |
+
align-items: center;
|
| 272 |
+
gap: 10px;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
.topbar-center {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
position: absolute;
|
| 277 |
left: 50%;
|
| 278 |
transform: translateX(-50%);
|
| 279 |
+
font-family: 'Syne', sans-serif;
|
| 280 |
+
font-weight: 700;
|
| 281 |
+
font-size: 14px;
|
| 282 |
+
color: var(--text);
|
| 283 |
+
letter-spacing: 0.01em;
|
| 284 |
}
|
| 285 |
+
|
| 286 |
+
.topbar-title {
|
| 287 |
+
opacity: 0.92;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.topbar-state {
|
| 291 |
+
display: flex;
|
| 292 |
+
align-items: center;
|
| 293 |
+
gap: 8px;
|
| 294 |
+
padding: 8px 12px;
|
| 295 |
border: 1px solid var(--border);
|
| 296 |
+
border-radius: 999px;
|
| 297 |
+
background: rgba(255, 255, 255, 0.04);
|
| 298 |
+
box-shadow: var(--shadow-soft);
|
|
|
|
|
|
|
| 299 |
}
|
| 300 |
+
|
| 301 |
.state-dot {
|
| 302 |
+
width: 8px;
|
| 303 |
+
height: 8px;
|
| 304 |
+
border-radius: 999px;
|
| 305 |
+
background: var(--good);
|
| 306 |
+
box-shadow: 0 0 0 4px rgba(22, 163, 74, 0.12);
|
|
|
|
| 307 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
+
.state-dot.listening {
|
| 310 |
+
background: var(--accent-2);
|
| 311 |
+
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.state-dot.recording {
|
| 315 |
+
background: var(--danger);
|
| 316 |
+
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.12);
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.state-dot.processing {
|
| 320 |
+
background: var(--warn);
|
| 321 |
+
box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.12);
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.state-dot.speaking {
|
| 325 |
+
background: var(--accent);
|
| 326 |
+
box-shadow: 0 0 0 4px rgba(15, 23, 42, 0.08);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
#state-label {
|
| 330 |
+
font-size: 12px;
|
| 331 |
+
color: var(--text-2);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.mobile-menu-btn,
|
| 335 |
+
.clear-btn,
|
| 336 |
+
.brain-btn {
|
| 337 |
+
height: 38px;
|
| 338 |
+
border-radius: 999px;
|
| 339 |
+
background: rgba(255, 255, 255, 0.04);
|
| 340 |
+
color: var(--text);
|
| 341 |
cursor: pointer;
|
| 342 |
+
padding: 0 14px;
|
| 343 |
+
display: inline-flex;
|
| 344 |
+
align-items: center;
|
| 345 |
+
justify-content: center;
|
| 346 |
+
gap: 8px;
|
| 347 |
+
transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.brain-btn {
|
| 351 |
+
width: 40px;
|
| 352 |
+
padding: 0;
|
| 353 |
+
display: grid;
|
| 354 |
+
place-items: center;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.brain-btn.active {
|
| 358 |
+
background: var(--accent);
|
| 359 |
+
color: #fff;
|
| 360 |
+
border-color: var(--accent);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.brain-btn svg {
|
| 364 |
+
width: 20px;
|
| 365 |
+
height: 20px;
|
| 366 |
+
fill: currentColor;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.mobile-menu-btn:hover,
|
| 370 |
+
.clear-btn:hover,
|
| 371 |
+
.brain-btn:hover,
|
| 372 |
+
.sidebar-toggle:hover,
|
| 373 |
+
.stop-btn:hover,
|
| 374 |
+
#send-btn:hover,
|
| 375 |
+
.mic-btn:hover {
|
| 376 |
+
transform: translateY(-1px);
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.clear-btn {
|
| 380 |
font-size: 12px;
|
| 381 |
+
color: var(--text-2);
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.voice-caption {
|
| 385 |
+
width: min(var(--max-chat), calc(100% - 32px));
|
| 386 |
+
margin: 14px auto 0;
|
| 387 |
+
min-height: 24px;
|
| 388 |
+
color: var(--text-2);
|
| 389 |
+
font-size: 12px;
|
| 390 |
+
line-height: 1.5;
|
| 391 |
+
letter-spacing: 0.01em;
|
| 392 |
+
padding: 0 4px;
|
| 393 |
+
transition: opacity 0.2s ease;
|
| 394 |
}
|
|
|
|
| 395 |
|
|
|
|
| 396 |
#chat-box {
|
| 397 |
+
width: min(var(--max-chat), calc(100% - 32px));
|
| 398 |
+
margin: 10px auto 0;
|
| 399 |
+
padding: 16px 0 22px;
|
| 400 |
flex: 1;
|
| 401 |
overflow-y: auto;
|
|
|
|
| 402 |
display: flex;
|
| 403 |
flex-direction: column;
|
| 404 |
gap: 12px;
|
| 405 |
scroll-behavior: smooth;
|
| 406 |
}
|
| 407 |
+
|
| 408 |
+
#chat-box::-webkit-scrollbar,
|
| 409 |
+
.sidebar::-webkit-scrollbar {
|
| 410 |
+
width: 8px;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
#chat-box::-webkit-scrollbar-track,
|
| 414 |
+
.sidebar::-webkit-scrollbar-track {
|
| 415 |
+
background: transparent;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
#chat-box::-webkit-scrollbar-thumb,
|
| 419 |
+
.sidebar::-webkit-scrollbar-thumb {
|
| 420 |
+
background: rgba(15, 23, 42, 0.15);
|
| 421 |
+
border-radius: 999px;
|
| 422 |
+
}
|
| 423 |
|
| 424 |
.message {
|
| 425 |
+
max-width: min(100%, 760px);
|
| 426 |
+
padding: 14px 16px;
|
| 427 |
+
border-radius: 18px;
|
| 428 |
line-height: 1.65;
|
| 429 |
+
font-size: 15px;
|
| 430 |
+
border: 1px solid var(--border);
|
| 431 |
+
background: var(--panel);
|
| 432 |
+
box-shadow: var(--shadow-soft);
|
| 433 |
+
animation: msg-in 0.22s ease-out;
|
| 434 |
+
backdrop-filter: blur(16px);
|
| 435 |
}
|
| 436 |
+
|
| 437 |
@keyframes msg-in {
|
| 438 |
+
from {
|
| 439 |
+
opacity: 0;
|
| 440 |
+
transform: translateY(8px);
|
| 441 |
+
}
|
| 442 |
+
to {
|
| 443 |
+
opacity: 1;
|
| 444 |
+
transform: translateY(0);
|
| 445 |
+
}
|
| 446 |
}
|
| 447 |
+
|
| 448 |
.message.user {
|
| 449 |
+
align-self: flex-end;
|
| 450 |
+
background: rgba(100, 181, 255, 0.08);
|
| 451 |
+
border-bottom-right-radius: 6px;
|
|
|
|
| 452 |
}
|
| 453 |
+
|
| 454 |
.message.ai {
|
| 455 |
+
align-self: flex-start;
|
| 456 |
+
background: rgba(255, 255, 255, 0.05);
|
| 457 |
+
border-bottom-left-radius: 6px;
|
| 458 |
}
|
| 459 |
+
|
| 460 |
.message.system {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
align-self: center;
|
| 462 |
+
max-width: 92%;
|
| 463 |
+
font-size: 12px;
|
| 464 |
+
color: var(--text-2);
|
| 465 |
+
background: rgba(255, 255, 255, 0.04);
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.message p,
|
| 469 |
+
.message ul,
|
| 470 |
+
.message ol {
|
| 471 |
+
margin: 0.5em 0;
|
| 472 |
}
|
| 473 |
+
|
|
|
|
|
|
|
| 474 |
.message code {
|
|
|
|
|
|
|
| 475 |
padding: 1px 6px;
|
| 476 |
+
border-radius: 6px;
|
| 477 |
+
background: rgba(15, 23, 42, 0.06);
|
| 478 |
font-family: 'JetBrains Mono', monospace;
|
| 479 |
font-size: 13px;
|
| 480 |
}
|
| 481 |
+
|
| 482 |
.message pre {
|
|
|
|
|
|
|
|
|
|
| 483 |
overflow-x: auto;
|
| 484 |
+
padding: 12px;
|
| 485 |
+
border-radius: 14px;
|
| 486 |
+
background: rgba(15, 23, 42, 0.05);
|
| 487 |
}
|
| 488 |
|
| 489 |
+
.message.thinking {
|
| 490 |
+
display: inline-flex;
|
| 491 |
+
align-items: center;
|
| 492 |
+
gap: 6px;
|
| 493 |
+
width: max-content;
|
| 494 |
+
padding: 12px 14px;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
.message.thinking .dot {
|
| 498 |
+
width: 7px;
|
| 499 |
+
height: 7px;
|
| 500 |
+
border-radius: 999px;
|
| 501 |
+
background: var(--accent-2);
|
| 502 |
+
opacity: 0.35;
|
| 503 |
+
animation: bob 1.1s infinite ease-in-out;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.message.thinking .dot:nth-child(2) {
|
| 507 |
+
animation-delay: 0.16s;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
.message.thinking .dot:nth-child(3) {
|
| 511 |
+
animation-delay: 0.32s;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
@keyframes bob {
|
| 515 |
+
0%,
|
| 516 |
+
80%,
|
| 517 |
+
100% {
|
| 518 |
+
transform: translateY(0);
|
| 519 |
+
opacity: 0.35;
|
| 520 |
+
}
|
| 521 |
+
40% {
|
| 522 |
+
transform: translateY(-5px);
|
| 523 |
+
opacity: 1;
|
| 524 |
+
}
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
/* ── Voice visualizer ─────────────────────────────────────────────────────── */
|
| 528 |
.voice-visualizer {
|
| 529 |
+
width: min(var(--max-chat), calc(100% - 32px));
|
| 530 |
+
margin: 0 auto;
|
| 531 |
+
height: 0;
|
| 532 |
+
overflow: hidden;
|
| 533 |
display: flex;
|
| 534 |
align-items: center;
|
| 535 |
justify-content: center;
|
| 536 |
gap: 4px;
|
| 537 |
+
transition: height 0.22s ease;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.voice-visualizer.active {
|
| 541 |
+
height: 52px;
|
| 542 |
}
|
| 543 |
+
|
| 544 |
.viz-bar {
|
| 545 |
width: 4px;
|
|
|
|
|
|
|
| 546 |
height: 6px;
|
| 547 |
+
border-radius: 999px;
|
| 548 |
+
background: linear-gradient(180deg, rgba(37, 99, 235, 0.95), rgba(15, 23, 42, 0.55));
|
| 549 |
transition: height 0.08s ease;
|
|
|
|
| 550 |
}
|
| 551 |
|
| 552 |
+
/* ── Controls ─────────────────────────────────────────────────────────────── */
|
| 553 |
.controls {
|
| 554 |
+
width: min(var(--max-chat), calc(100% - 32px));
|
| 555 |
+
margin: 12px auto 18px;
|
| 556 |
+
padding: 14px;
|
| 557 |
+
border: 1px solid var(--border);
|
| 558 |
+
border-radius: 24px;
|
| 559 |
+
background: rgba(8, 12, 20, 0.78);
|
| 560 |
+
backdrop-filter: blur(18px);
|
| 561 |
+
box-shadow: var(--shadow);
|
| 562 |
flex-shrink: 0;
|
| 563 |
}
|
| 564 |
+
|
| 565 |
.text-row {
|
| 566 |
display: flex;
|
| 567 |
+
align-items: flex-end;
|
| 568 |
gap: 10px;
|
| 569 |
+
margin-bottom: 10px;
|
|
|
|
| 570 |
}
|
| 571 |
|
|
|
|
| 572 |
#text-input {
|
| 573 |
flex: 1;
|
| 574 |
+
min-height: 46px;
|
| 575 |
+
max-height: 220px;
|
| 576 |
+
resize: none;
|
| 577 |
+
overflow-y: auto;
|
| 578 |
+
padding: 12px 14px;
|
| 579 |
+
border-radius: 16px;
|
| 580 |
+
background: rgba(255, 255, 255, 0.04);
|
| 581 |
color: var(--text);
|
| 582 |
+
line-height: 1.55;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
#text-input::placeholder {
|
| 586 |
+
color: var(--text-3);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
|
| 589 |
#send-btn {
|
| 590 |
+
width: 48px;
|
| 591 |
+
height: 46px;
|
| 592 |
+
border-radius: 16px;
|
| 593 |
+
background: linear-gradient(135deg, #64b5ff, #8b5cf6);
|
| 594 |
+
color: #05070d;
|
| 595 |
cursor: pointer;
|
| 596 |
+
display: inline-flex;
|
|
|
|
| 597 |
align-items: center;
|
| 598 |
+
justify-content: center;
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
.voice-row {
|
| 602 |
+
display: flex;
|
| 603 |
+
gap: 10px;
|
| 604 |
}
|
|
|
|
|
|
|
| 605 |
|
|
|
|
| 606 |
.mic-btn {
|
| 607 |
flex: 1;
|
| 608 |
+
min-height: 48px;
|
| 609 |
+
border-radius: 16px;
|
| 610 |
+
background: rgba(255, 255, 255, 0.04);
|
| 611 |
+
color: var(--text);
|
| 612 |
display: flex;
|
| 613 |
align-items: center;
|
| 614 |
justify-content: center;
|
| 615 |
+
gap: 10px;
|
| 616 |
+
cursor: pointer;
|
| 617 |
+
padding: 0 16px;
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
.mic-btn.mic-listening {
|
| 621 |
+
border-color: rgba(37, 99, 235, 0.35);
|
| 622 |
+
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.08);
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.mic-btn.mic-recording {
|
| 626 |
+
border-color: rgba(239, 68, 68, 0.35);
|
| 627 |
+
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.08);
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.mic-btn.mic-processing {
|
| 631 |
+
border-color: rgba(217, 119, 6, 0.35);
|
| 632 |
+
box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.08);
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
.mic-icon {
|
| 636 |
+
font-size: 18px;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
.stop-btn {
|
| 640 |
+
min-width: 90px;
|
| 641 |
+
height: 48px;
|
| 642 |
+
border-radius: 16px;
|
| 643 |
+
background: rgba(255, 255, 255, 0.04);
|
| 644 |
+
color: #ff8da1;
|
| 645 |
+
display: inline-flex;
|
| 646 |
+
align-items: center;
|
| 647 |
+
justify-content: center;
|
| 648 |
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
cursor: pointer;
|
| 650 |
+
padding: 0 14px;
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
/* ── Brain mode ───────────────────────────────────────────────────────────── */
|
| 654 |
+
.brain-stage {
|
| 655 |
+
display: none;
|
| 656 |
+
width: min(var(--max-chat), calc(100% - 32px));
|
| 657 |
+
margin: 16px auto 0;
|
| 658 |
+
flex: 1;
|
| 659 |
+
align-items: center;
|
| 660 |
+
justify-content: center;
|
| 661 |
+
position: relative;
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
.brain-shell {
|
| 665 |
position: relative;
|
| 666 |
+
width: min(760px, 100%);
|
| 667 |
+
height: min(62vh, 620px);
|
| 668 |
+
border-radius: 32px;
|
| 669 |
+
background:
|
| 670 |
+
radial-gradient(circle at 50% 45%, rgba(100, 181, 255, 0.18), transparent 18%),
|
| 671 |
+
radial-gradient(circle at 50% 55%, rgba(139, 92, 246, 0.12), transparent 38%),
|
| 672 |
+
radial-gradient(circle at 50% 50%, rgba(15, 23, 42, 0.92), rgba(5, 7, 13, 0.98) 72%);
|
| 673 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 674 |
+
box-shadow: var(--shadow);
|
| 675 |
overflow: hidden;
|
| 676 |
}
|
| 677 |
+
|
| 678 |
+
.brain-bubbles {
|
| 679 |
+
position: absolute;
|
| 680 |
+
inset: 0;
|
| 681 |
+
z-index: 3;
|
| 682 |
+
pointer-events: none;
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.brain-bubble {
|
| 686 |
+
position: absolute;
|
| 687 |
+
width: min(280px, 42vw);
|
| 688 |
+
max-width: 320px;
|
| 689 |
+
min-height: 96px;
|
| 690 |
+
padding: 14px 16px;
|
| 691 |
+
border-radius: 22px;
|
| 692 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 693 |
+
background: rgba(8, 12, 20, 0.82);
|
| 694 |
+
backdrop-filter: blur(18px);
|
| 695 |
+
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
|
| 696 |
+
transform: translateY(8px) scale(0.96);
|
| 697 |
+
opacity: 0.72;
|
| 698 |
+
transition: transform 0.25s ease, opacity 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
.brain-bubble::after {
|
| 702 |
content: '';
|
| 703 |
+
position: absolute;
|
| 704 |
+
inset: auto 18px -8px auto;
|
| 705 |
+
width: 16px;
|
| 706 |
+
height: 16px;
|
| 707 |
+
background: inherit;
|
| 708 |
+
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
| 709 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
| 710 |
+
transform: rotate(45deg);
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
.brain-bubble-label {
|
| 714 |
+
font-size: 10px;
|
| 715 |
+
letter-spacing: 0.14em;
|
| 716 |
+
text-transform: uppercase;
|
| 717 |
+
color: var(--text-3);
|
| 718 |
+
margin-bottom: 8px;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.brain-bubble-text {
|
| 722 |
+
font-size: 15px;
|
| 723 |
+
line-height: 1.55;
|
| 724 |
+
color: var(--text);
|
| 725 |
+
min-height: 40px;
|
| 726 |
+
white-space: pre-wrap;
|
| 727 |
+
word-break: break-word;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.brain-bubble-stt {
|
| 731 |
+
left: 24px;
|
| 732 |
+
top: 24px;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.brain-bubble-tts {
|
| 736 |
+
right: 24px;
|
| 737 |
+
bottom: 24px;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
.brain-bubble.active {
|
| 741 |
+
opacity: 1;
|
| 742 |
+
transform: translateY(0) scale(1);
|
| 743 |
+
border-color: rgba(100, 181, 255, 0.28);
|
| 744 |
+
box-shadow:
|
| 745 |
+
0 22px 62px rgba(0, 0, 0, 0.48),
|
| 746 |
+
0 0 0 1px rgba(100, 181, 255, 0.08);
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
.brain-bubble.active .brain-bubble-label {
|
| 750 |
+
color: rgba(100, 181, 255, 0.82);
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
.brain-bubble.speaking {
|
| 754 |
+
animation: bubble-breathe 1.4s ease-in-out infinite;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.brain-bubble.speaking .brain-bubble-text {
|
| 758 |
+
position: relative;
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
.brain-bubble.speaking .brain-bubble-text::after {
|
| 762 |
+
content: '';
|
| 763 |
+
display: inline-block;
|
| 764 |
+
width: 10px;
|
| 765 |
+
height: 10px;
|
| 766 |
+
margin-left: 8px;
|
| 767 |
+
border-radius: 999px;
|
| 768 |
+
background: linear-gradient(135deg, #64b5ff, #8b5cf6);
|
| 769 |
+
box-shadow: 0 0 0 0 rgba(100, 181, 255, 0.26);
|
| 770 |
+
animation: bubble-dot 1s ease-in-out infinite;
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
.brain-scan {
|
| 774 |
position: absolute;
|
| 775 |
inset: 0;
|
| 776 |
+
background:
|
| 777 |
+
linear-gradient(180deg, transparent 0%, rgba(100, 181, 255, 0.12) 50%, transparent 100%),
|
| 778 |
+
linear-gradient(90deg, transparent 0%, rgba(100, 181, 255, 0.22) 48%, rgba(139, 92, 246, 0.22) 52%, transparent 100%);
|
| 779 |
+
background-size: 100% 180px, 280px 100%;
|
| 780 |
+
background-repeat: repeat-y, no-repeat;
|
| 781 |
+
mix-blend-mode: screen;
|
| 782 |
+
opacity: 0.55;
|
| 783 |
+
animation: scan 6s linear infinite;
|
| 784 |
+
pointer-events: none;
|
| 785 |
}
|
| 786 |
+
|
| 787 |
+
.brain-shell::before,
|
| 788 |
+
.brain-shell::after {
|
| 789 |
+
content: '';
|
| 790 |
+
position: absolute;
|
| 791 |
+
inset: -20%;
|
| 792 |
+
background:
|
| 793 |
+
radial-gradient(circle at 30% 30%, rgba(100, 181, 255, 0.18), transparent 14%),
|
| 794 |
+
radial-gradient(circle at 70% 40%, rgba(139, 92, 246, 0.12), transparent 16%),
|
| 795 |
+
radial-gradient(circle at 55% 68%, rgba(22, 163, 74, 0.11), transparent 15%);
|
| 796 |
+
animation: drift 16s linear infinite;
|
| 797 |
+
pointer-events: none;
|
| 798 |
}
|
| 799 |
+
|
| 800 |
+
.brain-shell::after {
|
| 801 |
+
animation-direction: reverse;
|
| 802 |
+
opacity: 0.7;
|
| 803 |
}
|
| 804 |
+
|
| 805 |
+
.brain-pulse {
|
| 806 |
+
position: absolute;
|
| 807 |
+
inset: 50% auto auto 50%;
|
| 808 |
+
width: 16px;
|
| 809 |
+
height: 16px;
|
| 810 |
+
border-radius: 999px;
|
| 811 |
+
border: 1px solid rgba(100, 181, 255, 0.55);
|
| 812 |
+
box-shadow: 0 0 0 0 rgba(100, 181, 255, 0.22);
|
| 813 |
+
transform: translate(-50%, -50%);
|
| 814 |
+
animation: pulse-ring 3.6s linear infinite;
|
| 815 |
+
pointer-events: none;
|
| 816 |
}
|
| 817 |
+
|
| 818 |
+
.pulse-a {
|
| 819 |
+
width: 110px;
|
| 820 |
+
height: 110px;
|
| 821 |
}
|
|
|
|
|
|
|
| 822 |
|
| 823 |
+
.pulse-b {
|
| 824 |
+
width: 220px;
|
| 825 |
+
height: 220px;
|
| 826 |
+
animation-delay: 0.8s;
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
.pulse-c {
|
| 830 |
+
width: 340px;
|
| 831 |
+
height: 340px;
|
| 832 |
+
animation-delay: 1.6s;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
.brain-svg {
|
| 836 |
+
position: absolute;
|
| 837 |
+
inset: 0;
|
| 838 |
+
width: 100%;
|
| 839 |
+
height: 100%;
|
| 840 |
+
filter: drop-shadow(0 0 16px rgba(37, 99, 235, 0.08));
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.brain-outline {
|
| 844 |
+
fill: rgba(255, 255, 255, 0.04);
|
| 845 |
+
stroke: rgba(255, 255, 255, 0.12);
|
| 846 |
+
stroke-width: 2;
|
| 847 |
+
stroke-dasharray: 10 12;
|
| 848 |
+
animation: pulse-line 8s linear infinite;
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
.brain-wire {
|
| 852 |
+
fill: none;
|
| 853 |
+
stroke: url(#brainGlow);
|
| 854 |
+
stroke-width: 2.5;
|
| 855 |
+
stroke-linecap: round;
|
| 856 |
+
stroke-dasharray: 10 10;
|
| 857 |
+
animation: dash 7s linear infinite;
|
| 858 |
+
opacity: 0.82;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
.brain-node {
|
| 862 |
+
fill: rgba(255, 255, 255, 0.9);
|
| 863 |
+
stroke: rgba(100, 181, 255, 0.35);
|
| 864 |
+
stroke-width: 2;
|
| 865 |
+
filter: drop-shadow(0 0 12px rgba(100, 181, 255, 0.38));
|
| 866 |
+
animation: node-pulse 2.4s ease-in-out infinite;
|
| 867 |
+
transform-box: fill-box;
|
| 868 |
+
transform-origin: center;
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
.node-2,
|
| 872 |
+
.node-5,
|
| 873 |
+
.node-8,
|
| 874 |
+
.node-11 {
|
| 875 |
+
animation-delay: 0.2s;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.node-3,
|
| 879 |
+
.node-6,
|
| 880 |
+
.node-9,
|
| 881 |
+
.node-12 {
|
| 882 |
+
animation-delay: 0.4s;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
.brain-core {
|
| 886 |
+
fill: rgba(100, 181, 255, 0.18);
|
| 887 |
+
stroke: rgba(100, 181, 255, 0.65);
|
| 888 |
+
stroke-width: 2;
|
| 889 |
+
animation: core-breath 2.8s ease-in-out infinite;
|
| 890 |
+
transform-box: fill-box;
|
| 891 |
+
transform-origin: center;
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
.brain-core.ring {
|
| 895 |
+
fill: none;
|
| 896 |
+
stroke: rgba(255, 255, 255, 0.12);
|
| 897 |
+
stroke-dasharray: 8 12;
|
| 898 |
+
animation: dash 9s linear infinite;
|
| 899 |
+
}
|
| 900 |
+
|
| 901 |
+
@keyframes dash {
|
| 902 |
+
to {
|
| 903 |
+
stroke-dashoffset: -120;
|
| 904 |
+
}
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
@keyframes pulse-line {
|
| 908 |
+
0%,
|
| 909 |
+
100% {
|
| 910 |
+
opacity: 0.65;
|
| 911 |
+
}
|
| 912 |
+
50% {
|
| 913 |
+
opacity: 1;
|
| 914 |
+
}
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
@keyframes node-pulse {
|
| 918 |
+
0%,
|
| 919 |
+
100% {
|
| 920 |
+
transform: scale(1);
|
| 921 |
+
opacity: 0.84;
|
| 922 |
+
}
|
| 923 |
+
50% {
|
| 924 |
+
transform: scale(1.18);
|
| 925 |
+
opacity: 1;
|
| 926 |
+
}
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
@keyframes core-breath {
|
| 930 |
+
0%,
|
| 931 |
+
100% {
|
| 932 |
+
transform: scale(1);
|
| 933 |
+
opacity: 0.7;
|
| 934 |
+
}
|
| 935 |
+
50% {
|
| 936 |
+
transform: scale(1.08);
|
| 937 |
+
opacity: 1;
|
| 938 |
+
}
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
@keyframes drift {
|
| 942 |
+
0% {
|
| 943 |
+
transform: translate3d(0, 0, 0) scale(1);
|
| 944 |
+
}
|
| 945 |
+
50% {
|
| 946 |
+
transform: translate3d(3%, -2%, 0) scale(1.02);
|
| 947 |
+
}
|
| 948 |
+
100% {
|
| 949 |
+
transform: translate3d(0, 0, 0) scale(1);
|
| 950 |
+
}
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
@keyframes scan {
|
| 954 |
+
0% {
|
| 955 |
+
transform: translateY(-20%);
|
| 956 |
+
}
|
| 957 |
+
100% {
|
| 958 |
+
transform: translateY(20%);
|
| 959 |
+
}
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
@keyframes pulse-ring {
|
| 963 |
+
0% {
|
| 964 |
+
opacity: 0.15;
|
| 965 |
+
transform: translate(-50%, -50%) scale(0.75);
|
| 966 |
+
}
|
| 967 |
+
50% {
|
| 968 |
+
opacity: 0.75;
|
| 969 |
+
transform: translate(-50%, -50%) scale(1);
|
| 970 |
+
}
|
| 971 |
+
100% {
|
| 972 |
+
opacity: 0.12;
|
| 973 |
+
transform: translate(-50%, -50%) scale(1.28);
|
| 974 |
+
}
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
@keyframes bubble-breathe {
|
| 978 |
+
0%,
|
| 979 |
+
100% {
|
| 980 |
+
transform: translateY(0) scale(1);
|
| 981 |
+
}
|
| 982 |
+
50% {
|
| 983 |
+
transform: translateY(-2px) scale(1.015);
|
| 984 |
+
}
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
@keyframes bubble-dot {
|
| 988 |
+
0%,
|
| 989 |
+
100% {
|
| 990 |
+
transform: scale(0.75);
|
| 991 |
+
opacity: 0.35;
|
| 992 |
+
}
|
| 993 |
+
50% {
|
| 994 |
+
transform: scale(1);
|
| 995 |
+
opacity: 1;
|
| 996 |
+
}
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
body.brain-mode .sidebar,
|
| 1000 |
+
body.brain-mode #chat-box,
|
| 1001 |
+
body.brain-mode .voice-caption,
|
| 1002 |
+
body.brain-mode .voice-visualizer {
|
| 1003 |
+
display: none;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
body.brain-mode .brain-stage {
|
| 1007 |
display: flex;
|
|
|
|
|
|
|
|
|
|
| 1008 |
}
|
|
|
|
|
|
|
| 1009 |
|
| 1010 |
+
body.brain-mode .controls {
|
| 1011 |
+
display: block;
|
| 1012 |
+
position: fixed;
|
| 1013 |
+
left: 50%;
|
| 1014 |
+
bottom: 18px;
|
| 1015 |
+
transform: translateX(-50%);
|
| 1016 |
+
width: min(420px, calc(100% - 24px));
|
| 1017 |
+
margin: 0;
|
| 1018 |
+
padding: 12px;
|
| 1019 |
+
background: rgba(5, 7, 13, 0.78);
|
| 1020 |
+
border-color: rgba(255, 255, 255, 0.08);
|
| 1021 |
+
}
|
| 1022 |
+
|
| 1023 |
+
body.brain-mode .text-row {
|
| 1024 |
+
display: none;
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
+
body.brain-mode .voice-row {
|
| 1028 |
+
display: flex;
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
body.brain-mode .mic-btn,
|
| 1032 |
+
body.brain-mode .stop-btn {
|
| 1033 |
+
background: rgba(255, 255, 255, 0.05);
|
| 1034 |
+
}
|
| 1035 |
+
|
| 1036 |
+
body.brain-mode .topbar {
|
| 1037 |
+
background: rgba(5, 7, 13, 0.45);
|
| 1038 |
+
border-bottom-color: transparent;
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
body.brain-mode .topbar-center,
|
| 1042 |
+
body.brain-mode #state-label {
|
| 1043 |
+
opacity: 0;
|
| 1044 |
+
pointer-events: none;
|
| 1045 |
+
}
|
| 1046 |
|
| 1047 |
+
body.brain-mode .clear-btn {
|
| 1048 |
+
width: 40px;
|
| 1049 |
+
padding: 0;
|
| 1050 |
+
font-size: 0;
|
| 1051 |
+
color: transparent;
|
| 1052 |
+
background: rgba(255, 255, 255, 0.05);
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
body.brain-mode .clear-btn::before {
|
| 1056 |
+
content: '↺';
|
| 1057 |
+
font-size: 13px;
|
| 1058 |
+
color: var(--text);
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
body.brain-mode .brain-shell {
|
| 1062 |
+
box-shadow:
|
| 1063 |
+
0 30px 80px rgba(0, 0, 0, 0.5),
|
| 1064 |
+
inset 0 0 0 1px rgba(255, 255, 255, 0.03);
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
body.brain-mode .brain-pulse {
|
| 1068 |
+
border-color: rgba(100, 181, 255, 0.35);
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
body.brain-mode .brain-stage.searching .brain-wire {
|
| 1072 |
+
stroke-width: 3.2;
|
| 1073 |
+
opacity: 1;
|
| 1074 |
+
animation-duration: 2.2s;
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
body.brain-mode .brain-stage.searching .brain-core {
|
| 1078 |
+
animation-duration: 1.8s;
|
| 1079 |
+
filter: drop-shadow(0 0 18px rgba(100, 181, 255, 0.45));
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
body.brain-mode .brain-stage.searching .brain-node {
|
| 1083 |
+
animation-duration: 1.2s;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
body.brain-mode .brain-stage[data-state='listening'] .brain-shell {
|
| 1087 |
+
box-shadow:
|
| 1088 |
+
0 30px 80px rgba(0, 0, 0, 0.46),
|
| 1089 |
+
0 0 0 1px rgba(100, 181, 255, 0.08),
|
| 1090 |
+
0 0 34px rgba(100, 181, 255, 0.12);
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
body.brain-mode .brain-stage[data-state='processing'] .brain-shell::before {
|
| 1094 |
+
animation-duration: 8s;
|
| 1095 |
+
filter: blur(0.3px);
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
body.brain-mode .brain-stage[data-state='speaking'] .brain-core {
|
| 1099 |
+
fill: rgba(100, 181, 255, 0.26);
|
| 1100 |
+
animation-duration: 1.4s;
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
body.brain-mode .brain-stage[data-state='speaking'] .brain-node {
|
| 1104 |
+
filter: drop-shadow(0 0 14px rgba(100, 181, 255, 0.52));
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
/* ── Responsive ───────────────────────────────────────────────────────────── */
|
| 1108 |
+
@media (max-width: 960px) {
|
| 1109 |
.sidebar {
|
| 1110 |
position: fixed;
|
| 1111 |
+
inset: 0 auto 0 0;
|
| 1112 |
transform: translateX(-100%);
|
| 1113 |
+
z-index: 30;
|
| 1114 |
+
box-shadow: var(--shadow);
|
| 1115 |
+
transition: transform 0.2s ease;
|
| 1116 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1117 |
|
| 1118 |
+
.sidebar.mobile-open {
|
| 1119 |
+
transform: translateX(0);
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
.mobile-menu-btn {
|
| 1123 |
+
display: inline-flex;
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
.topbar-center {
|
| 1127 |
+
font-size: 13px;
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
.brain-shell {
|
| 1131 |
+
height: min(56vh, 520px);
|
| 1132 |
+
}
|
| 1133 |
}
|
| 1134 |
+
|
| 1135 |
+
@media (max-width: 720px) {
|
| 1136 |
+
.topbar {
|
| 1137 |
+
padding: 12px 12px;
|
| 1138 |
+
}
|
| 1139 |
+
|
| 1140 |
+
.topbar-state {
|
| 1141 |
+
padding: 7px 10px;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
.clear-btn {
|
| 1145 |
+
padding: 0 12px;
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
.voice-caption,
|
| 1149 |
+
#chat-box,
|
| 1150 |
+
.controls,
|
| 1151 |
+
.voice-visualizer,
|
| 1152 |
+
.brain-stage {
|
| 1153 |
+
width: calc(100% - 20px);
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
#chat-box {
|
| 1157 |
+
padding-top: 12px;
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
.message {
|
| 1161 |
+
font-size: 14px;
|
| 1162 |
+
padding: 12px 14px;
|
| 1163 |
+
}
|
| 1164 |
+
|
| 1165 |
+
.controls {
|
| 1166 |
+
padding: 12px;
|
| 1167 |
+
border-radius: 20px;
|
| 1168 |
+
}
|
| 1169 |
+
|
| 1170 |
+
.text-row {
|
| 1171 |
+
margin-bottom: 8px;
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
.voice-row {
|
| 1175 |
+
gap: 8px;
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
.brain-shell {
|
| 1179 |
+
height: 50vh;
|
| 1180 |
+
border-radius: 24px;
|
| 1181 |
+
}
|
| 1182 |
}
|
services/streaming.py
CHANGED
|
@@ -13,7 +13,8 @@ FIX-ISSUE4 (Natural, slow, small-chunk TTS):
|
|
| 13 |
• Hard limit of 40 chars ensures no chunk ever gets too large.
|
| 14 |
• Sentence-ending punctuation (।.!?) always flushes immediately
|
| 15 |
regardless of length, giving natural pause points.
|
| 16 |
-
• The TTS rate is
|
|
|
|
| 17 |
|
| 18 |
Result: audio arrives in small, fast, overlapping synthesis tasks,
|
| 19 |
giving a low-latency, smooth, natural speech feel.
|
|
|
|
| 13 |
• Hard limit of 40 chars ensures no chunk ever gets too large.
|
| 14 |
• Sentence-ending punctuation (।.!?) always flushes immediately
|
| 15 |
regardless of length, giving natural pause points.
|
| 16 |
+
• The TTS rate is slightly faster than neutral in tts.py for a more
|
| 17 |
+
conversational pace.
|
| 18 |
|
| 19 |
Result: audio arrives in small, fast, overlapping synthesis tasks,
|
| 20 |
giving a low-latency, smooth, natural speech feel.
|
services/tts.py
CHANGED
|
@@ -2,7 +2,8 @@
|
|
| 2 |
services/tts.py — Ultra Low-Latency Dual TTS Backend
|
| 3 |
|
| 4 |
FIX-ISSUE4 (Normal-speed TTS):
|
| 5 |
-
• Default rate
|
|
|
|
| 6 |
• split_sentences() now splits on ALL clause delimiters (commas, colons,
|
| 7 |
em-dashes) in addition to sentence endings, so synthesis tasks are
|
| 8 |
smaller and start sooner. This pairs with streaming.py's 2–3 word
|
|
@@ -20,6 +21,7 @@ EDGE_VOICE = "bn-BD-NabanitaNeural"
|
|
| 20 |
ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY", "")
|
| 21 |
ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM")
|
| 22 |
ELEVENLABS_MODEL_ID = os.getenv("ELEVENLABS_MODEL_ID", "eleven_multilingual_v2")
|
|
|
|
| 23 |
ELEVENLABS_OUTPUT_FORMAT = "mp3_22050_32"
|
| 24 |
ELEVENLABS_STABILITY = 0.45
|
| 25 |
ELEVENLABS_SIMILARITY = 0.80
|
|
@@ -40,7 +42,10 @@ if USE_ELEVENLABS and not ELEVENLABS_API_KEY:
|
|
| 40 |
if not EDGE_TTS_AVAILABLE and not ELEVENLABS_API_KEY:
|
| 41 |
raise RuntimeError("[TTS] Neither edge_tts nor ELEVENLABS_API_KEY is available")
|
| 42 |
|
| 43 |
-
print(
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def split_sentences(text: str) -> list[str]:
|
|
@@ -61,10 +66,10 @@ def split_sentences(text: str) -> list[str]:
|
|
| 61 |
return [p.strip() for p in parts if len(p.strip()) > 1]
|
| 62 |
|
| 63 |
|
| 64 |
-
async def _edge_tts_stream(text: str, voice: str = EDGE_VOICE, rate: str = "+
|
| 65 |
"""
|
| 66 |
Stream Edge-TTS audio for a single text chunk.
|
| 67 |
-
Default rate is
|
| 68 |
"""
|
| 69 |
if edge_tts is None:
|
| 70 |
raise RuntimeError("edge_tts is not installed")
|
|
@@ -86,6 +91,7 @@ async def _elevenlabs_stream(
|
|
| 86 |
voice_id: str = ELEVENLABS_VOICE_ID,
|
| 87 |
model_id: str = ELEVENLABS_MODEL_ID,
|
| 88 |
output_format: str = ELEVENLABS_OUTPUT_FORMAT,
|
|
|
|
| 89 |
stability: float = ELEVENLABS_STABILITY,
|
| 90 |
similarity: float = ELEVENLABS_SIMILARITY,
|
| 91 |
style: float = ELEVENLABS_STYLE,
|
|
@@ -109,6 +115,7 @@ async def _elevenlabs_stream(
|
|
| 109 |
"similarity_boost": similarity,
|
| 110 |
"style": style,
|
| 111 |
"use_speaker_boost": speaker_boost,
|
|
|
|
| 112 |
},
|
| 113 |
}
|
| 114 |
try:
|
|
@@ -132,7 +139,7 @@ async def _elevenlabs_stream(
|
|
| 132 |
async def text_to_speech_stream(
|
| 133 |
text: str,
|
| 134 |
voice: str | None = None,
|
| 135 |
-
rate: str = "+
|
| 136 |
):
|
| 137 |
"""
|
| 138 |
Stream TTS audio for `text`.
|
|
|
|
| 2 |
services/tts.py — Ultra Low-Latency Dual TTS Backend
|
| 3 |
|
| 4 |
FIX-ISSUE4 (Normal-speed TTS):
|
| 5 |
+
• Default rate is now slightly faster than normal for a more natural
|
| 6 |
+
conversational pace.
|
| 7 |
• split_sentences() now splits on ALL clause delimiters (commas, colons,
|
| 8 |
em-dashes) in addition to sentence endings, so synthesis tasks are
|
| 9 |
smaller and start sooner. This pairs with streaming.py's 2–3 word
|
|
|
|
| 21 |
ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY", "")
|
| 22 |
ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM")
|
| 23 |
ELEVENLABS_MODEL_ID = os.getenv("ELEVENLABS_MODEL_ID", "eleven_multilingual_v2")
|
| 24 |
+
ELEVENLABS_SPEED = float(os.getenv("ELEVENLABS_SPEED", "1.08"))
|
| 25 |
ELEVENLABS_OUTPUT_FORMAT = "mp3_22050_32"
|
| 26 |
ELEVENLABS_STABILITY = 0.45
|
| 27 |
ELEVENLABS_SIMILARITY = 0.80
|
|
|
|
| 42 |
if not EDGE_TTS_AVAILABLE and not ELEVENLABS_API_KEY:
|
| 43 |
raise RuntimeError("[TTS] Neither edge_tts nor ELEVENLABS_API_KEY is available")
|
| 44 |
|
| 45 |
+
print(
|
| 46 |
+
f"[TTS] Backend: {'ElevenLabs' if USE_ELEVENLABS else 'Edge-TTS'} | "
|
| 47 |
+
f"edge rate: +8% | eleven speed: {ELEVENLABS_SPEED:.2f}"
|
| 48 |
+
)
|
| 49 |
|
| 50 |
|
| 51 |
def split_sentences(text: str) -> list[str]:
|
|
|
|
| 66 |
return [p.strip() for p in parts if len(p.strip()) > 1]
|
| 67 |
|
| 68 |
|
| 69 |
+
async def _edge_tts_stream(text: str, voice: str = EDGE_VOICE, rate: str = "+8%"):
|
| 70 |
"""
|
| 71 |
Stream Edge-TTS audio for a single text chunk.
|
| 72 |
+
Default rate is slightly faster than normal.
|
| 73 |
"""
|
| 74 |
if edge_tts is None:
|
| 75 |
raise RuntimeError("edge_tts is not installed")
|
|
|
|
| 91 |
voice_id: str = ELEVENLABS_VOICE_ID,
|
| 92 |
model_id: str = ELEVENLABS_MODEL_ID,
|
| 93 |
output_format: str = ELEVENLABS_OUTPUT_FORMAT,
|
| 94 |
+
speed: float = ELEVENLABS_SPEED,
|
| 95 |
stability: float = ELEVENLABS_STABILITY,
|
| 96 |
similarity: float = ELEVENLABS_SIMILARITY,
|
| 97 |
style: float = ELEVENLABS_STYLE,
|
|
|
|
| 115 |
"similarity_boost": similarity,
|
| 116 |
"style": style,
|
| 117 |
"use_speaker_boost": speaker_boost,
|
| 118 |
+
"speed": speed,
|
| 119 |
},
|
| 120 |
}
|
| 121 |
try:
|
|
|
|
| 139 |
async def text_to_speech_stream(
|
| 140 |
text: str,
|
| 141 |
voice: str | None = None,
|
| 142 |
+
rate: str = "+8%",
|
| 143 |
):
|
| 144 |
"""
|
| 145 |
Stream TTS audio for `text`.
|