RAM2106 commited on
Commit
cd5198d
·
0 Parent(s):

Initial VibeVoice Studio commit

Browse files
.gitignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node packages
2
+ node_modules/
3
+ frontend/node_modules/
4
+ frontend/dist/
5
+
6
+ # Python caching
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ .venv/
11
+ env/
12
+ venv/
13
+ ENV/
14
+
15
+ # OS files
16
+ .DS_Store
17
+ Thumbs.db
18
+
19
+ # Audio artifacts and temporary caches
20
+ static/temp/*
21
+ static/cloned_voices/*
22
+ !static/temp/.gitkeep
23
+ !static/cloned_voices/.gitkeep
24
+ combined_test.wav
25
+
26
+ # Local environment / API keys
27
+ .env
28
+ .gemini/
29
+ .git/
Dockerfile ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==========================================
2
+ # Stage 1: Build the React + Vite Frontend
3
+ # ==========================================
4
+ FROM node:20-slim AS frontend-builder
5
+ WORKDIR /app/frontend
6
+
7
+ # Copy frontend source files
8
+ COPY frontend/package*.json ./
9
+ RUN npm ci
10
+
11
+ COPY frontend/ ./
12
+ RUN npm run build
13
+
14
+ # ==========================================
15
+ # Stage 2: Build the FastAPI + PyTorch Backend
16
+ # ==========================================
17
+ FROM python:3.10-slim
18
+
19
+ # Install system dependencies (libsndfile1 is required by soundfile, espeak for pyttsx3 Linux speech)
20
+ RUN apt-get update && apt-get install -y --no-install-recommends \
21
+ libsndfile1 \
22
+ espeak \
23
+ ffmpeg \
24
+ && rm -rf /var/lib/apt/lists/*
25
+
26
+ WORKDIR /app
27
+
28
+ # Create a non-root user with UID 1000 (Hugging Face Spaces requirement)
29
+ RUN useradd -m -u 1000 user
30
+ ENV HOME=/home/user \
31
+ PATH=/home/user/.local/bin:$PATH \
32
+ PYTHONUNBUFFERED=1
33
+
34
+ # Pre-create writable directories for audio caches and temp audio files
35
+ RUN mkdir -p /app/static/temp && \
36
+ mkdir -p /home/user/.cache/huggingface && \
37
+ chown -R user:user /app /home/user
38
+
39
+ # Switch to the non-root user
40
+ USER user
41
+
42
+ # Install CPU-only PyTorch first to keep the container extremely lightweight (~1.5GB instead of 5GB)
43
+ RUN pip3 install --no-cache-dir --user torch torchaudio --index-url https://download.pytorch.org/whl/cpu
44
+
45
+ # Copy backend requirements and install dependencies
46
+ COPY --chown=user:user requirements.txt .
47
+ RUN pip3 install --no-cache-dir --user -r requirements.txt
48
+
49
+ # Copy FastAPI backend code and assets
50
+ COPY --chown=user:user app.py .
51
+ COPY --chown=user:user static/ static/
52
+
53
+ # Copy the built React static bundles from Stage 1
54
+ COPY --chown=user:user --from=frontend-builder /app/frontend/dist ./frontend/dist
55
+
56
+ # Expose Hugging Face Space port
57
+ EXPOSE 7860
58
+ ENV PORT=7860
59
+
60
+ # Start the FastAPI server dynamically binding to port 7860
61
+ CMD ["python3", "app.py"]
app.py ADDED
@@ -0,0 +1,908 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import math
4
+ import time
5
+ import asyncio
6
+ import threading
7
+ import traceback
8
+ import numpy as np
9
+ import soundfile as sf
10
+ import re
11
+ import pyttsx3
12
+ try:
13
+ import pythoncom
14
+ except ImportError:
15
+ pythoncom = None
16
+ import urllib.request
17
+ import hashlib
18
+ from typing import List, Optional
19
+
20
+ import torch
21
+
22
+ def parse_script_dialogue(script_text: str):
23
+ """
24
+ Parses script dialogue like:
25
+ Speaker 0: Hello there!
26
+ Speaker 1: Hi Sarah.
27
+ """
28
+ lines = []
29
+ current_speaker = 0
30
+
31
+ pattern = re.compile(r'^(Speaker\s*(\d+)|([A-Za-z0-9_\s]+))\s*:\s*(.*)$', re.IGNORECASE)
32
+
33
+ for line in script_text.strip().split('\n'):
34
+ line = line.strip()
35
+ if not line:
36
+ continue
37
+
38
+ match = pattern.match(line)
39
+ if match:
40
+ if match.group(2) is not None:
41
+ speaker_id = int(match.group(2))
42
+ else:
43
+ name = match.group(3).strip().lower()
44
+ if 'sarah' in name or 'dialogue' in name or 'female' in name or 'partner' in name:
45
+ speaker_id = 1
46
+ else:
47
+ speaker_id = 0
48
+ text = match.group(4).strip()
49
+ lines.append((speaker_id, text))
50
+ current_speaker = speaker_id
51
+ else:
52
+ lines.append((current_speaker, line))
53
+
54
+ return lines
55
+
56
+ def sapi5_generate_wav(text: str, speaker_id: int, output_path: str):
57
+ """
58
+ Generate high-fidelity speech WAV file cross-platform.
59
+ Uses Microsoft SAPI5 natively on Windows, and eSpeak fallback on Linux/macOS.
60
+ """
61
+ use_com = (os.name == 'nt' and pythoncom is not None)
62
+ if use_com:
63
+ try:
64
+ pythoncom.CoInitialize()
65
+ except Exception as e:
66
+ print(f"[Speech-Engine] Warning: CoInitialize failed: {e}")
67
+ use_com = False
68
+
69
+ try:
70
+ engine = pyttsx3.init()
71
+ voices = engine.getProperty('voices')
72
+ if len(voices) > 0:
73
+ voice_index = speaker_id % len(voices)
74
+ engine.setProperty('voice', voices[voice_index].id)
75
+
76
+ engine.setProperty('rate', 155) # Natural speech speed
77
+ engine.save_to_file(text, output_path)
78
+ engine.runAndWait()
79
+ time.sleep(0.1) # Let the file output settle
80
+ except Exception as e:
81
+ print(f"[Speech-Engine] Warning: pyttsx3 generation failed ({e}). Generating robust silent fallback.")
82
+ # If pyttsx3 fails in a headless cloud container, write a safe silent WAV so the app keeps running
83
+ sr = 24000
84
+ duration = max(1.0, len(text.split()) * 0.35)
85
+ dummy = np.zeros(int(sr * duration))
86
+ sf.write(output_path, dummy, sr)
87
+ finally:
88
+ if use_com:
89
+ try:
90
+ pythoncom.CoUninitialize()
91
+ except:
92
+ pass
93
+
94
+ def estimate_pitch_autocorrelation(audio_data, sr):
95
+ """
96
+ Estimate fundamental frequency F0 of mono audio data using a robust autocorrelation method.
97
+ Applies a low-pass filter to eliminate high-frequency noise/sibilants and restricts
98
+ the pitch search strictly to human conversational speech ranges (75Hz to 260Hz).
99
+ """
100
+ max_samples = min(len(audio_data), sr * 5)
101
+ signal = audio_data[:max_samples]
102
+
103
+ # 1. Apply a rolling average low-pass filter to smooth out high-frequency noise
104
+ # (Window of 5 samples acts as a fast, zero-dependency low-pass filter at 24kHz)
105
+ if len(signal) > 5:
106
+ signal = np.convolve(signal, np.ones(5)/5.0, mode='same')
107
+
108
+ signal = signal - np.mean(signal)
109
+
110
+ # 2. Restrict human speech F0 lag parameters:
111
+ # Deep male base (75Hz) to high female base (260Hz)
112
+ min_lag = int(sr / 260)
113
+ max_lag = int(sr / 75)
114
+
115
+ corr = np.correlate(signal, signal, mode='full')
116
+ corr = corr[len(corr)//2:]
117
+
118
+ if len(corr) > max_lag:
119
+ # Find peak lag within conversational bounds
120
+ peak_lag = np.argmax(corr[min_lag:max_lag]) + min_lag
121
+ f0 = sr / peak_lag
122
+
123
+ # Guard against zero lag or out of bound values
124
+ if 70.0 <= f0 <= 280.0:
125
+ return f0
126
+
127
+ return 150.0 # Safe conversational default pitch
128
+
129
+ def phase_vocoder_pitch_shift(audio, sr, shift_factor):
130
+ """
131
+ Shifts the pitch of an audio array by shift_factor using a phase vocoder
132
+ to keep duration/speed completely unchanged and avoid chipmunk artifacts.
133
+ """
134
+ if abs(shift_factor - 1.0) < 0.05:
135
+ return audio
136
+
137
+ n_fft = 1024
138
+ hop_length = 256
139
+ pad_len = n_fft
140
+ padded_audio = np.pad(audio, pad_len, mode='reflect')
141
+ window = np.hanning(n_fft)
142
+
143
+ frames = []
144
+ for i in range(0, len(audio) + pad_len, hop_length):
145
+ frame = padded_audio[i : i + n_fft]
146
+ if len(frame) < n_fft:
147
+ frame = np.pad(frame, (0, n_fft - len(frame)), mode='constant')
148
+ frames.append(frame * window)
149
+
150
+ frames = np.array(frames)
151
+ stft = np.fft.rfft(frames, axis=-1)
152
+
153
+ num_frames = len(stft)
154
+ new_num_frames = int(num_frames / shift_factor)
155
+
156
+ time_steps = np.linspace(0, num_frames - 1, new_num_frames)
157
+ new_stft = np.zeros((new_num_frames, stft.shape[1]), dtype=np.complex64)
158
+
159
+ phase_acc = np.angle(stft[0])
160
+ new_stft[0] = stft[0]
161
+ omega = 2 * np.pi * hop_length * np.arange(stft.shape[1]) / n_fft
162
+
163
+ for i in range(1, new_num_frames):
164
+ t = time_steps[i]
165
+ t_floor = int(np.floor(t))
166
+ t_ceil = min(t_floor + 1, num_frames - 1)
167
+ alpha = t - t_floor
168
+
169
+ mag = (1 - alpha) * np.abs(stft[t_floor]) + alpha * np.abs(stft[t_ceil])
170
+ phase_diff = np.angle(stft[t_ceil]) - np.angle(stft[t_floor])
171
+ phase_diff_corrected = phase_diff - omega
172
+ phase_diff_corrected = np.mod(phase_diff_corrected + np.pi, 2 * np.pi) - np.pi
173
+ phase_acc += omega + phase_diff_corrected
174
+ new_stft[i] = mag * np.exp(1j * phase_acc)
175
+
176
+ stretched_len = (new_num_frames - 1) * hop_length + n_fft
177
+ stretched_audio = np.zeros(stretched_len)
178
+ window_sum = np.zeros(stretched_len)
179
+
180
+ for i in range(new_num_frames):
181
+ frame = np.fft.irfft(new_stft[i])
182
+ idx = i * hop_length
183
+ if idx + n_fft <= stretched_len:
184
+ stretched_audio[idx : idx + n_fft] += frame * window
185
+ window_sum[idx : idx + n_fft] += window ** 2
186
+
187
+ window_sum[window_sum < 1e-4] = 1.0
188
+ stretched_audio /= window_sum
189
+
190
+ trim_start = n_fft // 2
191
+ trim_end = len(stretched_audio) - (n_fft // 2)
192
+ if trim_start < trim_end:
193
+ stretched_audio = stretched_audio[trim_start : trim_end]
194
+
195
+ target_len = len(audio)
196
+ resampled_audio = np.interp(
197
+ np.linspace(0, len(stretched_audio) - 1, target_len),
198
+ np.arange(len(stretched_audio)),
199
+ stretched_audio
200
+ )
201
+
202
+ return resampled_audio
203
+
204
+ def clone_voice_dsp(sapi5_wav_path: str, user_voice_path: str, output_path: str):
205
+ """
206
+ Analyzes the user's uploaded voice profile, extracts its characteristic pitch (F0),
207
+ and modulates/pitch-shifts the high-fidelity SAPI5 speech audio to clone the user's voice pitch.
208
+ """
209
+ try:
210
+ if not os.path.exists(user_voice_path) or os.path.getsize(user_voice_path) < 100:
211
+ return False
212
+
213
+ user_audio, user_sr = sf.read(user_voice_path)
214
+ if len(user_audio.shape) > 1:
215
+ user_audio = user_audio.mean(axis=1)
216
+ user_f0 = estimate_pitch_autocorrelation(user_audio, user_sr)
217
+
218
+ sapi5_audio, sapi5_sr = sf.read(sapi5_wav_path)
219
+ if len(sapi5_audio.shape) > 1:
220
+ sapi5_audio = sapi5_audio.mean(axis=1)
221
+ sapi5_f0 = estimate_pitch_autocorrelation(sapi5_audio, sapi5_sr)
222
+
223
+ print(f"[VoiceCloning-DSP] User pitch F0: {user_f0:.1f}Hz | SAPI5 pitch F0: {sapi5_f0:.1f}Hz")
224
+
225
+ # Calculate optimal pitch shift
226
+ shift_factor = user_f0 / sapi5_f0
227
+
228
+ # Cap the shift factor tightly to human conversational bounds
229
+ # (This prevents squeaky robotic cat formant warping!)
230
+ shift_factor = max(0.80, min(shift_factor, 1.35))
231
+
232
+ print(f"[VoiceCloning-DSP] Modulating synthesis pitch by shift factor: {shift_factor:.3f}")
233
+ cloned_audio = phase_vocoder_pitch_shift(sapi5_audio, sapi5_sr, shift_factor)
234
+
235
+ max_val = np.max(np.abs(cloned_audio))
236
+ if max_val > 0:
237
+ cloned_audio = cloned_audio / max_val * 0.8
238
+
239
+ sf.write(output_path, cloned_audio, sapi5_sr)
240
+ return True
241
+ except Exception as ex:
242
+ print(f"[VoiceCloning-DSP] Error during cloning: {ex}")
243
+ traceback.print_exc()
244
+ return False
245
+
246
+ def download_remote_voice(url: str) -> str:
247
+ """Helper to download Supabase/HTTP voice urls to a temp local file."""
248
+ if not url or not url.startswith("http"):
249
+ return url
250
+
251
+ url_hash = hashlib.md5(url.encode()).hexdigest()
252
+ local_path = os.path.join("static/temp", f"dl_{url_hash}.wav")
253
+
254
+ if os.path.exists(local_path):
255
+ return local_path
256
+
257
+ try:
258
+ print(f"[VibeVoice] Downloading remote voice profile from {url}...")
259
+ urllib.request.urlretrieve(url, local_path)
260
+ return local_path
261
+ except Exception as e:
262
+ print(f"[VibeVoice] Error downloading remote voice: {e}")
263
+ return url
264
+
265
+ def get_speaker_voices_list(text_script: str, voice_sample_path: Optional[str], speaker_voices: dict):
266
+ """
267
+ Parses speaker IDs from the text script and maps each speaker index to its respective downloaded voice sample path.
268
+ Falls back to the primary voice sample if no speaker-specific profile is uploaded.
269
+ """
270
+ dialogue_lines = parse_script_dialogue(text_script)
271
+ present_speakers = list(set(spk_id for spk_id, _ in dialogue_lines))
272
+ if not present_speakers:
273
+ present_speakers = [0]
274
+ max_spk = max(present_speakers)
275
+
276
+ voice_samples_list = []
277
+ for spk_idx in range(max_spk + 1):
278
+ path = speaker_voices.get(str(spk_idx)) or speaker_voices.get(spk_idx)
279
+ if not path or not os.path.exists(path):
280
+ path = voice_sample_path
281
+ if not path or not os.path.exists(path):
282
+ # Safe boundary fallback using existing audio
283
+ path = "combined_test.wav"
284
+ voice_samples_list.append(path)
285
+ return voice_samples_list
286
+
287
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, Form, HTTPException
288
+ from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
289
+ from fastapi.staticfiles import StaticFiles
290
+ from fastapi.middleware.cors import CORSMiddleware
291
+ from pydantic import BaseModel
292
+
293
+ # Initialize FastAPI App
294
+ app = FastAPI(
295
+ title="VibeVoice Agent Platform",
296
+ description="Full-stack real-time voice synthesis and agent cloning interface utilizing Microsoft VibeVoice.",
297
+ version="1.0.0"
298
+ )
299
+
300
+ # Enable CORS for direct local frontend connections
301
+ app.add_middleware(
302
+ CORSMiddleware,
303
+ allow_origins=["*"],
304
+ allow_credentials=True,
305
+ allow_methods=["*"],
306
+ allow_headers=["*"],
307
+ )
308
+
309
+
310
+ # Global Configuration
311
+ USE_REAL_MODEL = True # Set to True to load actual VibeVoice weights from Hugging Face
312
+ MODEL_ID = "microsoft/VibeVoice-Realtime-0.5B"
313
+ SAMPLING_RATE = 24000 # VibeVoice standard sampling rate
314
+
315
+ # Global references for the real VibeVoice model
316
+ processor = None
317
+ model = None
318
+ model_loading = False
319
+ model_error = None
320
+
321
+ def get_model():
322
+ """
323
+ Load the real VibeVoice models in a thread-safe way.
324
+ Uses CPU optimization and float32 since CUDA is not available.
325
+ """
326
+ global processor, model, model_loading, model_error
327
+ if USE_REAL_MODEL and model is None and not model_loading:
328
+ model_loading = True
329
+ try:
330
+ # Set optimal thread count to prevent resource contention and logical core thrashing
331
+ torch.set_num_threads(4)
332
+
333
+ print(f"[VibeVoice] Loading model '{MODEL_ID}' on CPU... This may take a while on first run.", flush=True)
334
+ from vibevoice.processor.vibevoice_processor import VibeVoiceProcessor
335
+ from vibevoice.modular.modeling_vibevoice_inference import VibeVoiceForConditionalGenerationInference
336
+
337
+ processor = VibeVoiceProcessor.from_pretrained(MODEL_ID)
338
+ model = VibeVoiceForConditionalGenerationInference.from_pretrained(
339
+ MODEL_ID,
340
+ torch_dtype=torch.float32,
341
+ device_map="cpu",
342
+ attn_implementation="eager"
343
+ )
344
+ print("[VibeVoice] Model loaded successfully!", flush=True)
345
+
346
+ # Initialize speech scaling and bias buffers if they are NaN
347
+ if torch.isnan(model.model.speech_scaling_factor) or torch.isnan(model.model.speech_bias_factor):
348
+ model.model.speech_scaling_factor.copy_(torch.tensor(1.0))
349
+ model.model.speech_bias_factor.copy_(torch.tensor(0.0))
350
+ print("[VibeVoice] Initialized scaling factor buffers to default 1.0 and 0.0.", flush=True)
351
+
352
+ model_loading = False
353
+ model_error = None
354
+ except Exception as e:
355
+ model_error = str(e)
356
+ model_loading = False
357
+ print(f"[VibeVoice] ERROR loading model: {e}", file=sys.stderr, flush=True)
358
+ traceback.print_exc()
359
+ return processor, model
360
+
361
+ # Create static directory and temp outputs folder if they don't exist
362
+ os.makedirs("static", exist_ok=True)
363
+ os.makedirs("static/temp", exist_ok=True)
364
+ os.makedirs("static/cloned_voices", exist_ok=True)
365
+
366
+ class GenerateRequest(BaseModel):
367
+ text: str
368
+ voice_sample_path: Optional[str] = None
369
+ speaker_id: Optional[int] = 0
370
+ speaker_voices: Optional[dict] = None
371
+
372
+
373
+ @app.get("/api/status")
374
+ async def get_status():
375
+ """Returns the current state of the VibeVoice model loading."""
376
+ global model, model_loading, model_error
377
+
378
+ # Trigger load in background if setting is active
379
+ if USE_REAL_MODEL and model is None and not model_loading:
380
+ threading.Thread(target=get_model, daemon=True).start()
381
+
382
+ return {
383
+ "use_real_model": USE_REAL_MODEL,
384
+ "model_id": MODEL_ID,
385
+ "loaded": model is not None,
386
+ "loading": model_loading,
387
+ "error": model_error,
388
+ "device": "CPU"
389
+ }
390
+
391
+ @app.post("/api/toggle-model")
392
+ async def toggle_model(enable: bool = True):
393
+ """Force enable real AI model execution as requested by the user."""
394
+ global USE_REAL_MODEL
395
+ USE_REAL_MODEL = True
396
+ threading.Thread(target=get_model, daemon=True).start()
397
+ return {
398
+ "status": "success",
399
+ "use_real_model": True,
400
+ "message": "Real VibeVoice model mode is locked to ENABLED."
401
+ }
402
+
403
+ @app.post("/api/upload-voice")
404
+ async def upload_voice(file: UploadFile = File(...), speaker_name: str = Form("Cloned Voice")):
405
+ """Uploads a voice reference file (WAV/MP3) for zero-shot speaker cloning."""
406
+ try:
407
+ filename = f"cloned_{int(time.time())}_{file.filename}"
408
+ save_path = os.path.join("static/cloned_voices", filename)
409
+
410
+ with open(save_path, "wb") as buffer:
411
+ content = await file.read()
412
+ buffer.write(content)
413
+
414
+ return {
415
+ "status": "success",
416
+ "voice_path": save_path,
417
+ "filename": filename,
418
+ "speaker_name": speaker_name,
419
+ "message": "Voice profile uploaded and prepared successfully!"
420
+ }
421
+ except Exception as e:
422
+ raise HTTPException(status_code=500, detail=f"Failed to upload voice: {str(e)}")
423
+
424
+ @app.delete("/api/delete-voice/{filename}")
425
+ async def delete_voice(filename: str):
426
+ """Deletes a voice reference file from local server storage."""
427
+ try:
428
+ # Prevent directory traversal attacks
429
+ safe_name = os.path.basename(filename)
430
+ save_path = os.path.join("static/cloned_voices", safe_name)
431
+ if os.path.exists(save_path):
432
+ os.remove(save_path)
433
+ return {
434
+ "status": "success",
435
+ "message": f"Successfully deleted voice profile file '{safe_name}'"
436
+ }
437
+ return {
438
+ "status": "error",
439
+ "message": f"File '{safe_name}' not found."
440
+ }
441
+ except Exception as e:
442
+ raise HTTPException(status_code=500, detail=f"Failed to delete voice: {str(e)}")
443
+
444
+ @app.post("/api/generate")
445
+ async def generate_speech(req: GenerateRequest):
446
+ """
447
+ Generates a full audio script using VibeVoice.
448
+ Supports multi-speaker formatting (Speaker 0: Text, Speaker 1: Text).
449
+ """
450
+ try:
451
+ # Standardize script format
452
+ text_script = req.text
453
+ if ":" not in text_script and "Speaker" not in text_script:
454
+ text_script = f"Speaker {req.speaker_id}: {text_script}"
455
+
456
+ # Resolve remote URLs to local paths if needed
457
+ voice_sample_path = download_remote_voice(req.voice_sample_path)
458
+ speaker_id = req.speaker_id
459
+
460
+ speaker_voices = req.speaker_voices or {}
461
+ for k, v in speaker_voices.items():
462
+ speaker_voices[k] = download_remote_voice(v)
463
+
464
+ output_filename = f"output_{int(time.time())}.wav"
465
+ output_path = os.path.join("static/temp", output_filename)
466
+
467
+ # Real AI Model Inference Path
468
+ if USE_REAL_MODEL:
469
+ proc, loaded_model = get_model()
470
+ if loaded_model is None:
471
+ raise HTTPException(
472
+ status_code=503,
473
+ detail="VibeVoice AI Model is currently loading or failed to load. Please try again or switch to Demo Mode."
474
+ )
475
+
476
+ # Load reference voices for all active speakers in order
477
+ voice_samples = get_speaker_voices_list(text_script, voice_sample_path, speaker_voices)
478
+
479
+ inputs = proc(text=text_script, voice_samples=voice_samples, return_tensors="pt")
480
+
481
+ print(f"[VibeVoice] Generating speech script: {text_script}")
482
+ with torch.no_grad():
483
+ outputs = loaded_model.generate(
484
+ **inputs,
485
+ tokenizer=proc.tokenizer,
486
+ max_new_tokens=1000,
487
+ do_sample=True,
488
+ cfg_scale=1.0
489
+ )
490
+
491
+ # Save generated audio
492
+ if outputs.speech_outputs and outputs.speech_outputs[0] is not None:
493
+ audio_tensor = outputs.speech_outputs[0]
494
+ proc.save_audio(audio_tensor, output_path=output_path, sampling_rate=SAMPLING_RATE)
495
+ else:
496
+ raise Exception("Generation succeeded but no audio output was generated.")
497
+
498
+ # High-Fidelity Demo/Simulated Mode Path (Runs instantly on CPU without GPU)
499
+ else:
500
+ print(f"[Demo] Compiling high-fidelity speech script: {text_script}")
501
+ try:
502
+ # 1. Parse dialogue into lines and speaker ids
503
+ dialogue_lines = parse_script_dialogue(text_script)
504
+
505
+ temp_files = []
506
+ concatenated_audio = []
507
+ last_sr = SAMPLING_RATE
508
+
509
+ # 2. Synthesize each line using local SAPI5 voices
510
+ for i, (spk_id, line_text) in enumerate(dialogue_lines):
511
+ # Check if custom voice profile path exists for this speaker ID
512
+ custom_voice = None
513
+ if speaker_voices:
514
+ custom_voice = speaker_voices.get(str(spk_id)) or speaker_voices.get(spk_id)
515
+ if not custom_voice and spk_id == 0:
516
+ custom_voice = voice_sample_path
517
+
518
+ temp_file = os.path.join("static/temp", f"temp_dialog_part_{int(time.time())}_{i}.wav")
519
+ sapi5_generate_wav(line_text, spk_id, temp_file)
520
+
521
+ # Apply DSP Voice Cloning if a custom voice sample is present
522
+ if custom_voice and os.path.exists(custom_voice) and os.path.getsize(custom_voice) > 100:
523
+ print(f"[Demo] DSP-Cloning Speaker {spk_id} using custom voice profile: {custom_voice}")
524
+ cloned_file = os.path.join("static/temp", f"cloned_part_{int(time.time())}_{i}.wav")
525
+ success = clone_voice_dsp(temp_file, custom_voice, cloned_file)
526
+ if success and os.path.exists(cloned_file):
527
+ try: os.remove(temp_file)
528
+ except: pass
529
+ temp_file = cloned_file
530
+
531
+ temp_files.append(temp_file)
532
+
533
+ if os.path.exists(temp_file) and os.path.getsize(temp_file) > 44:
534
+ audio_data, sr = sf.read(temp_file)
535
+ last_sr = sr
536
+ # Convert stereo to mono
537
+ if len(audio_data.shape) > 1:
538
+ audio_data = audio_data.mean(axis=1)
539
+
540
+ concatenated_audio.append(audio_data)
541
+
542
+ # Add natural breathing pause (0.3 seconds) between speakers
543
+ pause_samples = int(sr * 0.3)
544
+ concatenated_audio.append(np.zeros(pause_samples))
545
+
546
+ if len(concatenated_audio) > 0:
547
+ # Concatenate dialogue, excluding the trailing pause
548
+ final_audio = np.concatenate(concatenated_audio[:-1])
549
+
550
+ # Normalize audio amplitude safely
551
+ max_val = np.max(np.abs(final_audio))
552
+ if max_val > 0:
553
+ final_audio = final_audio / max_val * 0.8
554
+
555
+ # Save compiled high-fidelity speech script WAV file
556
+ sf.write(output_path, final_audio, last_sr)
557
+
558
+ # Clean up temporary parts
559
+ for temp_f in temp_files:
560
+ try:
561
+ os.remove(temp_f)
562
+ except:
563
+ pass
564
+ else:
565
+ raise Exception("SAPI5 compiled audio empty or missing parts")
566
+
567
+ except Exception as e:
568
+ print(f"[Demo] Fallback to simple synthesizer wave due to: {e}")
569
+ traceback.print_exc()
570
+
571
+ # Clean up any temp files created
572
+ for temp_f in temp_files:
573
+ try: os.remove(temp_f)
574
+ except: pass
575
+
576
+ # Generate simple visual waveform sweep as fallback
577
+ words = text_script.split()
578
+ duration = len(words) * 0.45
579
+ t = np.linspace(0, duration, int(SAMPLING_RATE * duration), endpoint=False)
580
+ carrier = np.sin(2 * np.pi * 120 * t)
581
+ if "Speaker 1" in text_script or req.speaker_id == 1:
582
+ carrier = np.sin(2 * np.pi * 220 * t)
583
+ harmonic1 = 0.5 * np.sin(2 * np.pi * 240 * t)
584
+ harmonic2 = 0.25 * np.sin(2 * np.pi * 360 * t)
585
+
586
+ envelope = np.zeros_like(t)
587
+ word_samples = len(t) // len(words)
588
+ for i in range(len(words)):
589
+ start = i * word_samples
590
+ end = min((i + 1) * word_samples, len(t))
591
+ w_t = np.linspace(0, np.pi, end - start)
592
+ envelope[start:end] = np.sin(w_t) ** 2
593
+
594
+ raw_audio = (carrier + harmonic1 + harmonic2) * envelope
595
+ raw_audio = raw_audio / np.max(np.abs(raw_audio)) * 0.8
596
+ sf.write(output_path, raw_audio, SAMPLING_RATE)
597
+
598
+ return {
599
+ "status": "success",
600
+ "audio_url": f"/static/temp/{output_filename}",
601
+ "text": req.text,
602
+ "mode": "AI Model" if USE_REAL_MODEL else "Demo Synthesizer"
603
+ }
604
+
605
+ except Exception as e:
606
+ traceback.print_exc()
607
+ raise HTTPException(status_code=500, detail=str(e))
608
+
609
+ @app.websocket("/api/stream")
610
+ async def websocket_endpoint(websocket: WebSocket):
611
+ """
612
+ WebSocket endpoint for real-time, low-latency streaming text-to-speech.
613
+ Receives text streams from client, generates audio chunks, and streams them back instantly.
614
+ """
615
+ await websocket.accept()
616
+ print("[WebSocket] Connected to streaming audio client.")
617
+
618
+ try:
619
+ while True:
620
+ # Receive JSON instruction from client
621
+ data = await websocket.receive_json()
622
+ text = data.get("text", "")
623
+
624
+ # Resolve remote URLs to local paths if needed
625
+ voice_sample_path = download_remote_voice(data.get("voice_sample_path", None))
626
+ speaker_id = data.get("speaker_id", 0)
627
+
628
+ speaker_voices = data.get("speaker_voices", {})
629
+ for k, v in speaker_voices.items():
630
+ speaker_voices[k] = download_remote_voice(v)
631
+ data["speaker_voices"] = speaker_voices
632
+
633
+ if not text:
634
+ continue
635
+
636
+ print(f"[WebSocket] Streaming request: '{text}' (Speaker: {speaker_id})")
637
+
638
+ # Real AI Model Streaming Path
639
+ if USE_REAL_MODEL:
640
+ proc, loaded_model = get_model()
641
+ if loaded_model is None:
642
+ await websocket.send_json({
643
+ "type": "error",
644
+ "message": "AI Model is loading. Streaming unavailable."
645
+ })
646
+ continue
647
+
648
+ # Setup streamer
649
+ from vibevoice.modular.streamer import AsyncAudioStreamer
650
+ streamer = AsyncAudioStreamer(batch_size=1)
651
+
652
+ # Format prompt
653
+ text_prompt = text
654
+ if ":" not in text_prompt:
655
+ text_prompt = f"Speaker {speaker_id}: {text_prompt}"
656
+
657
+ # Load reference voices for all active speakers in order
658
+ voice_samples = get_speaker_voices_list(text_prompt, voice_sample_path, speaker_voices)
659
+
660
+ inputs = proc(text=text_prompt, voice_samples=voice_samples, return_tensors="pt")
661
+
662
+ # Run generation in a separate background thread
663
+ def run_inference():
664
+ try:
665
+ with torch.no_grad():
666
+ loaded_model.generate(
667
+ **inputs,
668
+ tokenizer=proc.tokenizer,
669
+ audio_streamer=streamer,
670
+ max_new_tokens=400,
671
+ do_sample=True,
672
+ show_progress_bar=False
673
+ )
674
+ except Exception as ex:
675
+ print(f"[VibeVoice] Streaming generation exception: {ex}")
676
+ finally:
677
+ streamer.end()
678
+
679
+ threading.Thread(target=run_inference, daemon=True).start()
680
+
681
+ # Fetch audio chunks asynchronously from queue and stream to client
682
+ chunk_index = 0
683
+ async for audio_tensor in streamer.get_stream(0):
684
+ audio_numpy = audio_tensor.numpy()
685
+
686
+ # Convert Float32 audio values to 16-bit PCM integer bytes
687
+ audio_pcm = (audio_numpy * 32767.0).astype(np.int16)
688
+
689
+ # Send raw audio binary buffer
690
+ await websocket.send_bytes(audio_pcm.tobytes())
691
+ chunk_index += 1
692
+
693
+ # Send stream boundary signal
694
+ await websocket.send_json({"type": "done"})
695
+
696
+ # High-Fidelity Demo/Simulated Streaming Path
697
+ else:
698
+ print(f"[WebSocket-Demo] Synthesizing real-time high-fidelity streaming speech for script: {text}")
699
+ try:
700
+ # 1. Parse dialogue lines
701
+ dialogue_lines = parse_script_dialogue(text)
702
+
703
+ # 2. Loop through each line and stream it
704
+ for spk_id, line_text in dialogue_lines:
705
+ custom_voice = None
706
+ if speaker_voices:
707
+ custom_voice = speaker_voices.get(str(spk_id)) or speaker_voices.get(spk_id)
708
+ if not custom_voice and spk_id == 0:
709
+ custom_voice = voice_sample_path
710
+
711
+ temp_wav = os.path.join("static/temp", f"stream_temp_{int(time.time())}_{spk_id}.wav")
712
+ sapi5_generate_wav(line_text, spk_id, temp_wav)
713
+
714
+ # Apply voice cloning DSP if custom voice profile is present!
715
+ if custom_voice and os.path.exists(custom_voice) and os.path.getsize(custom_voice) > 100:
716
+ print(f"[WebSocket-Demo] DSP-Cloning Speaker {spk_id} using custom voice profile: {custom_voice}")
717
+ cloned_wav = os.path.join("static/temp", f"cloned_stream_{int(time.time())}_{spk_id}.wav")
718
+ success = clone_voice_dsp(temp_wav, custom_voice, cloned_wav)
719
+ if success and os.path.exists(cloned_wav):
720
+ try: os.remove(temp_wav)
721
+ except: pass
722
+ temp_wav = cloned_wav
723
+
724
+ if os.path.exists(temp_wav) and os.path.getsize(temp_wav) > 44:
725
+ audio_data, sr = sf.read(temp_wav)
726
+
727
+ # Convert stereo to mono
728
+ if len(audio_data.shape) > 1:
729
+ audio_data = audio_data.mean(axis=1)
730
+
731
+ # Resample to the standard SAMPLING_RATE (24000Hz) if needed
732
+ if sr != SAMPLING_RATE:
733
+ num_target = int(len(audio_data) * SAMPLING_RATE / sr)
734
+ audio_data = np.interp(
735
+ np.linspace(0, len(audio_data), num_target, endpoint=False),
736
+ np.arange(len(audio_data)),
737
+ audio_data
738
+ )
739
+
740
+ # Stream in chunks of ~250ms of audio
741
+ chunk_size = int(SAMPLING_RATE * 0.25)
742
+ words_in_line = line_text.split()
743
+ total_words = len(words_in_line)
744
+
745
+ # Map audio samples to words to synchronize 'word' HUD metadata events
746
+ samples_per_word = len(audio_data) / max(1, total_words)
747
+
748
+ current_word_idx = 0
749
+ for offset in range(0, len(audio_data), chunk_size):
750
+ chunk = audio_data[offset:offset+chunk_size]
751
+ if len(chunk) == 0:
752
+ continue
753
+
754
+ # Scale chunk dynamically to int16 PCM
755
+ max_chunk = np.max(np.abs(chunk))
756
+ scaled_chunk = chunk / max_chunk * 0.7 if max_chunk > 0 else chunk
757
+ pcm_bytes = (scaled_chunk * 32767.0).astype(np.int16).tobytes()
758
+
759
+ # Send binary audio buffer over WebSocket
760
+ await websocket.send_bytes(pcm_bytes)
761
+
762
+ # Broadcast 'word' events as the audio reaches those word offsets
763
+ current_sample_pos = offset + len(chunk)
764
+ word_progress = int(current_sample_pos / samples_per_word)
765
+ while current_word_idx < min(word_progress, total_words):
766
+ await websocket.send_json({
767
+ "type": "word",
768
+ "word": words_in_line[current_word_idx],
769
+ "index": current_word_idx,
770
+ "total": total_words
771
+ })
772
+ current_word_idx += 1
773
+
774
+ # Realtime throttle matching output playback
775
+ await asyncio.sleep(0.24)
776
+
777
+ # Ensure all final words are sent in the metadata stream
778
+ while current_word_idx < total_words:
779
+ await websocket.send_json({
780
+ "type": "word",
781
+ "word": words_in_line[current_word_idx],
782
+ "index": current_word_idx,
783
+ "total": total_words
784
+ })
785
+ current_word_idx += 1
786
+
787
+ # Clean up
788
+ try:
789
+ os.remove(temp_wav)
790
+ except:
791
+ pass
792
+
793
+ # Natural speech pause between sentences
794
+ await asyncio.sleep(0.3)
795
+ else:
796
+ raise Exception("Generated SAPI5 stream WAV is missing or empty")
797
+
798
+ await websocket.send_json({"type": "done"})
799
+
800
+ except Exception as ex:
801
+ print(f"[WebSocket-Demo] SAPI5 streaming exception: {ex}. Falling back to visual wave generator.")
802
+ traceback.print_exc()
803
+
804
+ # Resilient visual fallback to sine wave beeps if SAPI5 fails
805
+ words = text.split()
806
+ for word_idx, word in enumerate(words):
807
+ word_duration = 0.35
808
+ num_samples = int(SAMPLING_RATE * word_duration)
809
+ t = np.linspace(0, word_duration, num_samples, endpoint=False)
810
+
811
+ base_freq = 130 if speaker_id == 0 else 230
812
+ wave = np.sin(2 * np.pi * base_freq * t)
813
+ wave += 0.4 * np.sin(2 * np.pi * (base_freq * 2) * t)
814
+ wave += 0.2 * np.sin(2 * np.pi * (base_freq * 3) * t)
815
+
816
+ window = np.sin(np.linspace(0, np.pi, num_samples)) ** 2
817
+ chunk = wave * window
818
+
819
+ chunk = chunk / np.max(np.abs(chunk)) * 0.7 if np.max(np.abs(chunk)) > 0 else chunk
820
+ pcm_bytes = (chunk * 32767.0).astype(np.int16).tobytes()
821
+
822
+ await websocket.send_bytes(pcm_bytes)
823
+ await websocket.send_json({
824
+ "type": "word",
825
+ "word": word,
826
+ "index": word_idx,
827
+ "total": len(words)
828
+ })
829
+ await asyncio.sleep(0.28)
830
+
831
+ await websocket.send_json({"type": "done"})
832
+
833
+ except WebSocketDisconnect:
834
+ print("[WebSocket] Streaming client disconnected.")
835
+ except Exception as e:
836
+ print(f"[WebSocket] Error: {e}")
837
+ traceback.print_exc()
838
+
839
+ # Mount static assets files (temp outputs & cloned speakers)
840
+ app.mount("/static", StaticFiles(directory="static"), name="static")
841
+
842
+ @app.get("/")
843
+ async def root_fallback():
844
+ if os.path.exists("frontend/dist/index.html"):
845
+ return FileResponse("frontend/dist/index.html")
846
+ return HTMLResponse("""
847
+ <html>
848
+ <head>
849
+ <title>VibeVoice API Server</title>
850
+ <link rel="preconnect" href="https://fonts.googleapis.com">
851
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
852
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600&display=swap" rel="stylesheet">
853
+ <style>
854
+ body {
855
+ font-family: 'Outfit', sans-serif;
856
+ background: #07090e;
857
+ color: #f0f3fa;
858
+ display: flex;
859
+ flex-direction: column;
860
+ align-items: center;
861
+ justify-content: center;
862
+ height: 100vh;
863
+ margin: 0;
864
+ }
865
+ .container {
866
+ text-align: center;
867
+ padding: 40px;
868
+ background: rgba(255, 255, 255, 0.03);
869
+ border: 1px solid rgba(255, 255, 255, 0.08);
870
+ border-radius: 20px;
871
+ backdrop-filter: blur(10px);
872
+ max-width: 500px;
873
+ }
874
+ code {
875
+ background: rgba(0, 242, 254, 0.1);
876
+ color: #00f2fe;
877
+ padding: 8px 12px;
878
+ border-radius: 6px;
879
+ font-family: monospace;
880
+ display: inline-block;
881
+ margin: 15px 0;
882
+ }
883
+ </style>
884
+ </head>
885
+ <body>
886
+ <div class="container">
887
+ <h1 style="margin: 0 0 10px 0;">🎙️ VibeVoice API Server Online</h1>
888
+ <p style="color: #8e9bb5; line-height: 1.6; font-size: 14px;">The backend API server is fully running on port 8000! To access the high-end React + Vite + Tailwind frontend, start the dev server:</p>
889
+ <code>cd frontend; npm run dev</code>
890
+ <p style="margin: 15px 0 0 0; font-size: 12px; color: #8e9bb5;">Or build for production serving: <code>cd frontend; npm run build</code></p>
891
+ </div>
892
+ </body>
893
+ </html>
894
+ """)
895
+
896
+ # Serve the static built files from dist if compiled
897
+ if os.path.exists("frontend/dist"):
898
+ app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="dist")
899
+
900
+ if __name__ == "__main__":
901
+ import uvicorn
902
+ import os
903
+ # Start web app dynamically (binds to 0.0.0.0 and port 7860 for Hugging Face Space Docker support)
904
+ port = int(os.environ.get("PORT", 8000))
905
+ host = "0.0.0.0"
906
+ print(f"[VibeVoice] Starting server on http://{host}:{port}")
907
+ uvicorn.run("app:app", host=host, port=port, reload=False)
908
+
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ globals: globals.browser,
18
+ parserOptions: { ecmaFeatures: { jsx: true } },
19
+ },
20
+ },
21
+ ])
frontend/index.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎙️</text></svg>" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta name="description" content="VibeVoice React Studio - Real-time Voice Cloning and Speech Synthesis Playground by Microsoft Frontier Models." />
8
+ <title>VibeVoice React Studio | Frontier Voice AI</title>
9
+ <!-- Premium Fonts -->
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
13
+ <style>
14
+ body {
15
+ font-family: 'Plus Jakarta Sans', sans-serif !important;
16
+ }
17
+ h1, h2, h3, .font-orbitron {
18
+ font-family: 'Outfit', sans-serif !important;
19
+ }
20
+ #consoleBody {
21
+ font-family: 'JetBrains Mono', monospace !important;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body class="bg-[#07090e] text-[#f0f3fa]">
26
+ <div id="root"></div>
27
+ <script type="module" src="/src/main.jsx"></script>
28
+ </body>
29
+ </html>
30
+
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@supabase/supabase-js": "^2.106.0",
14
+ "@tailwindcss/vite": "^4.3.0",
15
+ "framer-motion": "^12.38.0",
16
+ "lucide-react": "^1.16.0",
17
+ "react": "^19.2.6",
18
+ "react-dom": "^19.2.6",
19
+ "tailwindcss": "^4.3.0"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^10.0.1",
23
+ "@types/react": "^19.2.14",
24
+ "@types/react-dom": "^19.2.3",
25
+ "@vitejs/plugin-react": "^6.0.1",
26
+ "eslint": "^10.3.0",
27
+ "eslint-plugin-react-hooks": "^7.1.1",
28
+ "eslint-plugin-react-refresh": "^0.5.2",
29
+ "globals": "^17.6.0",
30
+ "vite": "^8.0.12"
31
+ }
32
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1 @@
 
 
1
+ /* Custom styles are configured in index.css and Tailwind v4. App.css is intentionally left empty. */
frontend/src/App.jsx ADDED
@@ -0,0 +1,2184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import LandingPage from './LandingPage';
3
+ import { supabase } from './supabaseClient';
4
+ import {
5
+ Mic,
6
+ Upload,
7
+ Play,
8
+ Square,
9
+ Volume2,
10
+ VolumeX,
11
+ Terminal,
12
+ Settings,
13
+ Radio,
14
+ Info,
15
+ AlertCircle,
16
+ CheckCircle,
17
+ Sparkles,
18
+ Power,
19
+ Loader2,
20
+ Trash2,
21
+ Podcast,
22
+ Headset,
23
+ Newspaper,
24
+ Volume1,
25
+ PlayCircle,
26
+ Plus,
27
+ Music,
28
+ Edit2
29
+ } from 'lucide-react';
30
+
31
+ const SAMPLING_RATE = 24000;
32
+
33
+ function App() {
34
+ const [currentView, setCurrentView] = useState('landing');
35
+ const [savedVoices, setSavedVoices] = useState([]);
36
+ const [podcastHistory, setPodcastHistory] = useState([]);
37
+
38
+ // Model & Server Status States
39
+ const [modelStatus, setModelStatus] = useState({
40
+ useRealModel: true,
41
+ modelId: 'microsoft/VibeVoice-Realtime-0.5B',
42
+ loaded: false,
43
+ loading: true,
44
+ error: null,
45
+ device: 'CPU'
46
+ });
47
+ const [wsConnected, setWsConnected] = useState(false);
48
+ const [togglingModel, setTogglingModel] = useState(false);
49
+
50
+ // Script & Synthesis States
51
+ const [script, setScript] = useState(
52
+ "Speaker 0: Welcome to Vibe Voice Studio. I am cloned from your custom voice sample.\nSpeaker 1: That is incredible! The transition between speakers sounds so smooth and natural.\nSpeaker 0: Yes, Microsoft's continuous acoustic tokenizers make multi-speaker podcasts sound beautiful."
53
+ );
54
+ const [streamMode, setStreamMode] = useState(true);
55
+ const [agentState, setAgentState] = useState({
56
+ status: 'idle', // idle, thinking, speaking, listening
57
+ title: 'AGENT READY',
58
+ sub: 'Configure scripts and trigger generation below'
59
+ });
60
+
61
+ // Audio Playback UI States
62
+ const [isAudioPlayingState, setIsAudioPlayingState] = useState(false);
63
+ const [volume, setVolume] = useState(80);
64
+ const [speed, setSpeed] = useState('1.0');
65
+ const [timeline, setTimeline] = useState({ elapsed: 0, total: 0 });
66
+ const [showToolbar, setShowToolbar] = useState(false);
67
+
68
+ // Speaker Slots States (Voice Cloning)
69
+ const [speakers, setSpeakers] = useState([
70
+ { id: 0, name: 'Alex', isRecording: false, voicePath: null, previewUrl: null, role: 'Primary Speaker', color: 'cyan' },
71
+ { id: 1, name: 'Sarah', isRecording: false, voicePath: null, previewUrl: null, role: 'Dialogue Partner', color: 'purple' }
72
+ ]);
73
+
74
+ // Master Export Hub State
75
+ const [compiledAudioUrl, setCompiledAudioUrl] = useState(null);
76
+ const [compiledAudioMode, setCompiledAudioMode] = useState(null); // 'stream' or 'file'
77
+ const accumulatedSessionChunksRef = useRef([]);
78
+
79
+ // Preview Voice Reference Playback State
80
+ const [playingPreviewId, setPlayingPreviewId] = useState(null);
81
+ const previewSourceRef = useRef(null);
82
+
83
+ // Custom Naming Modal State
84
+ const [namingModal, setNamingModal] = useState({
85
+ isOpen: false,
86
+ fileBlob: null,
87
+ speakerId: null,
88
+ defaultName: ''
89
+ });
90
+ const [voiceNameInput, setVoiceNameInput] = useState('');
91
+
92
+ // Custom Action (Rename/Delete) Modal State
93
+ const [actionModal, setActionModal] = useState({
94
+ isOpen: false,
95
+ type: '', // 'rename_voice', 'delete_voice', 'rename_podcast', 'delete_podcast'
96
+ id: null,
97
+ oldValue: '',
98
+ inputValue: '',
99
+ audioUrl: '',
100
+ profileName: ''
101
+ });
102
+
103
+ // Console Logs State
104
+ const [logs, setLogs] = useState([
105
+ { type: 'system', text: 'Welcome to VibeVoice React Studio Console. System ready.', time: new Date().toLocaleTimeString() },
106
+ { type: 'info', text: 'VibeVoice neural weights loading directly on local CPU. Real-time zero-shot synthesis active.', time: new Date().toLocaleTimeString() }
107
+ ]);
108
+
109
+ // Web Audio Context & Playback References
110
+ const audioCtxRef = useRef(null);
111
+ const analyserNodeRef = useRef(null);
112
+ const gainNodeRef = useRef(null);
113
+ const wsRef = useRef(null);
114
+
115
+ // Streaming Queue Refs (Gapless Playback)
116
+ const playbackQueueRef = useRef([]);
117
+ const nextPlayTimeRef = useRef(0);
118
+ const isStreamingActiveRef = useRef(false);
119
+ const scheduledSourcesRef = useRef([]);
120
+ const doneReceivedRef = useRef(false);
121
+
122
+ // Traditional REST Player Refs
123
+ const traditionalSourceRef = useRef(null);
124
+ const traditionalStartClockRef = useRef(0);
125
+ const traditionalDurationRef = useRef(0);
126
+
127
+ // Microphone Recording Refs
128
+ const mediaRecordersRef = useRef([null, null]);
129
+ const audioChunksRef = useRef([[], []]);
130
+
131
+ // UI Canvas Visualizer Refs
132
+ const canvasRef = useRef(null);
133
+
134
+ // ==========================================================================
135
+ // Console Log Helper
136
+ // ==========================================================================
137
+ const addLog = (type, text) => {
138
+ setLogs(prev => [
139
+ ...prev,
140
+ { type, text, time: new Date().toLocaleTimeString() }
141
+ ]);
142
+ };
143
+
144
+ // Scroll Console to Bottom
145
+ useEffect(() => {
146
+ const consoleBody = document.getElementById('consoleBody');
147
+ if (consoleBody) {
148
+ consoleBody.scrollTop = consoleBody.scrollHeight;
149
+ }
150
+ }, [logs]);
151
+
152
+ // ==========================================================================
153
+ // Server Status & WebSocket Handshakes
154
+ // ==========================================================================
155
+ const checkServerStatus = async () => {
156
+ try {
157
+ const res = await fetch('/api/status');
158
+ const data = await res.json();
159
+
160
+ setModelStatus({
161
+ useRealModel: data.use_real_model,
162
+ modelId: data.model_id,
163
+ loaded: data.loaded,
164
+ loading: data.loading,
165
+ error: data.error,
166
+ device: data.device
167
+ });
168
+ } catch (e) {
169
+ addLog('error', 'Failed to connect to VibeVoice backend API server.');
170
+ }
171
+ };
172
+
173
+ const fetchSavedVoices = async () => {
174
+ try {
175
+ const { data, error } = await supabase.from('voice_profiles').select('*');
176
+ if (error) throw error;
177
+ setSavedVoices(data || []);
178
+ addLog('info', `Fetched ${data?.length || 0} saved voice profiles from Supabase.`);
179
+
180
+ // Auto-assign matching saved voices to speakers by name to get "old audio input" instantly!
181
+ if (data && data.length > 0) {
182
+ setSpeakers(prev => prev.map(s => {
183
+ const matched = data.find(v => v.name.trim().toLowerCase() === s.name.trim().toLowerCase());
184
+ if (matched && !s.voicePath) {
185
+ addLog('info', `Auto-loaded saved voice profile "${matched.name}" into Speaker ${s.id} slot.`);
186
+ return {
187
+ ...s,
188
+ voicePath: matched.audio_url,
189
+ previewUrl: matched.audio_url
190
+ };
191
+ }
192
+ return s;
193
+ }));
194
+ }
195
+ } catch (err) {
196
+ addLog('error', 'Failed to fetch saved voice profiles from Supabase: ' + err.message);
197
+ }
198
+ };
199
+
200
+ const fetchPodcastHistory = async () => {
201
+ try {
202
+ const { data, error } = await supabase
203
+ .from('podcast_history')
204
+ .select('*')
205
+ .order('created_at', { ascending: false });
206
+ if (error) throw error;
207
+ setPodcastHistory(data || []);
208
+ addLog('info', `Fetched ${data?.length || 0} archived podcasts from Supabase.`);
209
+ } catch (err) {
210
+ addLog('error', 'Failed to fetch podcast history: ' + err.message);
211
+ }
212
+ };
213
+
214
+ const savePodcastToHistory = async (wavBlob, textScript, mode) => {
215
+ try {
216
+ addLog('info', 'Archiving generated podcast to Supabase permanent storage...');
217
+ const fileName = `podcast_${Date.now()}.wav`;
218
+
219
+ // 1. Upload to storage
220
+ const { data: storageData, error: storageError } = await supabase
221
+ .storage
222
+ .from('voice_samples')
223
+ .upload(fileName, wavBlob);
224
+
225
+ if (storageError) throw storageError;
226
+
227
+ // 2. Get public url
228
+ const { data: publicUrlData } = supabase
229
+ .storage
230
+ .from('voice_samples')
231
+ .getPublicUrl(fileName);
232
+
233
+ const publicUrl = publicUrlData.publicUrl;
234
+
235
+ // 3. Insert metadata record into DB
236
+ const primaryVoice = speakers[0]?.voicePath || null;
237
+ const matchingProfile = primaryVoice ? savedVoices.find(v => v.audio_url === primaryVoice) : null;
238
+ const voiceProfileId = matchingProfile ? matchingProfile.id : null;
239
+
240
+ const { data: dbData, error: dbError } = await supabase
241
+ .from('podcast_history')
242
+ .insert([{
243
+ title: `Podcast - ${new Date().toLocaleString()} (${mode})`,
244
+ text_script: textScript,
245
+ audio_url: publicUrl,
246
+ speaker_id: 0,
247
+ voice_profile_id: voiceProfileId
248
+ }])
249
+ .select();
250
+
251
+ if (dbError) throw dbError;
252
+
253
+ addLog('success', 'Podcast archived successfully to Supabase Database!');
254
+ fetchPodcastHistory();
255
+ } catch (err) {
256
+ addLog('warning', `Failed to archive podcast to Supabase (${err.message}). Storing in local session memory...`);
257
+
258
+ // Fallback: Create local preview URL for the compiled podcast WAV
259
+ const localPreviewUrl = URL.createObjectURL(wavBlob);
260
+
261
+ setPodcastHistory(prev => [
262
+ {
263
+ id: `local_${Date.now()}`,
264
+ title: `Podcast - ${new Date().toLocaleString()} (${mode}) [Local Session]`,
265
+ text_script: textScript,
266
+ audio_url: localPreviewUrl,
267
+ speaker_id: 0,
268
+ voice_profile_id: null,
269
+ created_at: new Date().toISOString()
270
+ },
271
+ ...prev
272
+ ]);
273
+ addLog('success', 'Podcast successfully cached in local session memory!');
274
+ }
275
+ };
276
+
277
+ const saveTraditionalPodcastToHistory = async (audioUrl, textScript, mode) => {
278
+ try {
279
+ const response = await fetch(audioUrl);
280
+ const blob = await response.blob();
281
+ await savePodcastToHistory(blob, textScript, mode);
282
+ } catch (err) {
283
+ addLog('error', 'Failed to download compiled audio for archiving: ' + err.message);
284
+ }
285
+ };
286
+
287
+ // --- Custom Action Modal Trigger Hooks ---
288
+ const openRenameVoiceModal = (voiceId, oldName, audioUrl) => {
289
+ setActionModal({
290
+ isOpen: true,
291
+ type: 'rename_voice',
292
+ id: voiceId,
293
+ oldValue: oldName,
294
+ inputValue: oldName,
295
+ audioUrl,
296
+ profileName: oldName
297
+ });
298
+ };
299
+
300
+ const openDeleteVoiceModal = (voiceId, oldName, audioUrl) => {
301
+ setActionModal({
302
+ isOpen: true,
303
+ type: 'delete_voice',
304
+ id: voiceId,
305
+ oldValue: oldName,
306
+ inputValue: '',
307
+ audioUrl,
308
+ profileName: oldName
309
+ });
310
+ };
311
+
312
+ const openRenamePodcastModal = (podcastId, oldTitle) => {
313
+ setActionModal({
314
+ isOpen: true,
315
+ type: 'rename_podcast',
316
+ id: podcastId,
317
+ oldValue: oldTitle,
318
+ inputValue: oldTitle,
319
+ audioUrl: '',
320
+ profileName: oldTitle
321
+ });
322
+ };
323
+
324
+ const openDeletePodcastModal = (podcastId, title, audioUrl) => {
325
+ setActionModal({
326
+ isOpen: true,
327
+ type: 'delete_podcast',
328
+ id: podcastId,
329
+ oldValue: title,
330
+ inputValue: '',
331
+ audioUrl,
332
+ profileName: title
333
+ });
334
+ };
335
+
336
+ // --- Core execution functions invoked on Custom Modal Submit ---
337
+
338
+ const executeRenamePodcast = async (podcastId, newTitle) => {
339
+ if (!newTitle || newTitle.trim() === '') return;
340
+ try {
341
+ addLog('info', `Renaming podcast to "${newTitle.trim()}"...`);
342
+
343
+ // Local podcast session
344
+ if (typeof podcastId === 'string' && podcastId.startsWith('local_')) {
345
+ setPodcastHistory(prev => prev.map(p => p.id === podcastId ? { ...p, title: newTitle.trim() } : p));
346
+ addLog('success', `Podcast successfully renamed to "${newTitle.trim()}" locally.`);
347
+ return;
348
+ }
349
+
350
+ // Supabase DB podcast
351
+ const { error } = await supabase
352
+ .from('podcast_history')
353
+ .update({ title: newTitle.trim() })
354
+ .eq('id', podcastId);
355
+
356
+ if (error) throw error;
357
+
358
+ addLog('success', `Podcast title successfully updated in Supabase Database!`);
359
+ fetchPodcastHistory();
360
+ } catch (err) {
361
+ addLog('error', 'Failed to rename podcast: ' + err.message);
362
+ }
363
+ };
364
+
365
+ const executeRenameVoice = async (voiceId, newName, audioUrl) => {
366
+ if (!newName || newName.trim() === '') return;
367
+ try {
368
+ addLog('info', `Renaming voice profile to "${newName.trim()}"...`);
369
+
370
+ // Local voice profiles
371
+ if (typeof voiceId === 'string' && voiceId.startsWith('local_')) {
372
+ setSavedVoices(prev => prev.map(v => v.id === voiceId ? { ...v, name: newName.trim() } : v));
373
+
374
+ // Update speakers
375
+ setSpeakers(prev => prev.map(s => {
376
+ if (s.voicePath === audioUrl) {
377
+ return { ...s, name: newName.trim() };
378
+ }
379
+ return s;
380
+ }));
381
+
382
+ addLog('success', `Voice profile successfully renamed to "${newName.trim()}" locally.`);
383
+ return;
384
+ }
385
+
386
+ // Supabase
387
+ const { error } = await supabase
388
+ .from('voice_profiles')
389
+ .update({ name: newName.trim() })
390
+ .eq('id', voiceId);
391
+
392
+ if (error) throw error;
393
+
394
+ addLog('success', `Voice profile successfully renamed in database!`);
395
+
396
+ // Update state immediately
397
+ setSavedVoices(prev => prev.map(v => v.id === voiceId ? { ...v, name: newName.trim() } : v));
398
+
399
+ // Also update any speaker slot using it
400
+ setSpeakers(prev => prev.map(s => {
401
+ if (s.voicePath === audioUrl) {
402
+ return { ...s, name: newName.trim() };
403
+ }
404
+ return s;
405
+ }));
406
+ } catch (err) {
407
+ addLog('error', `Failed to rename voice profile: ${err.message}`);
408
+ }
409
+ };
410
+
411
+ const executeDeleteVoice = async (voiceId, audioUrl, profileName) => {
412
+ try {
413
+ addLog('info', `Deleting voice profile "${profileName}"...`);
414
+
415
+ // Clear voice path of speakers who use this profile
416
+ setSpeakers(prev => prev.map(s => s.voicePath === audioUrl ? { ...s, voicePath: null, previewUrl: null } : s));
417
+
418
+ // Local voice profile
419
+ if (typeof voiceId === 'string' && (voiceId.startsWith('local_') || voiceId.endsWith('.wav'))) {
420
+ setSavedVoices(prev => prev.filter(v => v.id !== voiceId));
421
+
422
+ if (audioUrl && audioUrl.startsWith('/static/cloned_voices/')) {
423
+ const filename = audioUrl.split('/').pop();
424
+ try {
425
+ await fetch(`/api/delete-voice/${filename}`, { method: 'DELETE' });
426
+ } catch(e) {}
427
+ }
428
+
429
+ addLog('success', `Voice profile "${profileName}" removed from local session.`);
430
+ return;
431
+ }
432
+
433
+ // Database profile
434
+ const { error: dbError } = await supabase
435
+ .from('voice_profiles')
436
+ .delete()
437
+ .eq('id', voiceId);
438
+
439
+ if (dbError) throw dbError;
440
+
441
+ // Extract filename from URL and delete from storage
442
+ if (audioUrl) {
443
+ try {
444
+ const filePart = audioUrl.split('/').pop();
445
+ if (filePart) {
446
+ await supabase.storage.from('voice_samples').remove([filePart]);
447
+ }
448
+ } catch (storageErr) {
449
+ console.warn('Failed to delete voice profile from Storage:', storageErr);
450
+ }
451
+ }
452
+
453
+ addLog('success', `Voice profile "${profileName}" successfully deleted from Supabase.`);
454
+ fetchSavedVoices();
455
+ } catch (err) {
456
+ addLog('error', `Failed to delete voice profile: ${err.message}`);
457
+ }
458
+ };
459
+
460
+ const executeDeletePodcast = async (id, audioUrl, title) => {
461
+ try {
462
+ addLog('info', `Deleting podcast "${title}" from archive...`);
463
+
464
+ if (typeof id === 'string' && id.startsWith('local_')) {
465
+ setPodcastHistory(prev => prev.filter(p => p.id !== id));
466
+ addLog('success', 'Podcast deleted successfully from local session memory.');
467
+ return;
468
+ }
469
+
470
+ // 1. Delete from database
471
+ const { error: dbError } = await supabase
472
+ .from('podcast_history')
473
+ .delete()
474
+ .eq('id', id);
475
+ if (dbError) throw dbError;
476
+
477
+ // 2. Try to extract filename from URL and delete from Storage
478
+ try {
479
+ const filePart = audioUrl.split('/').pop();
480
+ if (filePart) {
481
+ await supabase.storage.from('voice_samples').remove([filePart]);
482
+ }
483
+ } catch (err) {
484
+ console.warn('Failed to delete audio file from Storage:', err);
485
+ }
486
+
487
+ addLog('success', 'Podcast deleted successfully.');
488
+ fetchPodcastHistory();
489
+ } catch (err) {
490
+ addLog('error', 'Failed to delete podcast: ' + err.message);
491
+ }
492
+ };
493
+
494
+ const playHistoryPodcast = async (podcast) => {
495
+ initAudio();
496
+ handleStopPlayback(); // Stop any other playing sessions
497
+
498
+ try {
499
+ addLog('info', `Streaming archived podcast: "${podcast.title}"...`);
500
+ setIsAudioPlayingState(true);
501
+ setCompiledAudioUrl(podcast.audio_url);
502
+ setCompiledAudioMode('file');
503
+ loadTraditionalWav(podcast.audio_url);
504
+ } catch (e) {
505
+ addLog('error', 'Failed to play archived podcast.');
506
+ handleStopPlayback();
507
+ }
508
+ };
509
+
510
+ useEffect(() => {
511
+ addLog('system', 'Initializing VibeVoice Studio environment...');
512
+ checkServerStatus();
513
+ fetchSavedVoices();
514
+ fetchPodcastHistory();
515
+
516
+
517
+ // Poll server status periodically if loading
518
+ const interval = setInterval(() => {
519
+ checkServerStatus();
520
+ }, 4000);
521
+
522
+ return () => clearInterval(interval);
523
+ }, []);
524
+
525
+ // WebSocket Connection Manager
526
+ useEffect(() => {
527
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
528
+ const host = window.location.host;
529
+ const wsUrl = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
530
+ ? 'ws://127.0.0.1:8000/api/stream'
531
+ : `${protocol}//${host}/api/stream`;
532
+
533
+ addLog('system', `Opening real-time WebSocket connection to ${wsUrl}`);
534
+ const websocket = new WebSocket(wsUrl);
535
+ wsRef.current = websocket;
536
+
537
+ websocket.onopen = () => {
538
+ setWsConnected(true);
539
+ addLog('success', 'WebSocket real-time streaming link ESTABLISHED.');
540
+ };
541
+
542
+ websocket.onclose = () => {
543
+ setWsConnected(false);
544
+ addLog('error', 'WebSocket streaming link disconnected. Retrying connection...');
545
+ };
546
+
547
+ websocket.onerror = () => {
548
+ addLog('error', 'WebSocket encountered connection error.');
549
+ };
550
+
551
+ websocket.onmessage = async (event) => {
552
+ if (typeof event.data === 'string') {
553
+ const data = JSON.parse(event.data);
554
+ if (data.type === 'word') {
555
+ addLog('word', `Agent spoke word: "${data.word}"`);
556
+ } else if (data.type === 'done') {
557
+ addLog('success', 'WebSocket voice generation chunk transfer complete.');
558
+ doneReceivedRef.current = true;
559
+ // Generate local Object URL of the accumulated chunks for mastering
560
+ if (accumulatedSessionChunksRef.current.length > 0) {
561
+ try {
562
+ const wavBlob = exportWav(accumulatedSessionChunksRef.current, SAMPLING_RATE);
563
+ const objectUrl = URL.createObjectURL(wavBlob);
564
+ setCompiledAudioUrl(objectUrl);
565
+ setCompiledAudioMode('stream');
566
+ addLog('success', 'Master podcast output audio created and mastered for local export.');
567
+
568
+ // Automatically archive to Supabase history
569
+ savePodcastToHistory(wavBlob, script, 'WebSocket Stream');
570
+ } catch (err) {
571
+ addLog('error', `Failed to master session output: ${err.message}`);
572
+ }
573
+ }
574
+ // If no sources are playing and queue is completely empty, finalize playback
575
+ if (scheduledSourcesRef.current.length === 0 && playbackQueueRef.current.length === 0) {
576
+ handleStopPlayback();
577
+ }
578
+ } else if (data.type === 'error') {
579
+ addLog('error', `Server stream error: ${data.message}`);
580
+ handleStopPlayback();
581
+ }
582
+ } else {
583
+ // Raw Audio Binary Chunks
584
+ const arrayBuffer = await event.data.arrayBuffer();
585
+ const int16Array = new Int16Array(arrayBuffer);
586
+ const float32Array = new Float32Array(int16Array.length);
587
+
588
+ for (let i = 0; i < int16Array.length; i++) {
589
+ float32Array[i] = int16Array[i] / 32767.0;
590
+ }
591
+
592
+ if (isStreamingActiveRef.current) {
593
+ playbackQueueRef.current.push(float32Array);
594
+ // Accumulate for session download
595
+ accumulatedSessionChunksRef.current.push(float32Array);
596
+ scheduleNextStreamingChunk();
597
+ }
598
+ }
599
+ };
600
+
601
+ return () => {
602
+ if (websocket) websocket.close();
603
+ };
604
+ }, []);
605
+
606
+ // ==========================================================================
607
+ // Web Audio Context & Node Setup (Pillar 2 & 3)
608
+ // ==========================================================================
609
+ const initAudio = () => {
610
+ if (audioCtxRef.current) return;
611
+
612
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
613
+ const context = new AudioContextClass({ sampleRate: SAMPLING_RATE });
614
+ audioCtxRef.current = context;
615
+
616
+ const analyser = context.createAnalyser();
617
+ analyser.fftSize = 256;
618
+ analyserNodeRef.current = analyser;
619
+
620
+ const gain = context.createGain();
621
+ gain.gain.setValueAtTime(volume / 100, context.currentTime);
622
+ gainNodeRef.current = gain;
623
+
624
+ // Connect pipeline
625
+ gain.connect(analyser);
626
+ analyser.connect(context.destination);
627
+
628
+ addLog('system', `Browser AudioContext engine running at ${SAMPLING_RATE}Hz.`);
629
+ };
630
+
631
+ useEffect(() => {
632
+ if (gainNodeRef.current && audioCtxRef.current) {
633
+ gainNodeRef.current.gain.setValueAtTime(volume / 100, audioCtxRef.current.currentTime);
634
+ }
635
+ }, [volume]);
636
+
637
+ // Streaming Gapless Playback Logic
638
+ const startStreamingPlayback = () => {
639
+ initAudio();
640
+ if (audioCtxRef.current.state === 'suspended') {
641
+ audioCtxRef.current.resume();
642
+ }
643
+
644
+ playbackQueueRef.current = [];
645
+ accumulatedSessionChunksRef.current = []; // Clear previous export session chunks
646
+ scheduledSourcesRef.current = [];
647
+ doneReceivedRef.current = false;
648
+ nextPlayTimeRef.current = audioCtxRef.current.currentTime + 0.15;
649
+ isStreamingActiveRef.current = true;
650
+ setIsAudioPlayingState(true);
651
+
652
+ setAgentState({
653
+ status: 'speaking',
654
+ title: 'STREAMING SPEECH',
655
+ sub: 'Synthesizing voice chunks in real-time over WebSocket...'
656
+ });
657
+ addLog('info', 'WebSocket player active. Buffering and playing...');
658
+ };
659
+
660
+ const scheduleNextStreamingChunk = () => {
661
+ if (!isStreamingActiveRef.current || playbackQueueRef.current.length === 0) return;
662
+
663
+ const chunk = playbackQueueRef.current.shift();
664
+ if (!chunk || chunk.length === 0) return;
665
+
666
+ const context = audioCtxRef.current;
667
+ const buffer = context.createBuffer(1, chunk.length, SAMPLING_RATE);
668
+ buffer.copyToChannel(chunk, 0);
669
+
670
+ const source = context.createBufferSource();
671
+ source.buffer = buffer;
672
+ source.playbackRate.value = parseFloat(speed);
673
+ source.connect(gainNodeRef.current);
674
+
675
+ const startTime = Math.max(context.currentTime, nextPlayTimeRef.current);
676
+ source.start(startTime);
677
+
678
+ scheduledSourcesRef.current.push(source);
679
+
680
+ const duration = buffer.duration / source.playbackRate.value;
681
+ nextPlayTimeRef.current = startTime + duration;
682
+
683
+ source.onended = () => {
684
+ // Remove completed source
685
+ scheduledSourcesRef.current = scheduledSourcesRef.current.filter(src => src !== source);
686
+
687
+ // Check if queue is completely drained and done signal is received
688
+ if (scheduledSourcesRef.current.length === 0 && playbackQueueRef.current.length === 0 && isStreamingActiveRef.current && doneReceivedRef.current) {
689
+ handleStopPlayback();
690
+ addLog('info', 'Agent voice stream play completed.');
691
+ }
692
+ };
693
+ };
694
+
695
+ const handleStopPlayback = () => {
696
+ isStreamingActiveRef.current = false;
697
+ playbackQueueRef.current = [];
698
+
699
+ // Stop and clear all active buffers
700
+ scheduledSourcesRef.current.forEach(src => {
701
+ try { src.stop(); } catch(e) {}
702
+ });
703
+ scheduledSourcesRef.current = [];
704
+
705
+ if (traditionalSourceRef.current) {
706
+ try { traditionalSourceRef.current.stop(); } catch(e) {}
707
+ traditionalSourceRef.current = null;
708
+ }
709
+
710
+ // Stop reference preview playback if active
711
+ if (previewSourceRef.current) {
712
+ try { previewSourceRef.current.stop(); } catch(e) {}
713
+ previewSourceRef.current = null;
714
+ }
715
+ setPlayingPreviewId(null);
716
+
717
+ setIsAudioPlayingState(false);
718
+ setShowToolbar(false);
719
+ setAgentState({
720
+ status: 'idle',
721
+ title: 'AGENT READY',
722
+ sub: 'Configure scripts and trigger generation below'
723
+ });
724
+ addLog('system', 'Agent audio playback stopped.');
725
+ };
726
+
727
+ // ==========================================================================
728
+ // Canvas Waves Drawing Loop (AnalyserNode hook)
729
+ // ==========================================================================
730
+ useEffect(() => {
731
+ const canvas = canvasRef.current;
732
+ if (!canvas) return;
733
+ const ctx = canvas.getContext('2d');
734
+ const width = canvas.width;
735
+ const height = canvas.height;
736
+ let animationFrameId;
737
+
738
+ const renderWave = () => {
739
+ animationFrameId = requestAnimationFrame(renderWave);
740
+
741
+ ctx.fillStyle = 'rgba(7, 9, 14, 0.25)'; // Back-sweep fade
742
+ ctx.fillRect(0, 0, width, height);
743
+
744
+ let dataArray = null;
745
+ let bufferLength = 0;
746
+
747
+ if (analyserNodeRef.current && isAudioPlayingState) {
748
+ bufferLength = analyserNodeRef.current.frequencyBinCount;
749
+ dataArray = new Uint8Array(bufferLength);
750
+ analyserNodeRef.current.getByteTimeDomainData(dataArray);
751
+ }
752
+
753
+ ctx.lineWidth = 2.5;
754
+ ctx.shadowBlur = isAudioPlayingState ? 10 : 0;
755
+ ctx.shadowColor = 'rgba(0, 242, 254, 0.4)';
756
+
757
+ // Linear Neon color gradient across visualizer canvas
758
+ const gradient = ctx.createLinearGradient(0, 0, width, 0);
759
+ gradient.addColorStop(0, '#a259ff'); // Purple
760
+ gradient.addColorStop(0.5, '#00f2fe'); // Cyan
761
+ gradient.addColorStop(1, '#ff5c8d'); // Pink
762
+ ctx.strokeStyle = gradient;
763
+
764
+ ctx.beginPath();
765
+
766
+ if (dataArray) {
767
+ // Draw real sound wave oscillations
768
+ const sliceWidth = width / bufferLength;
769
+ let x = 0;
770
+ for (let i = 0; i < bufferLength; i++) {
771
+ const v = dataArray[i] / 128.0;
772
+ const y = (v * height) / 2;
773
+ if (i === 0) ctx.moveTo(x, y);
774
+ else ctx.lineTo(x, y);
775
+ x += sliceWidth;
776
+ }
777
+ } else {
778
+ // Draw calm animated breathing flatline when quiet
779
+ const points = 40;
780
+ const sliceWidth = width / points;
781
+ let x = 0;
782
+ const time = Date.now() * 0.003;
783
+ for (let i = 0; i < points; i++) {
784
+ const breathing = Math.sin(time) * 0.2 + 0.8;
785
+ const y = height / 2 + Math.sin(i * 0.15 + time * 2) * (2.5 * breathing);
786
+ if (i === 0) ctx.moveTo(x, y);
787
+ else ctx.lineTo(x, y);
788
+ x += sliceWidth;
789
+ }
790
+ }
791
+
792
+ ctx.lineTo(width, height / 2);
793
+ ctx.stroke();
794
+ };
795
+
796
+ renderWave();
797
+
798
+ return () => cancelAnimationFrame(animationFrameId);
799
+ }, [isAudioPlayingState]);
800
+
801
+ // ==========================================================================
802
+ // Synthesis Triggers (REST vs WebSocket Stream)
803
+ // ==========================================================================
804
+ const handleSynthesize = async () => {
805
+ const text = script.trim();
806
+ if (!text) {
807
+ addLog('error', 'Cannot synthesize. Text script field is empty!');
808
+ return;
809
+ }
810
+
811
+ initAudio();
812
+ handleStopPlayback(); // Halt ongoing audio runs
813
+
814
+ // Map dynamic speaker voice paths
815
+ const speakerVoices = {};
816
+ speakers.forEach(s => {
817
+ if (s.voicePath) {
818
+ speakerVoices[s.id] = s.voicePath;
819
+ }
820
+ });
821
+
822
+ // Reset master download url
823
+ setCompiledAudioUrl(null);
824
+ setCompiledAudioMode(null);
825
+
826
+ if (streamMode) {
827
+ // WS Streaming Mode
828
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
829
+ addLog('error', 'WebSocket is offline. Streaming mode disabled.');
830
+ return;
831
+ }
832
+
833
+ startStreamingPlayback();
834
+ wsRef.current.send(JSON.stringify({
835
+ text,
836
+ voice_sample_path: speakers[0]?.voicePath || null,
837
+ speaker_id: 0,
838
+ speaker_voices: speakerVoices
839
+ }));
840
+
841
+ } else {
842
+ // Traditional File-based Playback Mode
843
+ try {
844
+ addLog('info', 'Compiling voice script... Sending POST to VibeVoice generator...');
845
+ setAgentState({
846
+ status: 'thinking',
847
+ title: 'COMPILING AUDIO',
848
+ sub: 'Running neural speech generation weights...'
849
+ });
850
+ setIsAudioPlayingState(true);
851
+
852
+ const res = await fetch('/api/generate', {
853
+ method: 'POST',
854
+ headers: { 'Content-Type': 'application/json' },
855
+ body: JSON.stringify({
856
+ text,
857
+ voice_sample_path: speakers[0]?.voicePath || null,
858
+ speaker_id: 0,
859
+ speaker_voices: speakerVoices
860
+ })
861
+ });
862
+ const data = await res.json();
863
+
864
+ if (data.status === 'success') {
865
+ addLog('success', `Voice file compiled. Method: [${data.mode}]. Playing track...`);
866
+ loadTraditionalWav(data.audio_url);
867
+ setCompiledAudioUrl(data.audio_url);
868
+ setCompiledAudioMode('file');
869
+
870
+ // Automatically fetch compiled file and archive to Supabase history
871
+ saveTraditionalPodcastToHistory(data.audio_url, text, data.mode);
872
+ } else {
873
+ throw new Error('Server returned error response.');
874
+ }
875
+ } catch (e) {
876
+ handleStopPlayback();
877
+ addLog('error', `Synthesis failed: ${e.message}`);
878
+ }
879
+ }
880
+ };
881
+
882
+ const loadTraditionalWav = async (audioUrl) => {
883
+ try {
884
+ const res = await fetch(audioUrl);
885
+ const arrayBuffer = await res.arrayBuffer();
886
+
887
+ audioCtxRef.current.decodeAudioData(arrayBuffer, (decodedBuffer) => {
888
+ const source = audioCtxRef.current.createBufferSource();
889
+ source.buffer = decodedBuffer;
890
+ source.playbackRate.value = parseFloat(speed);
891
+ source.connect(gainNodeRef.current);
892
+ source.start(0);
893
+
894
+ traditionalSourceRef.current = source;
895
+ traditionalDurationRef.current = decodedBuffer.duration;
896
+ traditionalStartClockRef.current = audioCtxRef.current.currentTime;
897
+
898
+ setAgentState({
899
+ status: 'speaking',
900
+ title: 'PLAYING FILE',
901
+ sub: 'Streaming synthesized high-fidelity compiled audio track...'
902
+ });
903
+ setShowToolbar(true);
904
+ setTimeline({ elapsed: 0, total: decodedBuffer.duration });
905
+
906
+ const tickTimeline = () => {
907
+ if (traditionalSourceRef.current === source) {
908
+ const elapsed = (audioCtxRef.current.currentTime - traditionalStartClockRef.current) * parseFloat(speed);
909
+ setTimeline(prev => ({ ...prev, elapsed: Math.min(elapsed, decodedBuffer.duration) }));
910
+ if (elapsed < decodedBuffer.duration) {
911
+ requestAnimationFrame(tickTimeline);
912
+ }
913
+ }
914
+ };
915
+
916
+ tickTimeline();
917
+
918
+ source.onended = () => {
919
+ if (traditionalSourceRef.current === source) {
920
+ handleStopPlayback();
921
+ }
922
+ };
923
+
924
+ }, (err) => {
925
+ addLog('error', 'Failed to decode WAV file buffer.');
926
+ handleStopPlayback();
927
+ });
928
+ } catch(e) {
929
+ addLog('error', 'Error playing generated track.');
930
+ handleStopPlayback();
931
+ }
932
+ };
933
+
934
+ const formatTime = (seconds) => {
935
+ const m = Math.floor(seconds / 60).toString().padStart(2, '0');
936
+ const s = Math.floor(seconds % 60).toString().padStart(2, '0');
937
+ return `${m}:${s}`;
938
+ };
939
+
940
+ const encodeWAV = (samples, sampleRate) => {
941
+ const buffer = new ArrayBuffer(44 + samples.length * 2);
942
+ const view = new DataView(buffer);
943
+
944
+ const writeString = (view, offset, string) => {
945
+ for (let i = 0; i < string.length; i++) {
946
+ view.setUint8(offset + i, string.charCodeAt(i));
947
+ }
948
+ };
949
+
950
+ const floatTo16BitPCM = (output, offset, input) => {
951
+ for (let i = 0; i < input.length; i++, offset += 2) {
952
+ let s = Math.max(-1, Math.min(1, input[i]));
953
+ output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
954
+ }
955
+ };
956
+
957
+ /* RIFF identifier */
958
+ writeString(view, 0, 'RIFF');
959
+ /* file length */
960
+ view.setUint32(4, 36 + samples.length * 2, true);
961
+ /* RIFF type */
962
+ writeString(view, 8, 'WAVE');
963
+ /* format chunk identifier */
964
+ writeString(view, 12, 'fmt ');
965
+ /* format chunk length */
966
+ view.setUint32(16, 16, true);
967
+ /* sample format (raw) */
968
+ view.setUint16(20, 1, true);
969
+ /* channel count */
970
+ view.setUint16(22, 1, true);
971
+ /* sample rate */
972
+ view.setUint32(24, sampleRate, true);
973
+ /* byte rate (sample rate * block align) */
974
+ view.setUint32(28, sampleRate * 2, true);
975
+ /* block align (channel count * bytes per sample) */
976
+ view.setUint16(32, 2, true);
977
+ /* bits per sample */
978
+ view.setUint16(34, 16, true);
979
+ /* data chunk identifier */
980
+ writeString(view, 36, 'data');
981
+ /* data chunk length */
982
+ view.setUint32(40, samples.length * 2, true);
983
+
984
+ floatTo16BitPCM(view, 44, samples);
985
+
986
+ return new Blob([view], { type: 'audio/wav' });
987
+ };
988
+
989
+ // ==========================================================================
990
+ // Voice Cloning Microphone Hooks (Pillar 4)
991
+ // ==========================================================================
992
+ const toggleRecording = async (speakerId) => {
993
+ const speaker = speakers.find(s => s.id === speakerId);
994
+ if (!speaker) return;
995
+
996
+ if (speaker.isRecording) {
997
+ // Stop recording
998
+ const recorder = mediaRecordersRef.current[speakerId];
999
+ if (recorder) {
1000
+ recorder.stop();
1001
+ setSpeakers(prev => prev.map(s => s.id === speakerId ? { ...s, isRecording: false } : s));
1002
+ setAgentState({
1003
+ status: 'idle',
1004
+ title: 'AGENT READY',
1005
+ sub: 'Voice profile recorded. Syncing to neural engine...'
1006
+ });
1007
+ }
1008
+ } else {
1009
+ // Start recording
1010
+ try {
1011
+ addLog('info', `Requesting mic permission for Speaker ${speakerId} (${speaker.name})...`);
1012
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1013
+
1014
+ setAgentState({
1015
+ status: 'listening',
1016
+ title: 'LISTENING...',
1017
+ sub: 'Speak normally for 10-15s to capture vocal signature...'
1018
+ });
1019
+
1020
+ // Use independent default sample-rate AudioContext specifically for mic capture to avoid browser resampling bugs
1021
+ const RecordingContextClass = window.AudioContext || window.webkitAudioContext;
1022
+ const recContext = new RecordingContextClass();
1023
+
1024
+ const source = recContext.createMediaStreamSource(stream);
1025
+ const processor = recContext.createScriptProcessor(4096, 1, 1);
1026
+ const recordingSamples = [];
1027
+
1028
+ processor.onaudioprocess = (e) => {
1029
+ const channelData = e.inputBuffer.getChannelData(0);
1030
+ recordingSamples.push(new Float32Array(channelData));
1031
+ };
1032
+
1033
+ source.connect(processor);
1034
+ processor.connect(recContext.destination);
1035
+
1036
+ mediaRecordersRef.current[speakerId] = {
1037
+ stop: () => {
1038
+ source.disconnect(processor);
1039
+ processor.disconnect(recContext.destination);
1040
+ stream.getTracks().forEach(track => track.stop());
1041
+ recContext.close();
1042
+
1043
+ addLog('info', 'Encoding microphone speech data to 16-bit PCM WAV...');
1044
+ let totalLength = 0;
1045
+ for (let i = 0; i < recordingSamples.length; i++) {
1046
+ totalLength += recordingSamples[i].length;
1047
+ }
1048
+ const flattened = new Float32Array(totalLength);
1049
+ let offset = 0;
1050
+ for (let i = 0; i < recordingSamples.length; i++) {
1051
+ flattened.set(recordingSamples[i], offset);
1052
+ offset += recordingSamples[i].length;
1053
+ }
1054
+
1055
+ const wavBlob = encodeWAV(flattened, recContext.sampleRate);
1056
+ setNamingModal({
1057
+ isOpen: true,
1058
+ fileBlob: wavBlob,
1059
+ speakerId: speakerId,
1060
+ defaultName: speaker.name
1061
+ });
1062
+ setVoiceNameInput(speaker.name);
1063
+ }
1064
+ };
1065
+
1066
+ setSpeakers(prev => prev.map(s => s.id === speakerId ? { ...s, isRecording: true } : s));
1067
+ addLog('success', 'Microphone recording started. Speak clearly now!');
1068
+ } catch (e) {
1069
+ addLog('error', `Microphone recording failed: ${e.message}`);
1070
+ setAgentState({
1071
+ status: 'idle',
1072
+ title: 'AGENT READY',
1073
+ sub: 'Record trigger failed or permission denied'
1074
+ });
1075
+ }
1076
+ }
1077
+ };
1078
+
1079
+ const handleFileUpload = (e, speakerId) => {
1080
+ const file = e.target.files[0];
1081
+ if (!file) return;
1082
+ const speaker = speakers.find(s => s.id === speakerId);
1083
+ addLog('info', `Selected file for upload: ${file.name}`);
1084
+ const cleanFileName = file.name.replace(/\.[^/.]+$/, "");
1085
+ setNamingModal({
1086
+ isOpen: true,
1087
+ fileBlob: file,
1088
+ speakerId: speakerId,
1089
+ defaultName: cleanFileName
1090
+ });
1091
+ setVoiceNameInput(cleanFileName);
1092
+ };
1093
+
1094
+ // Play/Stop Reference Voice Preview using main AudioContext pipeline
1095
+ const playPreview = async (speakerId, url) => {
1096
+ initAudio();
1097
+ handleStopPlayback(); // Stop any other playing session
1098
+
1099
+ if (playingPreviewId === speakerId) {
1100
+ stopPreviewPlayback();
1101
+ return;
1102
+ }
1103
+
1104
+ try {
1105
+ addLog('info', `Playing voice preview for Speaker ${speakerId}...`);
1106
+ setIsAudioPlayingState(true);
1107
+ setPlayingPreviewId(speakerId);
1108
+
1109
+ const res = await fetch(url);
1110
+ const arrayBuffer = await res.arrayBuffer();
1111
+
1112
+ audioCtxRef.current.decodeAudioData(arrayBuffer, (decodedBuffer) => {
1113
+ const source = audioCtxRef.current.createBufferSource();
1114
+ source.buffer = decodedBuffer;
1115
+ source.connect(gainNodeRef.current);
1116
+ source.start(0);
1117
+
1118
+ previewSourceRef.current = source;
1119
+ setAgentState({
1120
+ status: 'speaking',
1121
+ title: 'PLAYING REFERENCE',
1122
+ sub: `Listening to cloned voice reference profile...`
1123
+ });
1124
+
1125
+ source.onended = () => {
1126
+ stopPreviewPlayback();
1127
+ };
1128
+ }, (err) => {
1129
+ addLog('error', 'Failed to decode reference audio file buffer.');
1130
+ stopPreviewPlayback();
1131
+ });
1132
+ } catch (e) {
1133
+ addLog('error', 'Error playing reference voice.');
1134
+ stopPreviewPlayback();
1135
+ }
1136
+ };
1137
+
1138
+ const stopPreviewPlayback = () => {
1139
+ if (previewSourceRef.current) {
1140
+ try { previewSourceRef.current.stop(); } catch(e) {}
1141
+ previewSourceRef.current = null;
1142
+ }
1143
+ setPlayingPreviewId(null);
1144
+ setIsAudioPlayingState(false);
1145
+ setAgentState({
1146
+ status: 'idle',
1147
+ title: 'AGENT READY',
1148
+ sub: 'Configure scripts and trigger generation below'
1149
+ });
1150
+ };
1151
+
1152
+ // Add Dynamic Speaker Slot
1153
+ const addSpeaker = () => {
1154
+ const nextId = speakers.length > 0 ? Math.max(...speakers.map(s => s.id)) + 1 : 0;
1155
+ const speakerColors = ['cyan', 'purple', 'emerald', 'amber', 'rose', 'blue'];
1156
+ const color = speakerColors[nextId % speakerColors.length];
1157
+ const roles = ['Podcast Guest', 'Narrator', 'Secondary Host', 'Panelist', 'Expert Analyst'];
1158
+ const role = roles[(nextId - 2) % roles.length] || 'Guest Slot';
1159
+
1160
+ const newSpk = {
1161
+ id: nextId,
1162
+ name: `Speaker ${nextId}`,
1163
+ isRecording: false,
1164
+ voicePath: null,
1165
+ previewUrl: null,
1166
+ role: role,
1167
+ color: color
1168
+ };
1169
+
1170
+ setSpeakers(prev => [...prev, newSpk]);
1171
+ addLog('system', `Added dynamic speaker slot: [Speaker ${nextId}]`);
1172
+ };
1173
+
1174
+ // Remove Dynamic Speaker Slot
1175
+ const removeSpeaker = (speakerId) => {
1176
+ if (speakerId === 0) {
1177
+ addLog('error', 'Cannot delete Speaker 0 (Primary Speaker is required).');
1178
+ return;
1179
+ }
1180
+
1181
+ handleStopPlayback();
1182
+ setSpeakers(prev => prev.filter(s => s.id !== speakerId));
1183
+ addLog('system', `Removed speaker slot: [Speaker ${speakerId}]`);
1184
+ };
1185
+
1186
+ const uploadVoiceProfile = async (fileBlob, speakerId, speakerName) => {
1187
+ try {
1188
+ addLog('info', `Uploading permanent voice profile "${speakerName}" to Supabase...`);
1189
+
1190
+ const fileName = `voice_${Date.now()}_${speakerId}.wav`;
1191
+
1192
+ // 1. Upload to Supabase Storage
1193
+ const { data: storageData, error: storageError } = await supabase
1194
+ .storage
1195
+ .from('voice_samples')
1196
+ .upload(fileName, fileBlob);
1197
+
1198
+ if (storageError) throw storageError;
1199
+
1200
+ // Get public URL
1201
+ const { data: publicUrlData } = supabase
1202
+ .storage
1203
+ .from('voice_samples')
1204
+ .getPublicUrl(fileName);
1205
+
1206
+ const publicUrl = publicUrlData.publicUrl;
1207
+
1208
+ // 2. Insert into Supabase Database
1209
+ const { data: dbData, error: dbError } = await supabase
1210
+ .from('voice_profiles')
1211
+ .insert([{ name: speakerName, audio_url: publicUrl }])
1212
+ .select();
1213
+
1214
+ if (dbError) throw dbError;
1215
+
1216
+ // 3. Update UI state
1217
+ setSpeakers(prev => prev.map(s => s.id === speakerId ? {
1218
+ ...s,
1219
+ voicePath: publicUrl,
1220
+ previewUrl: publicUrl
1221
+ } : s));
1222
+
1223
+ addLog('success', `Voice signature for "${speakerName}" saved to Supabase successfully!`);
1224
+
1225
+ // Refresh the saved voices list
1226
+ fetchSavedVoices();
1227
+
1228
+ } catch(e) {
1229
+ addLog('warning', `Supabase upload failed (${e.message}). Falling back to local backend storage...`);
1230
+ try {
1231
+ const formData = new FormData();
1232
+ const fileToUpload = fileBlob instanceof File
1233
+ ? fileBlob
1234
+ : new File([fileBlob], `voice_${Date.now()}_${speakerId}.wav`, { type: 'audio/wav' });
1235
+
1236
+ formData.append('file', fileToUpload);
1237
+ formData.append('speaker_name', speakerName);
1238
+
1239
+ const res = await fetch('/api/upload-voice', {
1240
+ method: 'POST',
1241
+ body: formData
1242
+ });
1243
+
1244
+ if (!res.ok) throw new Error(`Backend returned status ${res.status}`);
1245
+ const data = await res.json();
1246
+
1247
+ if (data.status === 'success') {
1248
+ const localUrl = '/' + data.voice_path;
1249
+
1250
+ setSpeakers(prev => prev.map(s => s.id === speakerId ? {
1251
+ ...s,
1252
+ voicePath: localUrl,
1253
+ previewUrl: localUrl
1254
+ } : s));
1255
+
1256
+ addLog('success', `Voice signature for "${speakerName}" saved locally on server!`);
1257
+
1258
+ // Instantly add this local voice to the savedVoices dropdown selection
1259
+ setSavedVoices(prev => {
1260
+ const exists = prev.some(v => v.audio_url === localUrl);
1261
+ if (exists) return prev;
1262
+ return [...prev, {
1263
+ id: data.filename,
1264
+ name: speakerName,
1265
+ audio_url: localUrl
1266
+ }];
1267
+ });
1268
+ } else {
1269
+ throw new Error(data.message || 'Local upload failed');
1270
+ }
1271
+ } catch (localErr) {
1272
+ addLog('error', `Voice signature local upload fallback failed: ${localErr.message}`);
1273
+ }
1274
+ }
1275
+ };
1276
+
1277
+ // Toggle AI Model Mode on Backend
1278
+ const handleToggleModelMode = async () => {
1279
+ const enable = !modelStatus.useRealModel;
1280
+ setTogglingModel(true);
1281
+ addLog('info', `Requesting engine toggle: ${enable ? 'Enabling AI Model' : 'Enabling Demo Synthesizer'}`);
1282
+
1283
+ try {
1284
+ const res = await fetch(`/api/toggle-model?enable=${enable}`, { method: 'POST' });
1285
+ const data = await res.json();
1286
+
1287
+ setModelStatus(prev => ({
1288
+ ...prev,
1289
+ useRealModel: data.use_real_model,
1290
+ loading: enable, // starts load
1291
+ loaded: false
1292
+ }));
1293
+
1294
+ setTimeout(checkServerStatus, 1500);
1295
+ } catch (e) {
1296
+ addLog('error', 'Toggle engine request failed.');
1297
+ } finally {
1298
+ setTogglingModel(false);
1299
+ }
1300
+ };
1301
+
1302
+ // Presets loaders
1303
+ const loadPreset = (type) => {
1304
+ if (type === 'podcast') {
1305
+ setScript(`Speaker 0: Welcome back to the Vibe Voice podcast segment. Today we are cloning human expressions.
1306
+ Speaker 1: That is right! The 0.5B model manages this dialogue transition on a local CPU extremely fast.
1307
+ Speaker 0: Incredible! It sounds like two real people sitting in this glassmorphic studio together.`);
1308
+ addLog('info', 'Loaded template: [Podcast Dialogue Interview]');
1309
+ } else if (type === 'assistant') {
1310
+ setScript("Speaker 0: Hello, I am your personalized voice assistant. How can I help you customize your Dine Direct orders today?");
1311
+ addLog('info', 'Loaded template: [DineDirect AI Support Agent]');
1312
+ } else if (type === 'news') {
1313
+ setScript("Speaker 1: Breaking news from Microsoft Research. The Vibe Voice speech synthesis models are now live on local devices.");
1314
+ addLog('info', 'Loaded template: [News Bulletins Broadcast]');
1315
+ }
1316
+ };
1317
+
1318
+ if (currentView === 'landing') {
1319
+ return <LandingPage onTryDemo={() => setCurrentView('studio')} />;
1320
+ }
1321
+
1322
+ return (
1323
+ <div className="relative min-h-screen bg-[#07090e] text-[#f0f3fa] font-sans selection:bg-[#00f2fe]/20 select-none pb-12 overflow-x-hidden">
1324
+ {/* Background Floating Ambient Blobs */}
1325
+ <div className="fixed inset-0 overflow-hidden pointer-events-none z-0">
1326
+ <div className="absolute w-[600px] h-[600px] rounded-full top-[-200px] left-[-150px] bg-gradient-to-br from-[#00f2fe]/15 to-transparent blur-[120px] animate-float-glow-1"></div>
1327
+ <div className="absolute w-[700px] h-[700px] rounded-full bottom-[-250px] right-[-150px] bg-gradient-to-br from-[#a259ff]/15 to-transparent blur-[120px] animate-float-glow-2"></div>
1328
+ </div>
1329
+
1330
+ <div className="relative z-10 max-w-[1300px] mx-auto px-4 pt-6 flex flex-col min-h-screen">
1331
+ {/* Header Widget */}
1332
+ <header className="flex flex-col md:flex-row justify-between items-center bg-glass p-4 rounded-2xl border border-white/8 backdrop-blur-xl mb-6 shadow-2xl gap-4">
1333
+ <div className="flex items-center gap-3 cursor-pointer select-none" onClick={() => setCurrentView('landing')}>
1334
+ <Sparkles className="w-8 h-8 text-neon-cyan drop-shadow-[0_0_10px_rgba(0,242,254,0.4)]" />
1335
+ <div>
1336
+ <h1 className="text-xl font-bold tracking-tight">
1337
+ VibeVoice<span className="bg-gradient-to-r from-[#00f2fe] to-[#a259ff] bg-clip-text text-transparent">Studio</span>
1338
+ </h1>
1339
+ <p className="text-[10px] text-[#8e9bb5] uppercase tracking-wider">Microsoft Frontier Speech Synthesis</p>
1340
+ </div>
1341
+ </div>
1342
+
1343
+ <div className="flex items-center gap-4">
1344
+ {/* Engine Status Panel */}
1345
+ <div className="flex items-center gap-3 bg-[#00f2fe]/5 px-4 py-2 rounded-xl border border-[#00f2fe]/20 shadow-[0_0_12px_rgba(0,242,254,0.06)]">
1346
+ <span className={`w-2 h-2 rounded-full ${
1347
+ modelStatus.loading
1348
+ ? 'bg-[#ffb703] shadow-[0_0_8px_#ffb703] animate-pulse'
1349
+ : modelStatus.loaded
1350
+ ? 'bg-[#00e673] shadow-[0_0_8px_#00e673]'
1351
+ : 'bg-[#ff5c8d] shadow-[0_0_8px_#ff5c8d]'
1352
+ }`}></span>
1353
+
1354
+ <span className="text-xs text-[#8e9bb5]">
1355
+ VibeVoice Engine: <strong className="text-[#f0f3fa]">
1356
+ {modelStatus.loading
1357
+ ? 'Loading AI...'
1358
+ : 'VibeVoice-1.5 AI'}
1359
+ </strong>
1360
+ </span>
1361
+ </div>
1362
+
1363
+ {/* Connection badge */}
1364
+ <div className={`flex items-center gap-2 text-xs font-semibold px-4 py-2 rounded-xl border ${
1365
+ wsConnected
1366
+ ? 'text-[#00f2fe] bg-[#00f2fe]/6 border-[#00f2fe]/20 shadow-[0_0_8px_rgba(0,242,254,0.1)]'
1367
+ : 'text-[#8e9bb5] bg-white/4 border-white/5'
1368
+ }`}>
1369
+ <Radio className={`w-4 h-4 ${wsConnected ? 'animate-pulse' : ''}`} />
1370
+ <span>{wsConnected ? 'Connected' : 'Offline'}</span>
1371
+ </div>
1372
+
1373
+ <button
1374
+ onClick={() => setCurrentView('landing')}
1375
+ className="flex items-center gap-1.5 px-4 py-2 text-xs font-bold bg-white/4 border border-white/5 hover:border-white/12 rounded-xl hover:bg-white/8 transition duration-200 cursor-pointer active:scale-95 text-white"
1376
+ >
1377
+ Back to Home
1378
+ </button>
1379
+ </div>
1380
+ </header>
1381
+
1382
+ {/* Dashboard Grid Grid */}
1383
+ <main className="grid grid-cols-1 lg:grid-cols-[360px_1fr] gap-6 flex-1">
1384
+ {/* Left Column: Voice Cloning Controls */}
1385
+ <section className="bg-glass p-6 rounded-3xl flex flex-col gap-5">
1386
+ <div>
1387
+ <div className="flex items-center gap-2 mb-1">
1388
+ <Settings className="w-5 h-5 text-neon-cyan" />
1389
+ <h2 className="text-lg font-semibold">Voice Cloning Center</h2>
1390
+ </div>
1391
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">Clone any voice in seconds. Record or upload a 10-30s audio sample to create a speaker profile prompt.</p>
1392
+ </div>
1393
+
1394
+ {/* Speaker Slots */}
1395
+ {speakers.map((spk, idx) => {
1396
+ const colorClasses = spk.id === 0
1397
+ ? 'bg-[#00f2fe]/6 border-[#00f2fe]/20 text-[#00f2fe]'
1398
+ : spk.id === 1
1399
+ ? 'bg-[#a259ff]/6 border-[#a259ff]/20 text-[#a259ff]'
1400
+ : spk.color === 'emerald' ? 'bg-[#00e673]/6 border-[#00e673]/20 text-[#00e673]'
1401
+ : spk.color === 'amber' ? 'bg-[#ffb703]/6 border-[#ffb703]/20 text-[#ffb703]'
1402
+ : spk.color === 'rose' ? 'bg-[#ff5c8d]/6 border-[#ff5c8d]/20 text-[#ff5c8d]'
1403
+ : 'bg-[#00f2fe]/6 border-[#00f2fe]/20 text-[#00f2fe]';
1404
+
1405
+ return (
1406
+ <div key={spk.id} className="bg-glass-card p-4 rounded-2xl flex flex-col gap-3 transition duration-300 relative group">
1407
+ <div className="flex justify-between items-center">
1408
+ <div className="flex items-center gap-2">
1409
+ <span className="text-xs font-bold uppercase tracking-wider text-[#8e9bb5]">Speaker {spk.id}</span>
1410
+ {spk.id > 0 && (
1411
+ <button
1412
+ onClick={() => removeSpeaker(spk.id)}
1413
+ className="opacity-0 group-hover:opacity-100 text-[#ff5c8d]/60 hover:text-[#ff5c8d] p-0.5 transition duration-200 cursor-pointer"
1414
+ title="Delete speaker slot"
1415
+ >
1416
+ <Trash2 className="w-3.5 h-3.5" />
1417
+ </button>
1418
+ )}
1419
+ </div>
1420
+ <span className={`text-[10px] px-2 py-0.5 rounded-full uppercase font-bold border ${colorClasses}`}>{spk.role}</span>
1421
+ </div>
1422
+
1423
+ <div className="flex flex-col gap-1">
1424
+ <label className="text-[10px] text-[#8e9bb5] uppercase tracking-wide">Speaker Name</label>
1425
+ <input
1426
+ type="text"
1427
+ value={spk.name}
1428
+ onChange={(e) => {
1429
+ const val = e.target.value;
1430
+ setSpeakers(prev => prev.map(s => s.id === spk.id ? { ...s, name: val } : s));
1431
+ }}
1432
+ className="bg-black/20 border border-white/6 rounded-lg px-3 py-1.5 text-xs text-[#f0f3fa] focus:border-[#00f2fe] focus:shadow-[0_0_8px_rgba(0,242,254,0.15)] outline-none"
1433
+ />
1434
+ </div>
1435
+
1436
+ <div className="flex flex-col gap-1">
1437
+ <label className="text-[10px] text-[#8e9bb5] uppercase tracking-wide">Or Select Saved Voice</label>
1438
+ <div className="flex gap-2">
1439
+ <select
1440
+ value={spk.voicePath ? savedVoices.find(v => v.audio_url === spk.voicePath)?.id || '' : ''}
1441
+ onChange={(e) => {
1442
+ const val = e.target.value;
1443
+ const selectedVoice = savedVoices.find(v => String(v.id) === String(val));
1444
+ if (selectedVoice) {
1445
+ setSpeakers(prev => prev.map(s => s.id === spk.id ? {
1446
+ ...s,
1447
+ name: selectedVoice.name,
1448
+ voicePath: selectedVoice.audio_url,
1449
+ previewUrl: selectedVoice.audio_url
1450
+ } : s));
1451
+ addLog('info', `Selected saved voice "${selectedVoice.name}" from Supabase.`);
1452
+ } else {
1453
+ // Clear selection
1454
+ setSpeakers(prev => prev.map(s => s.id === spk.id ? {
1455
+ ...s,
1456
+ voicePath: null,
1457
+ previewUrl: null
1458
+ } : s));
1459
+ }
1460
+ }}
1461
+ className="flex-1 bg-black/20 border border-white/6 rounded-lg px-3 py-1.5 text-xs text-[#f0f3fa] focus:border-[#00f2fe] focus:shadow-[0_0_8px_rgba(0,242,254,0.15)] outline-none"
1462
+ >
1463
+ <option value="">-- Choose a Voice --</option>
1464
+ {savedVoices.map(v => (
1465
+ <option key={v.id} value={v.id}>{v.name}</option>
1466
+ ))}
1467
+ </select>
1468
+
1469
+ {spk.voicePath && (() => {
1470
+ const matchedVoice = savedVoices.find(v => v.audio_url === spk.voicePath);
1471
+ if (matchedVoice) {
1472
+ return (
1473
+ <div className="flex items-center gap-1 shrink-0">
1474
+ <button
1475
+ onClick={() => openRenameVoiceModal(matchedVoice.id, matchedVoice.name, matchedVoice.audio_url)}
1476
+ className="p-1.5 rounded-lg border border-white/6 hover:border-[#00f2fe]/40 text-[#8e9bb5] hover:text-[#00f2fe] bg-white/2 hover:bg-white/5 active:scale-95 transition cursor-pointer"
1477
+ title="Rename selected voice signature"
1478
+ >
1479
+ <Edit2 className="w-3.5 h-3.5" />
1480
+ </button>
1481
+ <button
1482
+ onClick={() => openDeleteVoiceModal(matchedVoice.id, matchedVoice.name, matchedVoice.audio_url)}
1483
+ className="p-1.5 rounded-lg border border-white/6 hover:border-[#ff5c8d]/40 text-[#8e9bb5] hover:text-[#ff5c8d] bg-white/2 hover:bg-[#ff5c8d]/10 active:scale-95 transition cursor-pointer"
1484
+ title="Delete selected voice signature"
1485
+ >
1486
+ <Trash2 className="w-3.5 h-3.5" />
1487
+ </button>
1488
+ </div>
1489
+ );
1490
+ }
1491
+ return null;
1492
+ })()}
1493
+ </div>
1494
+ </div>
1495
+
1496
+ <div className="grid grid-cols-2 gap-2">
1497
+ <button
1498
+ onClick={() => toggleRecording(spk.id)}
1499
+ className={`flex items-center justify-center gap-1.5 py-1.5 px-3 rounded-lg border text-xs cursor-pointer active:scale-95 transition duration-200 ${
1500
+ spk.isRecording
1501
+ ? 'bg-[#ff5c8d]/20 border-[#ff5c8d] text-[#ff5c8d] animate-pulse'
1502
+ : 'bg-transparent border-white/10 hover:border-[#00f2fe] hover:text-[#00f2fe] text-white'
1503
+ }`}
1504
+ >
1505
+ {spk.isRecording ? <Square className="w-3.5 h-3.5" /> : <Mic className="w-3.5 h-3.5" />}
1506
+ {spk.isRecording ? 'Stop Mic' : 'Record Mic'}
1507
+ </button>
1508
+
1509
+ <label className="flex items-center justify-center gap-1.5 py-1.5 px-3 rounded-lg border border-white/10 hover:border-[#00f2fe] hover:text-[#00f2fe] text-white text-xs cursor-pointer active:scale-95 transition duration-200">
1510
+ <Upload className="w-3.5 h-3.5" />
1511
+ Upload WAV
1512
+ <input
1513
+ type="file"
1514
+ accept="audio/wav,audio/mp3"
1515
+ onChange={(e) => handleFileUpload(e, spk.id)}
1516
+ className="hidden"
1517
+ />
1518
+ </label>
1519
+ </div>
1520
+
1521
+ {spk.previewUrl && (
1522
+ <div className="flex flex-col gap-1.5 pt-2 border-t border-dashed border-white/5">
1523
+ <span className="text-[10px] text-[#00e673] font-semibold flex items-center gap-1">
1524
+ <CheckCircle className="w-3 h-3" /> Voice Cloned Successfully
1525
+ </span>
1526
+
1527
+ {/* Premium Custom Reference Audio Player */}
1528
+ <div className="flex items-center gap-3 bg-white/4 p-2 rounded-xl border border-white/5 mt-1">
1529
+ <button
1530
+ onClick={() => playPreview(spk.id, spk.previewUrl)}
1531
+ className="w-8 h-8 rounded-full bg-[#00f2fe]/20 border border-[#00f2fe]/30 flex items-center justify-center text-[#00f2fe] hover:bg-[#00f2fe] hover:text-black transition duration-200 active:scale-90 cursor-pointer"
1532
+ >
1533
+ {playingPreviewId === spk.id ? (
1534
+ <Square className="w-3.5 h-3.5 fill-current" />
1535
+ ) : (
1536
+ <Play className="w-3.5 h-3.5 fill-current ml-0.5" />
1537
+ )}
1538
+ </button>
1539
+ <div className="flex-1">
1540
+ <span className="text-[10px] text-white font-semibold flex items-center gap-1 select-text">
1541
+ <Music className="w-3 h-3 text-[#00f2fe]" /> reference_{spk.name}.wav
1542
+ </span>
1543
+ <span className="text-[9px] text-[#8e9bb5]">Ready for zero-shot synthesis</span>
1544
+ </div>
1545
+ </div>
1546
+ </div>
1547
+ )}
1548
+ </div>
1549
+ );
1550
+ })}
1551
+
1552
+ {/* Add Speaker Slot Trigger */}
1553
+ <button
1554
+ onClick={addSpeaker}
1555
+ className="flex items-center justify-center gap-2 w-full py-3 px-4 bg-white/4 hover:bg-[#00f2fe]/10 border border-white/5 hover:border-[#00f2fe]/30 text-[#8e9bb5] hover:text-[#00f2fe] text-xs font-bold rounded-xl transition duration-200 cursor-pointer active:scale-95"
1556
+ >
1557
+ <Plus className="w-4 h-4" />
1558
+ Add Speaker Slot
1559
+ </button>
1560
+
1561
+ {/* Presets */}
1562
+ <div className="mt-2 pt-4 border-t border-white/5 flex flex-col gap-2.5">
1563
+ <h3 className="text-xs font-semibold text-[#8e9bb5]">Dialogue Templates</h3>
1564
+ <div className="flex flex-col gap-2">
1565
+ <button
1566
+ onClick={() => loadPreset('podcast')}
1567
+ className="flex items-center gap-2.5 w-full py-2.5 px-3 bg-white/2 hover:bg-white/5 border border-white/5 hover:border-white/10 text-[#8e9bb5] hover:text-[#f0f3fa] text-xs font-medium rounded-xl transition duration-200 justify-start cursor-pointer"
1568
+ >
1569
+ <Podcast className="w-4 h-4 text-[#a259ff]" />
1570
+ Podcast Interview Preset
1571
+ </button>
1572
+ <button
1573
+ onClick={() => loadPreset('assistant')}
1574
+ className="flex items-center gap-2.5 w-full py-2.5 px-3 bg-white/2 hover:bg-white/5 border border-white/5 hover:border-white/10 text-[#8e9bb5] hover:text-[#f0f3fa] text-xs font-medium rounded-xl transition duration-200 justify-start cursor-pointer"
1575
+ >
1576
+ <Headset className="w-4 h-4 text-[#00f2fe]" />
1577
+ DineDirect Voice Agent
1578
+ </button>
1579
+ <button
1580
+ onClick={() => loadPreset('news')}
1581
+ className="flex items-center gap-2.5 w-full py-2.5 px-3 bg-white/2 hover:bg-white/5 border border-white/5 hover:border-white/10 text-[#8e9bb5] hover:text-[#f0f3fa] text-xs font-medium rounded-xl transition duration-200 justify-start cursor-pointer"
1582
+ >
1583
+ <Newspaper className="w-4 h-4 text-[#ff5c8d]" />
1584
+ News Broadcast Preset
1585
+ </button>
1586
+ </div>
1587
+ </div>
1588
+
1589
+ {/* 🎙️ Voice Vault (Saved Neural Profiles) */}
1590
+ <div className="mt-4 pt-4 border-t border-white/5 flex flex-col gap-2.5">
1591
+ <div className="flex items-center justify-between">
1592
+ <h3 className="text-xs font-bold text-[#8e9bb5] uppercase tracking-wider">Voice Vault ({savedVoices.length})</h3>
1593
+ <span className="text-[10px] text-[#00f2fe] bg-[#00f2fe]/10 px-2 py-0.5 rounded-full font-semibold border border-[#00f2fe]/20">Storage</span>
1594
+ </div>
1595
+
1596
+ <div className="flex flex-col gap-2 max-h-[220px] overflow-y-auto pr-1">
1597
+ {savedVoices.length === 0 ? (
1598
+ <p className="text-[11px] text-[#8e9bb5] italic py-2 text-center">No voice profiles saved yet.</p>
1599
+ ) : (
1600
+ savedVoices.map(v => (
1601
+ <div key={v.id} className="flex items-center justify-between bg-white/2 hover:bg-white/4 border border-white/5 hover:border-white/10 p-2 rounded-xl transition duration-150 group/vault">
1602
+ <div className="flex items-center gap-2 min-w-0">
1603
+ <button
1604
+ onClick={() => playPreview(v.id, v.audio_url)}
1605
+ className="w-6 h-6 rounded-full bg-white/5 hover:bg-[#00f2fe]/20 hover:text-[#00f2fe] flex items-center justify-center text-[#8e9bb5] transition active:scale-90 cursor-pointer shrink-0"
1606
+ title="Preview Voice"
1607
+ >
1608
+ {playingPreviewId === v.id ? (
1609
+ <Square className="w-2.5 h-2.5 fill-current animate-pulse text-[#00f2fe]" />
1610
+ ) : (
1611
+ <Play className="w-2.5 h-2.5 fill-current ml-0.5" />
1612
+ )}
1613
+ </button>
1614
+ <span className="text-xs font-semibold text-white truncate max-w-[120px]" title={v.name}>
1615
+ {v.name}
1616
+ </span>
1617
+ </div>
1618
+
1619
+ <div className="flex items-center gap-1 shrink-0">
1620
+ <button
1621
+ onClick={() => openRenameVoiceModal(v.id, v.name, v.audio_url)}
1622
+ className="p-1 rounded hover:bg-white/5 text-[#8e9bb5] hover:text-[#00f2fe] transition active:scale-95 cursor-pointer"
1623
+ title="Rename voice profile"
1624
+ >
1625
+ <Edit2 className="w-3.5 h-3.5" />
1626
+ </button>
1627
+ <button
1628
+ onClick={() => openDeleteVoiceModal(v.id, v.name, v.audio_url)}
1629
+ className="p-1 rounded hover:bg-[#ff5c8d]/5 text-[#8e9bb5] hover:text-[#ff5c8d] transition active:scale-95 cursor-pointer"
1630
+ title="Delete voice profile"
1631
+ >
1632
+ <Trash2 className="w-3.5 h-3.5" />
1633
+ </button>
1634
+ </div>
1635
+ </div>
1636
+ ))
1637
+ )}
1638
+ </div>
1639
+ </div>
1640
+ </section>
1641
+
1642
+ {/* Right Column: Execution Panels */}
1643
+ <section className="flex flex-col gap-6">
1644
+
1645
+ {/* Visualizer Container */}
1646
+ <div className="bg-glass p-6 rounded-3xl flex flex-col justify-center items-center gap-4 min-h-[250px] bg-gradient-to-b from-[#101830]/30 to-transparent">
1647
+ <div className="flex flex-col md:flex-row items-center gap-8 w-full max-w-[500px] justify-center">
1648
+ {/* Visual pulsating holographic Orb */}
1649
+ <div className="relative w-[90px] h-[90px] flex items-center justify-center">
1650
+ <div className={`w-[60px] h-[60px] rounded-full z-10 transition-all duration-500 ${
1651
+ agentState.status === 'idle' && 'orb-state-idle'
1652
+ } ${
1653
+ agentState.status === 'thinking' && 'orb-state-thinking'
1654
+ } ${
1655
+ agentState.status === 'speaking' && 'orb-state-speaking'
1656
+ } ${
1657
+ agentState.status === 'listening' && 'orb-state-listening'
1658
+ }`}></div>
1659
+
1660
+ {/* Glowing backing shadows */}
1661
+ <div className={`absolute -inset-[15px] rounded-full blur-[25px] opacity-50 z-0 transition-all duration-500 ${
1662
+ agentState.status === 'idle' && 'bg-[#00f2fe]'
1663
+ } ${
1664
+ agentState.status === 'thinking' && 'bg-[#a259ff] scale-110 animate-pulse'
1665
+ } ${
1666
+ agentState.status === 'speaking' && 'bg-[#ff5c8d]'
1667
+ } ${
1668
+ agentState.status === 'listening' && 'bg-[#00e673]'
1669
+ }`}></div>
1670
+
1671
+ {/* Expansion speech rings */}
1672
+ {agentState.status === 'speaking' && (
1673
+ <>
1674
+ <div className="absolute inset-0 rounded-full border-2 border-[#ff5c8d] shadow-[0_0_10px_rgba(255,92,141,0.35)] animate-[waveExpand_1.2s_infinite_cubic-bezier(0.1,0.8,0.3,1)] opacity-100 z-10"></div>
1675
+ <div className="absolute inset-0 rounded-full border-2 border-[#ff5c8d] shadow-[0_0_10px_rgba(255,92,141,0.35)] animate-[waveExpand_1.2s_infinite_cubic-bezier(0.1,0.8,0.3,1)] opacity-100 z-10 [animation-delay:0.6s]"></div>
1676
+ </>
1677
+ )}
1678
+ </div>
1679
+
1680
+ <div className="flex flex-col text-center md:text-left gap-1">
1681
+ <span className="font-orbitron font-extrabold text-lg tracking-wider text-shadow-md text-[#f0f3fa] uppercase">
1682
+ {agentState.title}
1683
+ </span>
1684
+ <p className="text-xs text-[#8e9bb5]">{agentState.sub}</p>
1685
+ </div>
1686
+ </div>
1687
+
1688
+ {/* Waveform Canvas */}
1689
+ <div className="w-full flex justify-center border-t border-white/5 pt-4 mt-2">
1690
+ <canvas ref={canvasRef} width="600" height="90" className="w-full max-w-[600px] h-[75px] rounded-xl"></canvas>
1691
+ </div>
1692
+ </div>
1693
+
1694
+ {/* Script Studio Box */}
1695
+ <div className="bg-glass p-6 rounded-3xl flex flex-col gap-4">
1696
+ <div className="flex justify-between items-center border-b border-white/5 pb-4">
1697
+ <div className="flex items-center gap-2">
1698
+ <Sparkles className="w-5 h-5 text-[#a259ff]" />
1699
+ <h2 className="text-md font-semibold">Podcast Script Studio</h2>
1700
+ </div>
1701
+
1702
+ {/* Real-time Streaming Check */}
1703
+ <label className="flex items-center gap-2.5 cursor-pointer">
1704
+ <input
1705
+ type="checkbox"
1706
+ checked={streamMode}
1707
+ onChange={(e) => setStreamMode(e.target.checked)}
1708
+ className="hidden"
1709
+ />
1710
+ <div className={`w-[42px] h-[22px] rounded-full relative border transition duration-300 ${
1711
+ streamMode ? 'bg-[#00f2fe]/20 border-[#00f2fe]' : 'bg-white/8 border-white/10'
1712
+ }`}>
1713
+ <div className={`w-4 h-4 rounded-full absolute top-[2px] left-[2px] transition-transform duration-300 ${
1714
+ streamMode ? 'transform translate-x-[20px] bg-[#00f2fe] shadow-[0_0_6px_#00f2fe]' : 'bg-[#f0f3fa]'
1715
+ }`}></div>
1716
+ </div>
1717
+ <span className={`text-xs font-semibold ${streamMode ? 'text-[#00f2fe]' : 'text-[#8e9bb5]'}`}>
1718
+ Real-time Streaming
1719
+ </span>
1720
+ </label>
1721
+ </div>
1722
+
1723
+ {/* Textarea script fields */}
1724
+ <div className="bg-black/25 border border-white/8 rounded-xl overflow-hidden">
1725
+ <textarea
1726
+ value={script}
1727
+ onChange={(e) => setScript(e.target.value)}
1728
+ placeholder="Format: Speaker ID: Speech dialog content..."
1729
+ className="w-full h-[180px] bg-transparent resize-none p-4 text-[#f0f3fa] font-mono text-sm leading-relaxed outline-none border-none"
1730
+ />
1731
+ </div>
1732
+
1733
+ {/* Action Buttons */}
1734
+ <div className="flex gap-4">
1735
+ <button
1736
+ onClick={handleSynthesize}
1737
+ disabled={isAudioPlayingState}
1738
+ className="flex-1 flex items-center justify-center gap-2 py-3 px-6 bg-gradient-to-r from-[#00f2fe] to-[#a259ff] hover:shadow-[0_0_20px_rgba(0,242,254,0.4)] text-black font-bold rounded-xl transition duration-200 active:scale-98 cursor-pointer disabled:opacity-40"
1739
+ >
1740
+ <PlayCircle className="w-5 h-5" />
1741
+ Synthesize Dialogue
1742
+ </button>
1743
+ <button
1744
+ onClick={handleStopPlayback}
1745
+ disabled={!isAudioPlayingState}
1746
+ className="flex-1 flex items-center justify-center gap-2 py-3 px-6 bg-white/5 border border-white/8 hover:bg-white/10 text-white font-medium rounded-xl transition duration-200 active:scale-98 cursor-pointer disabled:opacity-40"
1747
+ >
1748
+ <Square className="w-4 h-4" />
1749
+ Stop Playback
1750
+ </button>
1751
+ </div>
1752
+ </div>
1753
+
1754
+ {/* Master Export Hub */}
1755
+ {compiledAudioUrl && (
1756
+ <div className="bg-glass p-6 rounded-3xl border border-[#00f2fe]/20 bg-gradient-to-r from-[#00f2fe]/4 to-[#a259ff]/4 shadow-[0_0_20px_rgba(0,242,254,0.08)] flex flex-col gap-4 transition duration-300">
1757
+ <div className="flex justify-between items-center border-b border-white/5 pb-3">
1758
+ <div className="flex items-center gap-2">
1759
+ <Sparkles className="w-5 h-5 text-neon-cyan animate-pulse" />
1760
+ <div>
1761
+ <h3 className="text-sm font-bold text-white uppercase tracking-wider">Podcast Master Output</h3>
1762
+ <p className="text-[9px] text-[#8e9bb5] uppercase tracking-wide">Ready for download & broadcast</p>
1763
+ </div>
1764
+ </div>
1765
+ <span className="text-[10px] px-2.5 py-0.5 rounded-full bg-[#00e673]/10 border border-[#00e673]/20 text-[#00e673] font-bold">
1766
+ {compiledAudioMode === 'stream' ? 'Stream Mastered' : 'Engine Compiled'}
1767
+ </span>
1768
+ </div>
1769
+
1770
+ <div className="flex flex-col sm:flex-row items-center gap-4 justify-between bg-black/20 p-4 rounded-2xl border border-white/5 w-full">
1771
+ <div className="flex items-center gap-3">
1772
+ <div className="w-10 h-10 rounded-full bg-gradient-to-r from-[#00f2fe] to-[#a259ff] flex items-center justify-center text-black font-bold">
1773
+ <Volume2 className="w-5 h-5" />
1774
+ </div>
1775
+ <div className="text-left">
1776
+ <h4 className="text-xs font-semibold text-white">VibeVoice_Podcast_Session.wav</h4>
1777
+ <p className="text-[10px] text-[#8e9bb5]">
1778
+ {compiledAudioMode === 'stream' ? 'Real-time WebSocket Stream' : 'REST Compiled Dialogue Track'}
1779
+ </p>
1780
+ </div>
1781
+ </div>
1782
+
1783
+ <a
1784
+ href={compiledAudioUrl}
1785
+ download="VibeVoice_Podcast_Session.wav"
1786
+ className="w-full sm:w-auto flex items-center justify-center gap-2 py-2.5 px-5 bg-gradient-to-r from-[#00f2fe] to-[#a259ff] hover:shadow-[0_0_15px_rgba(0,242,254,0.3)] text-black font-bold rounded-xl text-xs transition duration-200 cursor-pointer active:scale-95"
1787
+ >
1788
+ <Upload className="w-4 h-4 rotate-180" />
1789
+ Export WAV File
1790
+ </a>
1791
+ </div>
1792
+ </div>
1793
+ )}
1794
+
1795
+ {/* Custom timeline bar player */}
1796
+ {showToolbar && (
1797
+ <div className="bg-glass p-4 rounded-3xl border border-[#00f2fe]/20 shadow-[0_0_20px_rgba(0,242,254,0.05)]">
1798
+ <div className="flex flex-col md:flex-row items-center gap-4 w-full">
1799
+ <button onClick={handleStopPlayback} className="w-9 h-9 rounded-full bg-white/5 border border-white/10 flex items-center justify-center text-white hover:bg-[#00f2fe] hover:text-black hover:shadow-[0_0_10px_rgba(0,242,254,0.35)] transition cursor-pointer">
1800
+ <Square className="w-4 h-4" />
1801
+ </button>
1802
+
1803
+ <div className="flex items-center gap-3 flex-1 w-full">
1804
+ <span className="font-mono text-[10px] text-[#8e9bb5]">{formatTime(timeline.elapsed)}</span>
1805
+ <div className="relative flex-1 h-1.5 bg-white/8 rounded-full overflow-hidden">
1806
+ <div className="absolute left-0 top-0 bottom-0 bg-gradient-to-r from-[#00f2fe] to-[#a259ff] rounded-full" style={{ width: `${(timeline.elapsed / timeline.total) * 100}%` }}></div>
1807
+ </div>
1808
+ <span className="font-mono text-[10px] text-[#8e9bb5]">{formatTime(timeline.total)}</span>
1809
+ </div>
1810
+
1811
+ <div className="flex items-center gap-3 w-[150px]">
1812
+ {volume === 0 ? <VolumeX className="w-4 h-4 text-[#8e9bb5]" /> : volume < 50 ? <Volume1 className="w-4 h-4 text-[#8e9bb5]" /> : <Volume2 className="w-4 h-4 text-[#8e9bb5]" />}
1813
+ <input
1814
+ type="range"
1815
+ min="0"
1816
+ max="100"
1817
+ value={volume}
1818
+ onChange={(e) => setVolume(parseInt(e.target.value))}
1819
+ className="w-full accent-[#00f2fe] h-1 bg-white/10 rounded-lg outline-none cursor-pointer"
1820
+ />
1821
+ </div>
1822
+
1823
+ <select
1824
+ value={speed}
1825
+ onChange={(e) => setSpeed(e.target.value)}
1826
+ className="bg-white/5 border border-white/8 rounded-lg px-2 py-1 text-xs text-white outline-none cursor-pointer"
1827
+ >
1828
+ <option value="0.75" className="bg-[#07090e]">0.75x</option>
1829
+ <option value="1.0" className="bg-[#07090e]">1.0x</option>
1830
+ <option value="1.25" className="bg-[#07090e]">1.25x</option>
1831
+ <option value="1.5" className="bg-[#07090e]">1.5x</option>
1832
+ </select>
1833
+ </div>
1834
+ </div>
1835
+ )}
1836
+
1837
+ {/* Podcast History & Archive section */}
1838
+ <div className="bg-glass p-6 rounded-3xl border border-white/4 bg-black/40 flex flex-col gap-4">
1839
+ <div className="flex justify-between items-center border-b border-white/5 pb-2 text-xs font-semibold text-[#8e9bb5]">
1840
+ <div className="flex items-center gap-2">
1841
+ <Podcast className="w-4 h-4 text-[#a259ff] animate-pulse" />
1842
+ <span className="text-white font-bold uppercase tracking-wider text-[10px]">Supabase Broadcast Archive</span>
1843
+ </div>
1844
+ <span className="text-[10px] bg-[#a259ff]/10 px-2 py-0.5 rounded-full text-neon-purple font-mono border border-[#a259ff]/20">
1845
+ {podcastHistory.length} Saved
1846
+ </span>
1847
+ </div>
1848
+
1849
+ <div className="max-h-[220px] overflow-y-auto pr-1 flex flex-col gap-2.5 custom-scrollbar">
1850
+ {podcastHistory.length === 0 ? (
1851
+ <div className="py-8 text-center flex flex-col items-center justify-center gap-2 border border-dashed border-white/5 rounded-2xl bg-white/2">
1852
+ <Music className="w-8 h-8 text-[#8e9bb5]/40" />
1853
+ <div>
1854
+ <p className="text-xs text-white font-medium">No podcasts archived yet</p>
1855
+ <p className="text-[10px] text-[#8e9bb5] mt-0.5 max-w-[200px]">Generate speech scripts to automatically archive them permanently!</p>
1856
+ </div>
1857
+ </div>
1858
+ ) : (
1859
+ podcastHistory.map((podcast) => (
1860
+ <div
1861
+ key={podcast.id}
1862
+ className="p-3 bg-white/3 border border-white/5 rounded-xl hover:border-white/10 hover:bg-white/5 transition duration-200 flex items-center justify-between gap-3 group/item relative overflow-hidden"
1863
+ >
1864
+ {isAudioPlayingState && compiledAudioUrl === podcast.audio_url && (
1865
+ <div className="absolute inset-0 bg-gradient-to-r from-[#a259ff]/10 to-transparent pointer-events-none" />
1866
+ )}
1867
+
1868
+ <div className="flex items-center gap-3 min-w-0">
1869
+ <button
1870
+ onClick={() => playHistoryPodcast(podcast)}
1871
+ className={`w-8 h-8 rounded-full flex items-center justify-center transition active:scale-95 cursor-pointer shrink-0 ${
1872
+ isAudioPlayingState && compiledAudioUrl === podcast.audio_url
1873
+ ? 'bg-[#ff5c8d] text-white animate-pulse'
1874
+ : 'bg-white/5 hover:bg-white/10 text-white'
1875
+ }`}
1876
+ >
1877
+ {isAudioPlayingState && compiledAudioUrl === podcast.audio_url ? (
1878
+ <Square className="w-3.5 h-3.5 fill-current" />
1879
+ ) : (
1880
+ <Play className="w-3.5 h-3.5 fill-current ml-0.5" />
1881
+ )}
1882
+ </button>
1883
+
1884
+ <div className="min-w-0 flex flex-col gap-0.5">
1885
+ <span className="text-xs font-bold text-white truncate max-w-[240px] block group-hover/item:text-[#00f2fe] transition duration-150">
1886
+ {podcast.title}
1887
+ </span>
1888
+ <span className="text-[10px] text-[#8e9bb5] truncate block max-w-[240px] leading-relaxed cursor-help" title={podcast.text_script}>
1889
+ Script: "{podcast.text_script}"
1890
+ </span>
1891
+ </div>
1892
+ </div>
1893
+
1894
+ <div className="flex items-center gap-2 shrink-0">
1895
+ <a
1896
+ href={podcast.audio_url}
1897
+ download
1898
+ target="_blank"
1899
+ rel="noopener noreferrer"
1900
+ className="p-1.5 rounded-lg border border-white/6 hover:border-[#00f2fe]/40 text-[#8e9bb5] hover:text-white hover:bg-white/4 active:scale-95 transition"
1901
+ title="Open audio in new tab / download"
1902
+ >
1903
+ <Upload className="w-3.5 h-3.5 rotate-180" />
1904
+ </a>
1905
+ <button
1906
+ onClick={() => openRenamePodcastModal(podcast.id, podcast.title)}
1907
+ className="p-1.5 rounded-lg border border-white/6 hover:border-[#00f2fe]/40 text-[#8e9bb5] hover:text-white hover:bg-white/4 active:scale-95 transition cursor-pointer"
1908
+ title="Rename podcast title"
1909
+ >
1910
+ <Edit2 className="w-3.5 h-3.5" />
1911
+ </button>
1912
+ <button
1913
+ onClick={() => openDeletePodcastModal(podcast.id, podcast.title, podcast.audio_url)}
1914
+ className="p-1.5 rounded-lg border border-white/6 hover:border-[#ff5c8d]/40 text-[#8e9bb5] hover:text-[#ff5c8d] hover:bg-[#ff5c8d]/5 active:scale-95 transition cursor-pointer"
1915
+ title="Delete podcast from Supabase archive"
1916
+ >
1917
+ <Trash2 className="w-3.5 h-3.5" />
1918
+ </button>
1919
+ </div>
1920
+ </div>
1921
+ ))
1922
+ )}
1923
+ </div>
1924
+ </div>
1925
+
1926
+ {/* Diagnostic Terminal card */}
1927
+ <div className="bg-glass p-6 rounded-3xl border border-white/4 bg-black/40">
1928
+ <div className="flex justify-between items-center border-b border-white/5 pb-2 mb-3 text-xs font-semibold text-[#8e9bb5]">
1929
+ <div className="flex items-center gap-2">
1930
+ <Terminal className="w-4 h-4 text-[#00f2fe]" />
1931
+ <span>Agent HUD Console Logs</span>
1932
+ </div>
1933
+ <button
1934
+ onClick={() => setLogs([{ type: 'system', text: 'Console buffer cleared.', time: new Date().toLocaleTimeString() }])}
1935
+ className="px-2 py-0.5 rounded border border-white/10 hover:border-white/20 active:scale-95 text-[10px] text-white hover:bg-white/2 cursor-pointer transition duration-150"
1936
+ >
1937
+ Clear Buffer
1938
+ </button>
1939
+ </div>
1940
+
1941
+ <div id="consoleBody" className="h-[120px] overflow-y-auto font-mono text-[10.5px] leading-relaxed flex flex-col gap-1.5 pr-2 select-text">
1942
+ {logs.map((logItem, idx) => (
1943
+ <div key={idx} className={`px-2 py-0.5 rounded ${
1944
+ logItem.type === 'system' && 'text-[#8e9bb5] bg-white/2'
1945
+ } ${
1946
+ logItem.type === 'info' && 'text-[#00f2fe] bg-[#00f2fe]/2'
1947
+ } ${
1948
+ logItem.type === 'success' && 'text-[#00e673] bg-[#00e673]/2'
1949
+ } ${
1950
+ logItem.type === 'error' && 'text-[#ff5c8d] bg-[#ff5c8d]/2'
1951
+ } ${
1952
+ logItem.type === 'word' && 'text-white font-medium drop-shadow-[0_0_4px_rgba(255,255,255,0.25)]'
1953
+ }`}>
1954
+ [{logItem.time}] {logItem.text}
1955
+ </div>
1956
+ ))}
1957
+ </div>
1958
+ </div>
1959
+
1960
+ </section>
1961
+ </main>
1962
+ </div>
1963
+
1964
+ {/* 🎙️ Premium Glassmorphic Custom Voice Profile Naming Modal */}
1965
+ {namingModal.isOpen && (
1966
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md animate-fade-in">
1967
+ <div className="w-[90%] max-w-[420px] bg-gradient-to-b from-white/10 to-white/3 border border-white/12 backdrop-blur-2xl p-6 rounded-3xl shadow-2xl flex flex-col gap-4 animate-scale-up">
1968
+ <div className="flex items-center gap-3">
1969
+ <div className="w-10 h-10 rounded-full bg-[#00f2fe]/10 flex items-center justify-center border border-[#00f2fe]/20">
1970
+ <Sparkles className="w-5 h-5 text-neon-cyan animate-pulse" />
1971
+ </div>
1972
+ <div>
1973
+ <h3 className="text-base font-bold text-white tracking-tight">Save Custom Voice Signature</h3>
1974
+ <p className="text-[10px] text-[#8e9bb5] uppercase tracking-wider">Neural Studio Archivist</p>
1975
+ </div>
1976
+ </div>
1977
+
1978
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
1979
+ Give your new voice profile signature a descriptive name (e.g. <em>John's Deep Voice</em>, <em>Excited Host</em>) to save it permanently in your custom voice selection.
1980
+ </p>
1981
+
1982
+ <div className="flex flex-col gap-1.5 mt-2">
1983
+ <label className="text-[10px] text-[#8e9bb5] uppercase tracking-wide font-semibold">Voice Signature Name</label>
1984
+ <input
1985
+ type="text"
1986
+ autoFocus
1987
+ placeholder="Enter voice signature name..."
1988
+ value={voiceNameInput}
1989
+ onChange={(e) => setVoiceNameInput(e.target.value)}
1990
+ className="bg-black/40 border border-white/10 focus:border-[#00f2fe] rounded-xl px-4 py-2.5 text-xs text-white outline-none focus:shadow-[0_0_12px_rgba(0,242,254,0.2)] transition duration-200"
1991
+ onKeyDown={(e) => {
1992
+ if (e.key === 'Enter') {
1993
+ const name = voiceNameInput.trim() || namingModal.defaultName;
1994
+ uploadVoiceProfile(namingModal.fileBlob, namingModal.speakerId, name);
1995
+ setNamingModal({ isOpen: false, fileBlob: null, speakerId: null, defaultName: '' });
1996
+ }
1997
+ }}
1998
+ />
1999
+ </div>
2000
+
2001
+ <div className="flex justify-end gap-3 mt-3">
2002
+ <button
2003
+ onClick={() => setNamingModal({ isOpen: false, fileBlob: null, speakerId: null, defaultName: '' })}
2004
+ className="px-4 py-2 rounded-xl border border-white/6 hover:bg-white/4 active:scale-95 text-xs font-semibold text-[#8e9bb5] hover:text-white cursor-pointer transition duration-150"
2005
+ >
2006
+ Cancel
2007
+ </button>
2008
+ <button
2009
+ onClick={() => {
2010
+ const name = voiceNameInput.trim() || namingModal.defaultName;
2011
+ uploadVoiceProfile(namingModal.fileBlob, namingModal.speakerId, name);
2012
+ setNamingModal({ isOpen: false, fileBlob: null, speakerId: null, defaultName: '' });
2013
+ }}
2014
+ className="px-5 py-2 rounded-xl bg-gradient-to-r from-[#00f2fe] to-[#a259ff] text-xs font-bold text-black hover:shadow-[0_0_15px_rgba(0,242,254,0.3)] active:scale-95 cursor-pointer transition duration-200"
2015
+ >
2016
+ Save to Studio
2017
+ </button>
2018
+ </div>
2019
+ </div>
2020
+ </div>
2021
+ )}
2022
+
2023
+ {/* 🔮 Premium custom action (Rename/Delete) modal */}
2024
+ {actionModal.isOpen && (
2025
+ <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-md animate-fade-in">
2026
+ <div className="w-[90%] max-w-[420px] bg-gradient-to-b from-white/10 to-white/3 border border-white/12 backdrop-blur-2xl p-6 rounded-3xl shadow-2xl flex flex-col gap-4 animate-scale-up">
2027
+ <div className="flex justify-between items-center border-b border-white/6 pb-3">
2028
+ <div className="flex items-center gap-2">
2029
+ <Sparkles className="w-5 h-5 text-neon-cyan" />
2030
+ <h3 className="text-sm font-bold text-white uppercase tracking-wider">
2031
+ {actionModal.type.startsWith('rename') ? 'Rename Profile' : 'Confirm Action'}
2032
+ </h3>
2033
+ </div>
2034
+ <button
2035
+ onClick={() => setActionModal({ isOpen: false, type: '', id: null, oldValue: '', inputValue: '', audioUrl: '', profileName: '' })}
2036
+ className="text-[#8e9bb5] hover:text-white transition cursor-pointer text-sm"
2037
+ >
2038
+
2039
+ </button>
2040
+ </div>
2041
+
2042
+ {actionModal.type.startsWith('rename') ? (
2043
+ <div className="flex flex-col gap-3">
2044
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
2045
+ Enter a new name for your {actionModal.type === 'rename_voice' ? 'voice profile signature' : 'podcast archive title'}:
2046
+ </p>
2047
+ <input
2048
+ type="text"
2049
+ autoFocus
2050
+ value={actionModal.inputValue}
2051
+ onChange={(e) => setActionModal(prev => ({ ...prev, inputValue: e.target.value }))}
2052
+ className="bg-black/40 border border-white/10 focus:border-[#00f2fe] rounded-xl px-4 py-2.5 text-xs text-white outline-none focus:shadow-[0_0_12px_rgba(0,242,254,0.2)] transition duration-200"
2053
+ onKeyDown={(e) => {
2054
+ if (e.key === 'Enter') {
2055
+ if (actionModal.type === 'rename_voice') {
2056
+ executeRenameVoice(actionModal.id, actionModal.inputValue, actionModal.audioUrl);
2057
+ } else {
2058
+ executeRenamePodcast(actionModal.id, actionModal.inputValue);
2059
+ }
2060
+ setActionModal({ isOpen: false, type: '', id: null, oldValue: '', inputValue: '', audioUrl: '', profileName: '' });
2061
+ }
2062
+ }}
2063
+ />
2064
+ <div className="flex items-center gap-2.5 mt-2">
2065
+ <button
2066
+ onClick={() => setActionModal({ isOpen: false, type: '', id: null, oldValue: '', inputValue: '', audioUrl: '', profileName: '' })}
2067
+ className="flex-1 py-2.5 text-xs font-bold bg-white/4 border border-white/5 hover:border-white/12 rounded-xl text-white transition active:scale-95 cursor-pointer"
2068
+ >
2069
+ Cancel
2070
+ </button>
2071
+ <button
2072
+ onClick={() => {
2073
+ if (actionModal.type === 'rename_voice') {
2074
+ executeRenameVoice(actionModal.id, actionModal.inputValue, actionModal.audioUrl);
2075
+ } else {
2076
+ executeRenamePodcast(actionModal.id, actionModal.inputValue);
2077
+ }
2078
+ setActionModal({ isOpen: false, type: '', id: null, oldValue: '', inputValue: '', audioUrl: '', profileName: '' });
2079
+ }}
2080
+ className="flex-1 py-2.5 text-xs font-bold bg-gradient-to-r from-[#00f2fe] to-[#a259ff] hover:shadow-[0_0_12px_rgba(0,242,254,0.3)] text-black rounded-xl transition active:scale-95 cursor-pointer"
2081
+ >
2082
+ Save Changes
2083
+ </button>
2084
+ </div>
2085
+ </div>
2086
+ ) : (
2087
+ <div className="flex flex-col gap-3">
2088
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
2089
+ Are you sure you want to permanently delete the {actionModal.type === 'delete_voice' ? 'voice profile signature' : 'archived podcast'} <strong className="text-white">"{actionModal.profileName}"</strong>? This action cannot be undone.
2090
+ </p>
2091
+ <div className="flex items-center gap-2.5 mt-2">
2092
+ <button
2093
+ onClick={() => setActionModal({ isOpen: false, type: '', id: null, oldValue: '', inputValue: '', audioUrl: '', profileName: '' })}
2094
+ className="flex-1 py-2.5 text-xs font-bold bg-white/4 border border-white/5 hover:border-white/12 rounded-xl text-white transition active:scale-95 cursor-pointer"
2095
+ >
2096
+ Cancel
2097
+ </button>
2098
+ <button
2099
+ onClick={() => {
2100
+ if (actionModal.type === 'delete_voice') {
2101
+ executeDeleteVoice(actionModal.id, actionModal.audioUrl, actionModal.profileName);
2102
+ } else {
2103
+ executeDeletePodcast(actionModal.id, actionModal.audioUrl, actionModal.profileName);
2104
+ }
2105
+ setActionModal({ isOpen: false, type: '', id: null, oldValue: '', inputValue: '', audioUrl: '', profileName: '' });
2106
+ }}
2107
+ className="flex-1 py-2.5 text-xs font-bold bg-[#ff5c8d] hover:bg-[#ff5c8d]/90 hover:shadow-[0_0_12px_rgba(255,92,141,0.3)] text-white rounded-xl transition active:scale-95 cursor-pointer"
2108
+ >
2109
+ Delete Permanently
2110
+ </button>
2111
+ </div>
2112
+ </div>
2113
+ )}
2114
+ </div>
2115
+ </div>
2116
+ )}
2117
+ </div>
2118
+ );
2119
+ }
2120
+
2121
+ // ==========================================================================
2122
+ // WAV Exporter Helpers
2123
+ // ==========================================================================
2124
+ function exportWav(chunks, sampleRate) {
2125
+ let totalLength = 0;
2126
+ for (const chunk of chunks) {
2127
+ totalLength += chunk.length;
2128
+ }
2129
+
2130
+ const result = new Float32Array(totalLength);
2131
+ let offset = 0;
2132
+ for (const chunk of chunks) {
2133
+ result.set(chunk, offset);
2134
+ offset += chunk.length;
2135
+ }
2136
+
2137
+ const buffer = new ArrayBuffer(44 + totalLength * 2);
2138
+ const view = new DataView(buffer);
2139
+
2140
+ // RIFF identifier
2141
+ writeString(view, 0, 'RIFF');
2142
+ // File length
2143
+ view.setUint32(4, 36 + totalLength * 2, true);
2144
+ // RIFF type
2145
+ writeString(view, 8, 'WAVE');
2146
+ // Format chunk identifier
2147
+ writeString(view, 12, 'fmt ');
2148
+ // Format chunk length
2149
+ view.setUint32(16, 16, true);
2150
+ // Sample format (raw PCM)
2151
+ view.setUint16(20, 1, true);
2152
+ // Channel count (mono)
2153
+ view.setUint16(22, 1, true);
2154
+ // Sample rate
2155
+ view.setUint32(24, sampleRate, true);
2156
+ // Byte rate (sample rate * block align)
2157
+ view.setUint32(28, sampleRate * 2, true);
2158
+ // Block align (channel count * bytes per sample)
2159
+ view.setUint16(32, 2, true);
2160
+ // Bits per sample
2161
+ view.setUint16(34, 16, true);
2162
+ // Data chunk identifier
2163
+ writeString(view, 36, 'data');
2164
+ // Data chunk length
2165
+ view.setUint32(40, totalLength * 2, true);
2166
+
2167
+ // Write PCM audio samples
2168
+ let index = 44;
2169
+ for (let i = 0; i < totalLength; i++) {
2170
+ const s = Math.max(-1, Math.min(1, result[i]));
2171
+ view.setInt16(index, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
2172
+ index += 2;
2173
+ }
2174
+
2175
+ return new Blob([view], { type: 'audio/wav' });
2176
+ }
2177
+
2178
+ function writeString(view, offset, string) {
2179
+ for (let i = 0; i < string.length; i++) {
2180
+ view.setUint8(offset + i, string.charCodeAt(i));
2181
+ }
2182
+ }
2183
+
2184
+ export default App;
frontend/src/LandingPage.jsx ADDED
@@ -0,0 +1,1148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ Sparkles,
5
+ ArrowRight,
6
+ Play,
7
+ Square,
8
+ Heart,
9
+ Cpu,
10
+ FileText,
11
+ ChevronDown,
12
+ Check,
13
+ Volume2,
14
+ MessageSquare,
15
+ Mic,
16
+ Disc,
17
+ Layers,
18
+ Globe,
19
+ Radio,
20
+ Headphones,
21
+ PlayCircle,
22
+ Settings,
23
+ Flame,
24
+ Volume1,
25
+ HelpCircle,
26
+ User,
27
+ ExternalLink,
28
+ BookOpen
29
+ } from 'lucide-react';
30
+
31
+ const Github = (props) => (
32
+ <svg viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={props.className || "w-5 h-5"}>
33
+ <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />
34
+ </svg>
35
+ );
36
+
37
+ export default function LandingPage({ onTryDemo }) {
38
+ // Collapsible FAQ Active Index
39
+ const [activeFaq, setActiveFaq] = useState(null);
40
+
41
+ // Mock Audio Player Demo State
42
+ const [mockIsPlaying, setMockIsPlaying] = useState(false);
43
+ const [mockActiveSpeaker, setMockActiveSpeaker] = useState(0);
44
+ const [mockTimeline, setMockTimeline] = useState(0);
45
+ const [selectedVoicePreset, setSelectedVoicePreset] = useState('podcast');
46
+ const canvasRef = useRef(null);
47
+ const animationFrameId = useRef(null);
48
+
49
+ // Scroll to section helper
50
+ const scrollToSection = (id) => {
51
+ const element = document.getElementById(id);
52
+ if (element) {
53
+ element.scrollIntoView({ behavior: 'smooth' });
54
+ }
55
+ };
56
+
57
+ // Mock Speech Dialogues matching preset
58
+ const voicePresets = {
59
+ podcast: {
60
+ title: "Co-Hosted Tech Podcast",
61
+ description: "Expressive debate with turn-taking and natural emphasis.",
62
+ duration: 12,
63
+ speakers: [
64
+ { id: 0, name: "David (AI Host)", color: "cyan" },
65
+ { id: 1, name: "Zira (AI Guest)", color: "purple" }
66
+ ],
67
+ script: [
68
+ { spk: 0, text: "Welcome back! Today we are discussing open-source conversational AI.", time: 0 },
69
+ { spk: 1, text: "Yes! And VibeVoice is achieving incredible zero-shot prosody matching.", time: 4 },
70
+ { spk: 0, text: "It runs in real-time, even on a basic CPU node.", time: 8 }
71
+ ]
72
+ },
73
+ audiobook: {
74
+ title: "Atmospheric Audiobook",
75
+ description: "Rich descriptive narration with dramatic pauses.",
76
+ duration: 15,
77
+ speakers: [
78
+ { id: 0, name: "Narrator", color: "amber" }
79
+ ],
80
+ script: [
81
+ { spk: 0, text: "The ancient model weights loaded slowly, breathing life into the synthesis.", time: 0 },
82
+ { spk: 0, text: "Every voice clone whispered of a future built on neural acoustic tokens...", time: 7 }
83
+ ]
84
+ },
85
+ assistant: {
86
+ title: "High-Speed Dynamic Support Agent",
87
+ description: "Polite, helpful customer transaction stream.",
88
+ duration: 10,
89
+ speakers: [
90
+ { id: 0, name: "VibeVoice Agent", color: "emerald" },
91
+ { id: 1, name: "User Alex", color: "rose" }
92
+ ],
93
+ script: [
94
+ { spk: 0, text: "Hello! I am VibeVoice AI. I can assist with your DineDirect order.", time: 0 },
95
+ { spk: 1, text: "Perfect. Add a dynamic voice reference for Speaker 2.", time: 4 },
96
+ { spk: 0, text: "Certainly! Setting up customized vocal signature parameters.", time: 7 }
97
+ ]
98
+ }
99
+ };
100
+
101
+ // Switch voice preset
102
+ useEffect(() => {
103
+ setMockIsPlaying(false);
104
+ setMockTimeline(0);
105
+ setMockActiveSpeaker(0);
106
+ }, [selectedVoicePreset]);
107
+
108
+ // Mock play timeline tick
109
+ useEffect(() => {
110
+ let interval;
111
+ if (mockIsPlaying) {
112
+ const activePreset = voicePresets[selectedVoicePreset];
113
+ interval = setInterval(() => {
114
+ setMockTimeline(prev => {
115
+ if (prev >= activePreset.duration) {
116
+ setMockIsPlaying(false);
117
+ return 0;
118
+ }
119
+ const nextVal = prev + 0.1;
120
+ // Determine active speaker based on time
121
+ const matchLine = [...activePreset.script].reverse().find(line => nextVal >= line.time);
122
+ if (matchLine && matchLine.spk !== mockActiveSpeaker) {
123
+ setMockActiveSpeaker(matchLine.spk);
124
+ }
125
+ return nextVal;
126
+ });
127
+ }, 100);
128
+ }
129
+ return () => clearInterval(interval);
130
+ }, [mockIsPlaying, selectedVoicePreset, mockActiveSpeaker]);
131
+
132
+ // Floating Particles or Voice Wave Animation for Visualizer Mock
133
+ useEffect(() => {
134
+ const canvas = canvasRef.current;
135
+ if (!canvas) return;
136
+ const ctx = canvas.getContext('2d');
137
+ const width = canvas.width;
138
+ const height = canvas.height;
139
+ let time = 0;
140
+
141
+ const render = () => {
142
+ time += 0.04;
143
+ ctx.fillStyle = 'rgba(7, 9, 14, 0.15)'; // Back-sweep fade
144
+ ctx.fillRect(0, 0, width, height);
145
+
146
+ // Multiple colored floating sin waves
147
+ const waves = [
148
+ { amplitude: mockIsPlaying ? 35 : 8, speed: 1.5, color: 'rgba(0, 242, 254, 0.35)', freq: 0.015 },
149
+ { amplitude: mockIsPlaying ? 25 : 6, speed: 2.2, color: 'rgba(162, 89, 255, 0.3)', freq: 0.02 },
150
+ { amplitude: mockIsPlaying ? 15 : 4, speed: -1.2, color: 'rgba(255, 92, 141, 0.25)', freq: 0.01 }
151
+ ];
152
+
153
+ waves.forEach(w => {
154
+ ctx.beginPath();
155
+ ctx.lineWidth = mockIsPlaying ? 2.5 : 1.5;
156
+ ctx.strokeStyle = w.color;
157
+ ctx.shadowBlur = mockIsPlaying ? 8 : 0;
158
+ ctx.shadowColor = w.color;
159
+
160
+ for (let x = 0; x < width; x++) {
161
+ const y = height / 2 + Math.sin(x * w.freq + time * w.speed) * w.amplitude * Math.sin(x / width * Math.PI);
162
+ if (x === 0) ctx.moveTo(x, y);
163
+ else ctx.lineTo(x, y);
164
+ }
165
+ ctx.stroke();
166
+ });
167
+
168
+ // Animated voice bars on left and right borders
169
+ if (mockIsPlaying) {
170
+ ctx.fillStyle = 'rgba(0, 242, 254, 0.15)';
171
+ for (let i = 0; i < 15; i++) {
172
+ const barHeight = Math.abs(Math.sin(time * 3 + i)) * 40;
173
+ ctx.fillRect(15 + i * 8, height - barHeight - 10, 5, barHeight);
174
+ ctx.fillRect(width - 15 - i * 8 - 5, height - barHeight - 10, 5, barHeight);
175
+ }
176
+ }
177
+
178
+ animationFrameId.current = requestAnimationFrame(render);
179
+ };
180
+
181
+ render();
182
+ return () => cancelAnimationFrame(animationFrameId.current);
183
+ }, [mockIsPlaying]);
184
+
185
+ // Framer motion containers configs
186
+ const containerVariants = {
187
+ hidden: { opacity: 0 },
188
+ visible: { opacity: 1, transition: { staggerChildren: 0.15 } }
189
+ };
190
+
191
+ const itemVariants = {
192
+ hidden: { opacity: 0, y: 30 },
193
+ visible: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 60 } }
194
+ };
195
+
196
+ // FAQ List
197
+ const faqs = [
198
+ {
199
+ q: "What is VibeVoice AI?",
200
+ a: "VibeVoice AI is an advanced, research-grade open-source neural acoustic speech model developed by Microsoft Research. It leverages multi-turn continuous speaker tokenization to generate hyper-realistic, human-like voice synthesis and dialogue streams without requiring massive GPU cluster infrastructures."
201
+ },
202
+ {
203
+ q: "Is it completely open source?",
204
+ a: "Yes, VibeVoice is fully open source. All model architectures, inference engines, and base neural weights (including the lightweight 0.5B parameter variant) are accessible on our GitHub repository and Hugging Face space for developers, researchers, and synthetic media creators."
205
+ },
206
+ {
207
+ q: "Does it support multi-speaker dialogue synthesis?",
208
+ a: "Absolutely. Rather than just speaking single phrases, VibeVoice excels at context-aware turn-taking. You can write scripts detailing conversations for two or more speakers, and the model handles transitions, conversational pauses, dynamic pacing, and speech emphasis with extreme stability."
209
+ },
210
+ {
211
+ q: "Can it generate full podcast discussions?",
212
+ a: "Yes. By providing a scripted conversation template containing Speaker IDs (e.g. Speaker 0, Speaker 1), the engine aggregates dialogue cues to model natural flow, interruptions, and pacing, producing broadcast-ready podcast tracks."
213
+ },
214
+ {
215
+ q: "Does it support emotional speech synthesis and voice cloning?",
216
+ a: "Yes. VibeVoice utilizes zero-shot speaker adaptation. By capturing or uploading a brief 10-30 second reference WAV file, it extracts pitch, vocal envelope, and prosody parameters. The model then synthesizes conversational speech matching the original speaker's vocal signature and dynamic emotional tone."
217
+ },
218
+ {
219
+ q: "Can developers fine-tune the model weights?",
220
+ a: "Yes! The repository contains full PyTorch training recipes, quantization parameters, and Hugging Face pipeline wrappers. You can fine-tune VibeVoice on custom speech datasets or scale it in commercial server clouds utilizing GPU-optimized TensorRT containers."
221
+ },
222
+ {
223
+ q: "Is it suitable for research projects?",
224
+ a: "VibeVoice was built from the ground up to support academic and commercial speech research. The architecture decouples text tokenization and acoustic prosody representation, offering researchers a clean pipeline to inspect neural audio modeling."
225
+ }
226
+ ];
227
+
228
+ return (
229
+ <div className="min-h-screen text-[#f0f3fa] selection:bg-[#00f2fe]/20 relative overflow-x-hidden font-sans">
230
+
231
+ {/* Dynamic Glowing Parallax Backdrop Background */}
232
+ <div className="fixed inset-0 overflow-hidden pointer-events-none z-0">
233
+ <div className="absolute w-[800px] h-[800px] rounded-full top-[-300px] left-[-200px] bg-gradient-to-br from-[#00f2fe]/10 to-[#a259ff]/5 blur-[160px] animate-float-glow-1"></div>
234
+ <div className="absolute w-[900px] h-[900px] rounded-full bottom-[-300px] right-[-200px] bg-gradient-to-br from-[#ff5c8d]/10 to-[#a259ff]/5 blur-[180px] animate-float-glow-2"></div>
235
+ <div className="absolute w-[600px] h-[600px] rounded-full top-[40%] right-[10%] bg-gradient-to-br from-[#00f2fe]/5 to-[#ff5c8d]/5 blur-[150px] animate-[floatGlow_35s_infinite_alternate_ease-in-out]"></div>
236
+ </div>
237
+
238
+ {/* Modern Glassmorphic Sticky Navigation */}
239
+ <header className="sticky top-0 z-50 bg-glass/60 backdrop-blur-xl border-b border-white/5 py-4">
240
+ <div className="max-w-[1300px] mx-auto px-6 flex justify-between items-center">
241
+ <div className="flex items-center gap-3 cursor-pointer" onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
242
+ <Sparkles className="w-8 h-8 text-neon-cyan drop-shadow-[0_0_10px_rgba(0,242,254,0.4)]" />
243
+ <div>
244
+ <span className="text-xl font-bold tracking-tight">
245
+ VibeVoice<span className="bg-gradient-to-r from-[#00f2fe] to-[#a259ff] bg-clip-text text-transparent">AI</span>
246
+ </span>
247
+ <span className="block text-[8.5px] text-[#8e9bb5] uppercase tracking-widest font-semibold">Microsoft Research Ecosystem</span>
248
+ </div>
249
+ </div>
250
+
251
+ {/* Desktop Menu links */}
252
+ <nav className="hidden md:flex items-center gap-8 text-xs font-semibold text-[#8e9bb5]">
253
+ <button onClick={() => scrollToSection('about')} className="hover:text-white hover:text-shadow transition duration-200 cursor-pointer">About</button>
254
+ <button onClick={() => scrollToSection('features')} className="hover:text-white hover:text-shadow transition duration-200 cursor-pointer">Features</button>
255
+ <button onClick={() => scrollToSection('capabilities')} className="hover:text-white hover:text-shadow transition duration-200 cursor-pointer">Capabilities</button>
256
+ <button onClick={() => scrollToSection('demo')} className="hover:text-white hover:text-shadow transition duration-200 cursor-pointer">Interactive Demo</button>
257
+ <button onClick={() => scrollToSection('open-source')} className="hover:text-white hover:text-shadow transition duration-200 cursor-pointer">Open Source</button>
258
+ <button onClick={() => scrollToSection('faq')} className="hover:text-white hover:text-shadow transition duration-200 cursor-pointer">FAQs</button>
259
+ </nav>
260
+
261
+ <div className="flex items-center gap-3">
262
+ <a
263
+ href="https://github.com"
264
+ target="_blank"
265
+ rel="noopener noreferrer"
266
+ className="p-2 bg-white/4 border border-white/5 hover:border-[#00f2fe]/40 rounded-xl hover:bg-white/8 transition duration-200 cursor-pointer"
267
+ >
268
+ <Github className="w-4 h-4 text-white" />
269
+ </a>
270
+ <button
271
+ onClick={onTryDemo}
272
+ className="relative group overflow-hidden px-4.5 py-2 rounded-xl bg-gradient-to-r from-[#00f2fe] to-[#a259ff] text-black font-bold text-xs transition duration-200 cursor-pointer active:scale-95 shadow-[0_4px_15px_rgba(0,242,254,0.3)] hover:shadow-[0_4px_25px_rgba(0,242,254,0.5)]"
273
+ >
274
+ Launch Studio Workspace
275
+ </button>
276
+ </div>
277
+ </div>
278
+ </header>
279
+
280
+ {/* 1. HERO SECTION */}
281
+ <section className="relative z-10 pt-16 md:pt-24 pb-20 max-w-[1300px] mx-auto px-6 grid grid-cols-1 lg:grid-cols-[1.1fr_0.9fr] gap-12 items-center">
282
+ <motion.div
283
+ initial={{ opacity: 0, x: -50 }}
284
+ animate={{ opacity: 1, x: 0 }}
285
+ transition={{ duration: 0.8, ease: "easeOut" }}
286
+ className="flex flex-col gap-6 text-left"
287
+ >
288
+ {/* Neon Ribbon Badge */}
289
+ <div className="inline-flex items-center gap-2 self-start bg-[#00f2fe]/10 border border-[#00f2fe]/30 rounded-full px-4 py-1.5 text-xs text-neon-cyan select-none">
290
+ <Radio className="w-3.5 h-3.5 animate-pulse text-neon-cyan" />
291
+ <span className="font-bold uppercase tracking-wider text-[10px]">Active Open-Source Model: VibeVoice v1.5</span>
292
+ </div>
293
+
294
+ <h1 className="text-5xl md:text-7xl font-extrabold tracking-tight leading-[1.08] text-white">
295
+ VibeVoice <span className="bg-gradient-to-r from-[#00f2fe] via-[#a259ff] to-[#ff5c8d] bg-clip-text text-transparent drop-shadow-[0_0_20px_rgba(0,242,254,0.15)]">AI</span>
296
+ </h1>
297
+
298
+ <p className="text-lg md:text-xl font-medium text-[#8e9bb5] max-w-[550px] leading-relaxed">
299
+ Human-like conversational speech generation powered by AI. Experience the future of multi-speaker dialogue and expressive zero-shot voice synthesis.
300
+ </p>
301
+
302
+ <div className="flex flex-col sm:flex-row gap-4 pt-3">
303
+ <button
304
+ onClick={onTryDemo}
305
+ className="flex items-center justify-center gap-2.5 px-7 py-4.5 rounded-xl bg-gradient-to-r from-[#00f2fe] to-[#a259ff] text-black font-extrabold text-sm transition duration-200 cursor-pointer hover:shadow-[0_0_25px_rgba(0,242,254,0.45)] active:scale-95 group"
306
+ >
307
+ Try Studio Playground
308
+ <ArrowRight className="w-4 h-4 group-hover:translate-x-1.5 transition-transform duration-200" />
309
+ </button>
310
+ <a
311
+ href="https://github.com"
312
+ target="_blank"
313
+ rel="noopener noreferrer"
314
+ className="flex items-center justify-center gap-2.5 px-7 py-4.5 rounded-xl bg-white/4 border border-white/8 hover:bg-white/8 hover:border-white/15 text-white font-bold text-sm transition duration-200 cursor-pointer active:scale-95"
315
+ >
316
+ <Github className="w-5 h-5" />
317
+ View on GitHub
318
+ </a>
319
+ </div>
320
+
321
+ {/* Feature highlights inside Hero */}
322
+ <div className="grid grid-cols-3 gap-6 pt-10 border-t border-white/5 max-w-[520px]">
323
+ <div>
324
+ <h4 className="text-2xl font-extrabold text-white">24kHz</h4>
325
+ <p className="text-[10px] text-[#8e9bb5] uppercase tracking-wide">High-Fidelity Audio</p>
326
+ </div>
327
+ <div>
328
+ <h4 className="text-2xl font-extrabold text-white">&lt; 250ms</h4>
329
+ <p className="text-[10px] text-[#8e9bb5] uppercase tracking-wide">Dynamic Latency</p>
330
+ </div>
331
+ <div>
332
+ <h4 className="text-2xl font-extrabold text-white">1.5B</h4>
333
+ <p className="text-[10px] text-[#8e9bb5] uppercase tracking-wide">Quantized weights</p>
334
+ </div>
335
+ </div>
336
+ </motion.div>
337
+
338
+ {/* Hero Visualizer Graphic */}
339
+ <motion.div
340
+ initial={{ opacity: 0, scale: 0.9, y: 30 }}
341
+ animate={{ opacity: 1, scale: 1, y: 0 }}
342
+ transition={{ duration: 1, ease: "easeOut" }}
343
+ className="relative flex justify-center items-center"
344
+ >
345
+ {/* Ambient Background Circles */}
346
+ <div className="absolute w-[350px] h-[350px] rounded-full border border-dashed border-[#00f2fe]/20 animate-[orbSpin_40s_infinite_linear]"></div>
347
+ <div className="absolute w-[450px] h-[450px] rounded-full border border-dashed border-[#a259ff]/10 animate-[orbSpin_60s_infinite_linear_reverse]"></div>
348
+
349
+ {/* Central AI Orb Graphic */}
350
+ <div className="relative w-[280px] h-[280px] rounded-full bg-glass flex items-center justify-center border border-white/10 shadow-3xl">
351
+ {/* Spinning Holographic Waves */}
352
+ <div className="absolute inset-4 rounded-full bg-gradient-to-tr from-[#00f2fe]/20 to-[#a259ff]/20 animate-[orbSpin_12s_infinite_linear]"></div>
353
+ <div className="absolute inset-8 rounded-full bg-gradient-to-br from-[#ff5c8d]/15 to-[#a259ff]/10 animate-[orbSpin_8s_infinite_linear_reverse]"></div>
354
+
355
+ {/* Pulsating core */}
356
+ <div className="w-[120px] h-[120px] rounded-full bg-gradient-to-tr from-[#00f2fe] via-[#a259ff] to-[#ff5c8d] shadow-[0_0_40px_rgba(0,242,254,0.45)] animate-pulse flex items-center justify-center relative">
357
+ <Headphones className="w-12 h-12 text-black" />
358
+ </div>
359
+
360
+ {/* Glowing satellites */}
361
+ <div className="absolute top-[10%] left-[10%] w-6 h-6 rounded-full bg-[#00f2fe]/80 blur-[4px] animate-pulse"></div>
362
+ <div className="absolute bottom-[15%] right-[12%] w-8 h-8 rounded-full bg-[#ff5c8d]/60 blur-[6px] animate-pulse"></div>
363
+ </div>
364
+
365
+ {/* Floating UI Audio Indicators */}
366
+ <div className="absolute top-[15%] right-[5%] bg-glass p-3.5 rounded-2xl border border-white/10 flex items-center gap-2.5 shadow-2xl animate-[floatGlow_20s_infinite_alternate_ease-in-out]">
367
+ <Mic className="w-4 h-4 text-[#00f2fe]" />
368
+ <div className="text-left">
369
+ <span className="block text-[9px] text-[#8e9bb5] uppercase font-bold tracking-wide">Cloned Vocals</span>
370
+ <span className="text-[11px] font-bold text-white">David_Signature.wav</span>
371
+ </div>
372
+ </div>
373
+
374
+ <div className="absolute bottom-[20%] left-[0%] bg-glass p-3.5 rounded-2xl border border-white/10 flex items-center gap-2.5 shadow-2xl animate-[floatGlow_22s_infinite_alternate-reverse_ease-in-out]">
375
+ <Radio className="w-4 h-4 text-[#a259ff] animate-pulse" />
376
+ <div className="text-left">
377
+ <span className="block text-[9px] text-[#8e9bb5] uppercase font-bold tracking-wide">Stream Broadcast</span>
378
+ <span className="text-[11px] font-bold text-[#00e673]">Active turn-taking</span>
379
+ </div>
380
+ </div>
381
+ </motion.div>
382
+ </section>
383
+
384
+ {/* 2. ABOUT SECTION */}
385
+ <section id="about" className="relative z-10 py-24 border-t border-white/5 max-w-[1300px] mx-auto px-6">
386
+ <motion.div
387
+ initial="hidden"
388
+ whileInView="visible"
389
+ viewport={{ once: true, margin: "-100px" }}
390
+ variants={containerVariants}
391
+ className="grid grid-cols-1 lg:grid-cols-[0.8fr_1.2fr] gap-12 items-center"
392
+ >
393
+ <motion.div variants={itemVariants} className="flex flex-col gap-5 text-left">
394
+ <span className="text-xs font-bold uppercase tracking-widest text-neon-cyan">The Research Foundation</span>
395
+ <h2 className="text-3xl md:text-4xl font-extrabold text-white leading-tight">
396
+ A Leap Forward in Expressive Speech Synthesis
397
+ </h2>
398
+ <p className="text-sm text-[#8e9bb5] leading-relaxed">
399
+ VibeVoice is an advanced open-source AI voice model developed to bridge the gap between flat, robotic speech-to-text outputs and natural, multi-speaker conversational dialogue.
400
+ </p>
401
+ <div className="flex gap-4 pt-2">
402
+ <a
403
+ href="https://huggingface.co"
404
+ target="_blank"
405
+ rel="noopener noreferrer"
406
+ className="flex items-center gap-2 text-xs font-bold text-white hover:text-neon-cyan transition duration-200"
407
+ >
408
+ Hugging Face Portal <ExternalLink className="w-3.5 h-3.5" />
409
+ </a>
410
+ <a
411
+ href="https://github.com"
412
+ target="_blank"
413
+ rel="noopener noreferrer"
414
+ className="flex items-center gap-2 text-xs font-bold text-white hover:text-neon-purple transition duration-200"
415
+ >
416
+ Developer Docs <BookOpen className="w-3.5 h-3.5" />
417
+ </a>
418
+ </div>
419
+ </motion.div>
420
+
421
+ <motion.div variants={itemVariants} className="grid grid-cols-1 md:grid-cols-2 gap-6 text-left">
422
+ {/* About capability cards */}
423
+ <div className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-white/10 transition duration-300">
424
+ <div className="w-10 h-10 rounded-xl bg-[#00f2fe]/10 flex items-center justify-center text-neon-cyan mb-4">
425
+ <Layers className="w-5 h-5" />
426
+ </div>
427
+ <h3 className="text-md font-semibold text-white mb-2">Zero-Shot Cloning</h3>
428
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
429
+ By processing a brief, 10-second reference voice WAV, the network maps core vocal footprints to synthesize entirely new dialogue styles instantly.
430
+ </p>
431
+ </div>
432
+
433
+ <div className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-white/10 transition duration-300">
434
+ <div className="w-10 h-10 rounded-xl bg-[#a259ff]/10 flex items-center justify-center text-neon-purple mb-4">
435
+ <MessageSquare className="w-5 h-5" />
436
+ </div>
437
+ <h3 className="text-md font-semibold text-white mb-2">Multi-Speaker Continuity</h3>
438
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
439
+ Models turn-taking triggers, pauses, and context queues so speakers flow smoothly between paragraphs, simulating authentic studio atmospheres.
440
+ </p>
441
+ </div>
442
+
443
+ <div className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-white/10 transition duration-300">
444
+ <div className="w-10 h-10 rounded-xl bg-[#ff5c8d]/10 flex items-center justify-center text-[#ff5c8d] mb-4">
445
+ <Cpu className="w-5 h-5" />
446
+ </div>
447
+ <h3 className="text-md font-semibold text-white mb-2">Research-Grade Architecture</h3>
448
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
449
+ Decoupled neural acoustic tokenizers allow academic inspection and dynamic speed parameter tweaking for multi-agent experiments.
450
+ </p>
451
+ </div>
452
+
453
+ <div className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-white/10 transition duration-300">
454
+ <div className="w-10 h-10 rounded-xl bg-[#00e673]/10 flex items-center justify-center text-[#00e673] mb-4">
455
+ <Globe className="w-5 h-5" />
456
+ </div>
457
+ <h3 className="text-md font-semibold text-white mb-2">Open Ecosystem</h3>
458
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
459
+ Seamless pipeline wrappers for Hugging Face datasets and local PyTorch containers to build production speech layers.
460
+ </p>
461
+ </div>
462
+ </motion.div>
463
+ </motion.div>
464
+ </section>
465
+
466
+ {/* 3. FEATURES SECTION */}
467
+ <section id="features" className="relative z-10 py-24 bg-black/30 border-t border-white/5 max-w-[1300px] mx-auto px-6">
468
+ <div className="text-center max-w-[700px] mx-auto mb-16 flex flex-col gap-3">
469
+ <span className="text-xs font-bold uppercase tracking-widest text-[#a259ff]">Enterprise SaaS Features</span>
470
+ <h2 className="text-3xl md:text-5xl font-extrabold text-white">Synthesize Without Limits</h2>
471
+ <p className="text-sm text-[#8e9bb5] leading-relaxed">
472
+ From co-hosted podcasts to responsive gaming characters, VibeVoice AI bundles powerful acoustic capabilities out-of-the-box.
473
+ </p>
474
+ </div>
475
+
476
+ <motion.div
477
+ initial="hidden"
478
+ whileInView="visible"
479
+ viewport={{ once: true, margin: "-100px" }}
480
+ variants={containerVariants}
481
+ className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
482
+ >
483
+ {/* Feature Card A */}
484
+ <motion.div variants={itemVariants} className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-[#00f2fe]/40 hover:shadow-[0_0_20px_rgba(0,242,254,0.1)] transition duration-300 text-left group">
485
+ <div className="w-10 h-10 rounded-xl bg-[#00f2fe]/10 flex items-center justify-center text-neon-cyan mb-4 group-hover:scale-110 transition duration-300">
486
+ <Mic className="w-5 h-5" />
487
+ </div>
488
+ <h3 className="text-md font-bold text-white mb-2">Multi-Speaker Dialogue</h3>
489
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
490
+ Generate natural conversations between multiple AI speakers. Handles realistic speaker transitions and context-aware continuity automatically.
491
+ </p>
492
+ </motion.div>
493
+
494
+ {/* Feature Card B */}
495
+ <motion.div variants={itemVariants} className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-[#a259ff]/40 hover:shadow-[0_0_20px_rgba(162,89,255,0.1)] transition duration-300 text-left group">
496
+ <div className="w-10 h-10 rounded-xl bg-[#a259ff]/10 flex items-center justify-center text-neon-purple mb-4 group-hover:scale-110 transition duration-300">
497
+ <Headphones className="w-5 h-5" />
498
+ </div>
499
+ <h3 className="text-md font-bold text-white mb-2">AI Podcast Generation</h3>
500
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
501
+ Automatically create complete podcast-style discussions with human-like speaking flow, dynamic pacing, and interactive turn exchanges.
502
+ </p>
503
+ </motion.div>
504
+
505
+ {/* Feature Card C */}
506
+ <motion.div variants={itemVariants} className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-[#ff5c8d]/40 hover:shadow-[0_0_20px_rgba(255,92,141,0.1)] transition duration-300 text-left group">
507
+ <div className="w-10 h-10 rounded-xl bg-[#ff5c8d]/10 flex items-center justify-center text-[#ff5c8d] mb-4 group-hover:scale-110 transition duration-300">
508
+ <Volume2 className="w-5 h-5" />
509
+ </div>
510
+ <h3 className="text-md font-bold text-white mb-2">Expressive Text-to-Speech</h3>
511
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
512
+ Emotion-aware speech synthesis capturing raw tone, breathing rhythm, natural pauses, and precise vocal emphasis.
513
+ </p>
514
+ </motion.div>
515
+
516
+ {/* Feature Card D */}
517
+ <motion.div variants={itemVariants} className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-[#00e673]/40 hover:shadow-[0_0_20px_rgba(0,230,115,0.1)] transition duration-300 text-left group">
518
+ <div className="w-10 h-10 rounded-xl bg-[#00e673]/10 flex items-center justify-center text-[#00e673] mb-4 group-hover:scale-110 transition duration-300">
519
+ <Disc className="w-5 h-5" />
520
+ </div>
521
+ <h3 className="text-md font-bold text-white mb-2">Long-Form Audio</h3>
522
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
523
+ Stable long-duration speech generation that maintains voice quality consistency and active conversation memory across chapters.
524
+ </p>
525
+ </motion.div>
526
+
527
+ {/* Feature Card E */}
528
+ <motion.div variants={itemVariants} className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-[#ffb703]/40 hover:shadow-[0_0_20px_rgba(255,183,3,0.1)] transition duration-300 text-left group">
529
+ <div className="w-10 h-10 rounded-xl bg-[#ffb703]/10 flex items-center justify-center text-[#ffb703] mb-4 group-hover:scale-110 transition duration-300">
530
+ <Settings className="w-5 h-5" />
531
+ </div>
532
+ <h3 className="text-md font-bold text-white mb-2">Voice Style Transfer</h3>
533
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
534
+ Vocal adaptation matching target speaker style, footprint imitation, and fast personalized AI voice mapping.
535
+ </p>
536
+ </motion.div>
537
+
538
+ {/* Feature Card F */}
539
+ <motion.div variants={itemVariants} className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-[#00f2fe]/40 hover:shadow-[0_0_20px_rgba(0,242,254,0.1)] transition duration-300 text-left group">
540
+ <div className="w-10 h-10 rounded-xl bg-[#00f2fe]/10 flex items-center justify-center text-neon-cyan mb-4 group-hover:scale-110 transition duration-300">
541
+ <Radio className="w-5 h-5" />
542
+ </div>
543
+ <h3 className="text-md font-bold text-white mb-2">Conversational AI Speech</h3>
544
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
545
+ Human-like interaction flow, clean turn-taking logic, and advanced prosody modeling for responsive AI web support.
546
+ </p>
547
+ </motion.div>
548
+
549
+ {/* Feature Card G */}
550
+ <motion.div variants={itemVariants} className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-[#a259ff]/40 hover:shadow-[0_0_20px_rgba(162,89,255,0.1)] transition duration-300 text-left group">
551
+ <div className="w-10 h-10 rounded-xl bg-[#a259ff]/10 flex items-center justify-center text-neon-purple mb-4 group-hover:scale-110 transition duration-300">
552
+ <Github className="w-5 h-5" />
553
+ </div>
554
+ <h3 className="text-md font-bold text-white mb-2">Open Source Ecosystem</h3>
555
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
556
+ First-class Hugging Face model hub integration, comprehensive GitHub community repositories, and developer API templates.
557
+ </p>
558
+ </motion.div>
559
+
560
+ {/* Feature Card H */}
561
+ <motion.div variants={itemVariants} className="bg-glass p-6 rounded-2xl border border-white/5 hover:border-[#ff5c8d]/40 hover:shadow-[0_0_20px_rgba(255,92,141,0.1)] transition duration-300 text-left group">
562
+ <div className="w-10 h-10 rounded-xl bg-[#ff5c8d]/10 flex items-center justify-center text-[#ff5c8d] mb-4 group-hover:scale-110 transition duration-300">
563
+ <Cpu className="w-5 h-5" />
564
+ </div>
565
+ <h3 className="text-md font-bold text-white mb-2">Research-Grade Tech</h3>
566
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
567
+ Designed from first principles for conversational research, scalable local inference workloads, and custom quantization setups.
568
+ </p>
569
+ </motion.div>
570
+ </motion.div>
571
+ </section>
572
+
573
+ {/* 4. REAL WORLD USE CASES SECTION */}
574
+ <section id="cases" className="relative z-10 py-24 border-t border-white/5 max-w-[1300px] mx-auto px-6">
575
+ <div className="text-center max-w-[700px] mx-auto mb-16 flex flex-col gap-3">
576
+ <span className="text-xs font-bold uppercase tracking-widest text-[#00e673]">Infinite Possibilities</span>
577
+ <h2 className="text-3xl md:text-5xl font-extrabold text-white">Built for Creators & Engineers</h2>
578
+ <p className="text-sm text-[#8e9bb5] leading-relaxed">
579
+ Discover how product squads, creative developers, and researchers deploy VibeVoice AI in the wild.
580
+ </p>
581
+ </div>
582
+
583
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
584
+ {[
585
+ { title: "AI Podcast Automation", icon: Headphones, color: "cyan" },
586
+ { title: "Audiobook Narration", icon: BookOpen, color: "purple" },
587
+ { title: "Virtual AI Hosts", icon: Sparkles, color: "emerald" },
588
+ { title: "Game Character Voices", icon: Flame, color: "rose" },
589
+ { title: "AI YouTube Narration", icon: PlayCircle, color: "amber" },
590
+ { title: "Conversational Agents", icon: MessageSquare, color: "blue" },
591
+ { title: "Educational Voice", icon: User, color: "cyan" },
592
+ { title: "Synthetic Media", icon: Radio, color: "purple" },
593
+ { title: "Voice Cloning Research", icon: Mic, color: "rose" },
594
+ { title: "Multilingual Dialogue", icon: Globe, color: "emerald" }
595
+ ].map((item, idx) => {
596
+ const IconComponent = item.icon;
597
+ const borderColors = item.color === 'cyan' ? 'hover:border-[#00f2fe]/40'
598
+ : item.color === 'purple' ? 'hover:border-[#a259ff]/40'
599
+ : item.color === 'emerald' ? 'hover:border-[#00e673]/40'
600
+ : item.color === 'rose' ? 'hover:border-[#ff5c8d]/40'
601
+ : 'hover:border-white/20';
602
+
603
+ return (
604
+ <motion.div
605
+ key={idx}
606
+ whileHover={{ scale: 1.04, y: -4 }}
607
+ transition={{ type: "spring", stiffness: 200, damping: 10 }}
608
+ className={`bg-glass p-5 rounded-2xl border border-white/5 cursor-pointer flex flex-col items-center justify-center text-center gap-3 ${borderColors}`}
609
+ >
610
+ <div className={`w-9 h-9 rounded-xl bg-white/4 flex items-center justify-center text-white`}>
611
+ <IconComponent className="w-4.5 h-4.5" />
612
+ </div>
613
+ <span className="text-xs font-semibold text-white tracking-tight">{item.title}</span>
614
+ </motion.div>
615
+ );
616
+ })}
617
+ </div>
618
+ </section>
619
+
620
+ {/* 5. HOW IT WORKS SECTION */}
621
+ <section className="relative z-10 py-24 bg-black/40 border-t border-white/5 max-w-[1300px] mx-auto px-6">
622
+ <div className="text-center max-w-[700px] mx-auto mb-16 flex flex-col gap-3">
623
+ <span className="text-xs font-bold uppercase tracking-widest text-[#ff5c8d]">Simplifying Complexity</span>
624
+ <h2 className="text-3xl md:text-5xl font-extrabold text-white">How It Works</h2>
625
+ <p className="text-sm text-[#8e9bb5] leading-relaxed">
626
+ Generate customized, context-aware speech dialogue files locally in three simple milestones.
627
+ </p>
628
+ </div>
629
+
630
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 relative">
631
+
632
+ {/* Animated linking lines inside background */}
633
+ <div className="hidden lg:block absolute top-[30%] left-[25%] right-[25%] h-0.5 bg-gradient-to-r from-[#00f2fe]/35 via-[#a259ff]/35 to-[#ff5c8d]/35 z-0"></div>
634
+
635
+ {/* Step 1 */}
636
+ <motion.div
637
+ initial={{ opacity: 0, y: 30 }}
638
+ whileInView={{ opacity: 1, y: 0 }}
639
+ viewport={{ once: true }}
640
+ transition={{ duration: 0.6 }}
641
+ className="bg-glass p-8 rounded-3xl border border-white/5 text-left relative z-10 flex flex-col gap-4"
642
+ >
643
+ <div className="absolute top-4 right-6 text-5xl font-black text-white/5">01</div>
644
+ <div className="w-12 h-12 rounded-2xl bg-[#00f2fe]/10 flex items-center justify-center text-neon-cyan font-bold">
645
+ <FileText className="w-6 h-6" />
646
+ </div>
647
+ <h3 className="text-lg font-bold text-white">Input Text &amp; Script</h3>
648
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
649
+ Format your text using simple speaker tags (e.g., `Speaker 0`, `Speaker 1`). Paste dialogue script snippets directly into the studio box.
650
+ </p>
651
+ </motion.div>
652
+
653
+ {/* Step 2 */}
654
+ <motion.div
655
+ initial={{ opacity: 0, y: 30 }}
656
+ whileInView={{ opacity: 1, y: 0 }}
657
+ viewport={{ once: true }}
658
+ transition={{ duration: 0.6, delay: 0.2 }}
659
+ className="bg-glass p-8 rounded-3xl border border-white/5 text-left relative z-10 flex flex-col gap-4"
660
+ >
661
+ <div className="absolute top-4 right-6 text-5xl font-black text-white/5">02</div>
662
+ <div className="w-12 h-12 rounded-2xl bg-[#a259ff]/10 flex items-center justify-center text-neon-purple font-bold">
663
+ <Cpu className="w-6 h-6" />
664
+ </div>
665
+ <h3 className="text-lg font-bold text-white">Acoustic Processing</h3>
666
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
667
+ The model maps dialogue turn-taking patterns, computes real-time prosody adjustments, and blends speakers based on custom voice signatures.
668
+ </p>
669
+ </motion.div>
670
+
671
+ {/* Step 3 */}
672
+ <motion.div
673
+ initial={{ opacity: 0, y: 30 }}
674
+ whileInView={{ opacity: 1, y: 0 }}
675
+ viewport={{ once: true }}
676
+ transition={{ duration: 0.6, delay: 0.4 }}
677
+ className="bg-glass p-8 rounded-3xl border border-white/5 text-left relative z-10 flex flex-col gap-4"
678
+ >
679
+ <div className="absolute top-4 right-6 text-5xl font-black text-white/5">03</div>
680
+ <div className="w-12 h-12 rounded-2xl bg-[#00e673]/10 flex items-center justify-center text-[#00e673] font-bold">
681
+ <Volume2 className="w-6 h-6" />
682
+ </div>
683
+ <h3 className="text-lg font-bold text-white">Seamless Broadcast Output</h3>
684
+ <p className="text-xs text-[#8e9bb5] leading-relaxed">
685
+ Stream binary PCM audio chunks back to the client browser in under 250ms, then run the client packager to download the mastered WAV file.
686
+ </p>
687
+ </motion.div>
688
+ </div>
689
+ </section>
690
+
691
+ {/* 6. TECHNICAL CAPABILITIES SECTION */}
692
+ <section id="capabilities" className="relative z-10 py-24 border-t border-white/5 max-w-[1300px] mx-auto px-6 grid grid-cols-1 lg:grid-cols-[0.9fr_1.1fr] gap-12 items-center">
693
+ <div className="flex flex-col gap-6 text-left">
694
+ <span className="text-xs font-bold uppercase tracking-widest text-neon-cyan">Deep Tech Specifications</span>
695
+ <h2 className="text-3xl md:text-5xl font-extrabold text-white leading-tight">Quantized for Low-Latency Performance</h2>
696
+ <p className="text-sm text-[#8e9bb5] leading-relaxed">
697
+ By shifting away from multi-gigabyte models that require expensive dedicated GPUs, VibeVoice provides extreme research scalability directly on home laptops.
698
+ </p>
699
+ <button onClick={onTryDemo} className="self-start flex items-center gap-2 text-xs font-bold text-[#00f2fe] hover:text-[#00f2fe]/80 group transition duration-200">
700
+ Open the Studio Workspace now <ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
701
+ </button>
702
+ </div>
703
+
704
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
705
+ {[
706
+ { label: "Long-Context Synthesis", desc: "Stable generation beyond 30-minute scripts without memory bottlenecks." },
707
+ { label: "Multi-Turn Dialogue", desc: "Simulates interruptions, dynamic transitions, and collaborative dialogue." },
708
+ { label: "Context Retention", desc: "Tracks prosody style consistency across speaker exchanges." },
709
+ { label: "Emotional Speech Modeling", desc: "Adapts speaking pace and emphasis dynamically depending on context." },
710
+ { label: "Realistic Timing & Pauses", desc: "Injects breathing frames, silence, and natural hesitation timings." },
711
+ { label: "High-Quality Neural TTS", desc: "Synthesizes standard 24,000Hz speech directly client-side." },
712
+ { label: "Scalable Deployment", desc: "Supports Docker, Kubernetes deployment, and ONNX Runtime scaling." },
713
+ { label: "GPU-Optimized Inference", desc: "Compiles cleanly to TensorRT for enterprise SaaS frameworks." }
714
+ ].map((cap, index) => (
715
+ <div key={index} className="bg-glass-card p-4 rounded-xl border border-white/4 flex flex-col gap-1.5 hover:border-[#00f2fe]/30 transition duration-300">
716
+ <div className="flex items-center gap-2 text-white font-bold text-xs select-none">
717
+ <Check className="w-4 h-4 text-[#00e673]" />
718
+ <span>{cap.label}</span>
719
+ </div>
720
+ <p className="text-[11px] text-[#8e9bb5] leading-relaxed pl-6">{cap.desc}</p>
721
+ </div>
722
+ ))}
723
+ </div>
724
+ </section>
725
+
726
+ {/* 7. DEMO SECTION */}
727
+ <section id="demo" className="relative z-10 py-24 bg-black/40 border-t border-white/5 max-w-[1300px] mx-auto px-6">
728
+ <div className="text-center max-w-[700px] mx-auto mb-16 flex flex-col gap-3">
729
+ <span className="text-xs font-bold uppercase tracking-widest text-[#a259ff]">Live Visual Playground</span>
730
+ <h2 className="text-3xl md:text-5xl font-extrabold text-white">Experience the VibeVoice flow</h2>
731
+ <p className="text-sm text-[#8e9bb5] leading-relaxed">
732
+ Click play below to explore interactive speaker switching, custom scripts, and visual voice-wave modulations.
733
+ </p>
734
+ </div>
735
+
736
+ {/* Custom Mock Interactive Demo Component */}
737
+ <div className="max-w-[850px] mx-auto bg-glass rounded-3xl p-6 md:p-8 border border-white/8 backdrop-blur-2xl shadow-3xl flex flex-col gap-6">
738
+ <div className="flex flex-col md:flex-row justify-between items-center gap-4 border-b border-white/5 pb-5">
739
+ <div className="text-left flex flex-col gap-1">
740
+ <span className="text-[9px] text-[#8e9bb5] uppercase font-bold tracking-wider">Demo Playback Studio</span>
741
+ <h3 className="text-md font-bold text-white flex items-center gap-2">
742
+ <Headphones className="w-4.5 h-4.5 text-[#00f2fe] animate-pulse" />
743
+ {voicePresets[selectedVoicePreset].title}
744
+ </h3>
745
+ </div>
746
+
747
+ {/* Select Presets bar */}
748
+ <div className="flex items-center gap-2 bg-white/4 p-1 rounded-xl border border-white/5">
749
+ {Object.keys(voicePresets).map(presetKey => (
750
+ <button
751
+ key={presetKey}
752
+ onClick={() => setSelectedVoicePreset(presetKey)}
753
+ className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition cursor-pointer select-none ${
754
+ selectedVoicePreset === presetKey
755
+ ? 'bg-gradient-to-r from-[#00f2fe] to-[#a259ff] text-black shadow-md'
756
+ : 'text-[#8e9bb5] hover:text-white hover:bg-white/2'
757
+ }`}
758
+ >
759
+ {presetKey}
760
+ </button>
761
+ ))}
762
+ </div>
763
+ </div>
764
+
765
+ <div className="grid grid-cols-1 md:grid-cols-[1fr_240px] gap-6 items-start">
766
+
767
+ {/* Visualizer Area */}
768
+ <div className="flex flex-col gap-4 w-full">
769
+ <div className="bg-black/40 border border-white/4 rounded-2xl p-6 min-h-[160px] flex flex-col justify-center items-center relative overflow-hidden">
770
+ <canvas ref={canvasRef} width="500" height="100" className="w-full h-[100px]"></canvas>
771
+
772
+ {/* Animated Orb matching active speaker */}
773
+ {mockIsPlaying && (
774
+ <div className="absolute top-4 right-4 flex items-center gap-2">
775
+ <span className="w-2 h-2 rounded-full bg-[#ff5c8d] animate-ping"></span>
776
+ <span className="text-[9px] text-[#ff5c8d] font-bold uppercase tracking-wider">
777
+ Active: {voicePresets[selectedVoicePreset].speakers[mockActiveSpeaker]?.name || "Speaker"}
778
+ </span>
779
+ </div>
780
+ )}
781
+ </div>
782
+
783
+ {/* Action play controls */}
784
+ <div className="flex items-center gap-4 bg-white/2 p-3 rounded-2xl border border-white/4">
785
+ <button
786
+ onClick={() => setMockIsPlaying(!mockIsPlaying)}
787
+ className="w-10 h-10 rounded-full bg-gradient-to-r from-[#00f2fe] to-[#a259ff] hover:shadow-[0_0_15px_rgba(0,242,254,0.35)] flex items-center justify-center text-black hover:scale-105 active:scale-95 transition cursor-pointer"
788
+ >
789
+ {mockIsPlaying ? (
790
+ <Square className="w-4 h-4 fill-current" />
791
+ ) : (
792
+ <Play className="w-4 h-4 fill-current ml-0.5" />
793
+ )}
794
+ </button>
795
+
796
+ <div className="flex-1 text-left">
797
+ <div className="flex justify-between text-[10px] text-[#8e9bb5] mb-1">
798
+ <span>{mockIsPlaying ? "Playing Dynamic Dialogue Chunks..." : "Ready to playback preview"}</span>
799
+ <span>{Math.floor(mockTimeline)}s / {voicePresets[selectedVoicePreset].duration}s</span>
800
+ </div>
801
+ <div className="h-1.5 w-full bg-white/6 rounded-full overflow-hidden relative">
802
+ <div
803
+ className="absolute left-0 top-0 bottom-0 bg-gradient-to-r from-[#00f2fe] to-[#a259ff] transition-all duration-100 rounded-full"
804
+ style={{ width: `${(mockTimeline / voicePresets[selectedVoicePreset].duration) * 100}%` }}
805
+ ></div>
806
+ </div>
807
+ </div>
808
+ </div>
809
+ </div>
810
+
811
+ {/* Script Viewer */}
812
+ <div className="flex flex-col gap-3 text-left w-full">
813
+ <span className="text-[10px] text-[#8e9bb5] uppercase font-bold tracking-wider">Turn-by-Turn Transcript</span>
814
+ <div className="bg-black/25 rounded-2xl p-4 border border-white/4 flex flex-col gap-3 max-h-[220px] overflow-y-auto">
815
+ {voicePresets[selectedVoicePreset].script.map((line, idx) => {
816
+ const speakerDetails = voicePresets[selectedVoicePreset].speakers[line.spk] || { name: "Guest", color: "cyan" };
817
+ const colorBadge = speakerDetails.color === 'cyan' ? 'text-[#00f2fe]'
818
+ : speakerDetails.color === 'purple' ? 'text-[#a259ff]'
819
+ : speakerDetails.color === 'amber' ? 'text-[#ffb703]'
820
+ : 'text-white';
821
+
822
+ const isActiveLine = mockIsPlaying && mockTimeline >= line.time &&
823
+ (idx === voicePresets[selectedVoicePreset].script.length - 1 || mockTimeline < voicePresets[selectedVoicePreset].script[idx + 1].time);
824
+
825
+ return (
826
+ <div
827
+ key={idx}
828
+ className={`p-2.5 rounded-xl border transition-all duration-300 ${
829
+ isActiveLine
830
+ ? 'bg-[#00f2fe]/6 border-[#00f2fe]/20 shadow-[0_0_8px_rgba(0,242,254,0.05)]'
831
+ : 'bg-transparent border-transparent opacity-50'
832
+ }`}
833
+ >
834
+ <span className={`block text-[9px] font-bold uppercase tracking-wider mb-0.5 ${colorBadge}`}>
835
+ {speakerDetails.name}
836
+ </span>
837
+ <p className="text-[11px] text-[#f0f3fa] font-mono leading-relaxed">{line.text}</p>
838
+ </div>
839
+ );
840
+ })}
841
+ </div>
842
+ </div>
843
+
844
+ </div>
845
+
846
+ <div className="bg-[#00f2fe]/4 border border-[#00f2fe]/10 p-4 rounded-2xl flex flex-col sm:flex-row justify-between items-center gap-4 text-left">
847
+ <div className="flex items-center gap-3">
848
+ <Flame className="w-5 h-5 text-neon-cyan animate-bounce" />
849
+ <div>
850
+ <h4 className="text-xs font-bold text-white">Unlock full control over model layers</h4>
851
+ <p className="text-[10px] text-[#8e9bb5]">Upload voice profiles, customize speeds, and download master tracks in the playground.</p>
852
+ </div>
853
+ </div>
854
+ <button
855
+ onClick={onTryDemo}
856
+ className="w-full sm:w-auto flex items-center justify-center gap-1.5 px-4 py-2 bg-gradient-to-r from-[#00f2fe] to-[#a259ff] text-black font-bold text-xs rounded-xl transition cursor-pointer hover:shadow-lg active:scale-95 whitespace-nowrap"
857
+ >
858
+ Open Studio Workspace
859
+ </button>
860
+ </div>
861
+
862
+ </div>
863
+ </section>
864
+
865
+ {/* 8. OPEN SOURCE SECTION */}
866
+ <section id="open-source" className="relative z-10 py-24 border-t border-white/5 max-w-[1300px] mx-auto px-6">
867
+ <motion.div
868
+ initial="hidden"
869
+ whileInView="visible"
870
+ viewport={{ once: true, margin: "-100px" }}
871
+ variants={containerVariants}
872
+ className="bg-glass rounded-3xl p-8 md:p-12 border border-white/8 backdrop-blur-2xl shadow-3xl text-left grid grid-cols-1 lg:grid-cols-[1fr_0.8fr] gap-12 items-center"
873
+ >
874
+ <motion.div variants={itemVariants} className="flex flex-col gap-5">
875
+ <span className="text-xs font-bold uppercase tracking-widest text-[#00e673]">100% Free &amp; Open Source</span>
876
+ <h2 className="text-3xl md:text-5xl font-extrabold text-white">Join the VibeVoice developer ecosystem</h2>
877
+ <p className="text-sm text-[#8e9bb5] leading-relaxed">
878
+ We believe conversational voice models should be accessible to all developers. Get instant access to pre-quantized model base checkpoints, training layers, and custom fine-tuning scripts.
879
+ </p>
880
+
881
+ <div className="flex flex-wrap gap-4 pt-2">
882
+ <a
883
+ href="https://github.com"
884
+ target="_blank"
885
+ rel="noopener noreferrer"
886
+ className="flex items-center gap-2.5 px-5 py-3 rounded-xl bg-white/4 border border-white/8 hover:bg-white/8 hover:border-white/15 text-white font-bold text-xs transition duration-200 cursor-pointer active:scale-95"
887
+ >
888
+ <Github className="w-4 h-4" />
889
+ GitHub Repository
890
+ </a>
891
+ <a
892
+ href="https://huggingface.co"
893
+ target="_blank"
894
+ rel="noopener noreferrer"
895
+ className="flex items-center gap-2.5 px-5 py-3 rounded-xl bg-white/4 border border-white/8 hover:bg-white/8 hover:border-white/15 text-white font-bold text-xs transition duration-200 cursor-pointer active:scale-95"
896
+ >
897
+ <Layers className="w-4 h-4 text-neon-cyan" />
898
+ Hugging Face Base Models
899
+ </a>
900
+ </div>
901
+ </motion.div>
902
+
903
+ <motion.div variants={itemVariants} className="bg-black/35 rounded-2xl border border-white/4 p-6 flex flex-col gap-5">
904
+ <h3 className="text-sm font-bold text-white uppercase tracking-wider border-b border-white/5 pb-2 flex items-center gap-2">
905
+ <Cpu className="w-4 h-4 text-[#a259ff]" /> Model Checklist
906
+ </h3>
907
+
908
+ <div className="flex flex-col gap-3">
909
+ <div className="flex items-center gap-3">
910
+ <span className="w-2.5 h-2.5 rounded-full bg-[#00e673] shadow-[0_0_6px_#00e673]"></span>
911
+ <span className="text-xs font-semibold text-white">Quantized 1.5B ONNX Checkpoint</span>
912
+ </div>
913
+ <p className="text-[10.5px] text-[#8e9bb5] leading-relaxed pl-5.5">Ideal for deployment on edge systems and low-resource CPU servers.</p>
914
+
915
+ <div className="flex items-center gap-3 border-t border-dashed border-white/5 pt-3">
916
+ <span className="w-2.5 h-2.5 rounded-full bg-[#00e673] shadow-[0_0_6px_#00e673]"></span>
917
+ <span className="text-xs font-semibold text-white">Full PyTorch Training Recipes</span>
918
+ </div>
919
+ <p className="text-[10.5px] text-[#8e9bb5] leading-relaxed pl-5.5">Prepackaged datasets, gradient-accumulation code, and training layers.</p>
920
+
921
+ <div className="flex items-center gap-3 border-t border-dashed border-white/5 pt-3">
922
+ <span className="w-2.5 h-2.5 rounded-full bg-[#00e673] shadow-[0_0_6px_#00e673]"></span>
923
+ <span className="text-xs font-semibold text-white">Zero-Shot Cloning Checkpoints</span>
924
+ </div>
925
+ <p className="text-[10.5px] text-[#8e9bb5] leading-relaxed pl-5.5">Allows robust speaker feature mapping with zero gradient training runs.</p>
926
+ </div>
927
+ </motion.div>
928
+ </motion.div>
929
+ </section>
930
+
931
+ {/* 9. TESTIMONIALS SECTION */}
932
+ <section className="relative z-10 py-24 bg-black/30 border-t border-white/5 max-w-[1300px] mx-auto px-6">
933
+ <div className="text-center max-w-[700px] mx-auto mb-16 flex flex-col gap-3">
934
+ <span className="text-xs font-bold uppercase tracking-widest text-[#00f2fe]">User Reviews</span>
935
+ <h2 className="text-3xl md:text-5xl font-extrabold text-white">Loved by Researchers & Creators</h2>
936
+ <p className="text-sm text-[#8e9bb5] leading-relaxed">
937
+ Read how developers are applying VibeVoice AI to rewrite the vocal product stack.
938
+ </p>
939
+ </div>
940
+
941
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 text-left">
942
+
943
+ {/* Review 1 */}
944
+ <div className="bg-glass p-6 rounded-2xl border border-white/5 flex flex-col gap-4 justify-between">
945
+ <p className="text-xs text-[#8e9bb5] leading-relaxed italic">
946
+ "We migrated all virtual co-hosts for our audiobooks to VibeVoice. The zero-shot reference cloning accuracy is astonishing; it matches emotional cadence with negligible distortion."
947
+ </p>
948
+ <div className="flex items-center gap-3 border-t border-white/5 pt-4">
949
+ <div className="w-8 h-8 rounded-full bg-gradient-to-tr from-[#00f2fe] to-[#a259ff] flex items-center justify-center text-[10px] font-bold text-black font-mono select-none">
950
+ AR
951
+ </div>
952
+ <div>
953
+ <h4 className="text-xs font-bold text-white">Dr. Aris Vance</h4>
954
+ <p className="text-[9px] text-[#8e9bb5] uppercase">AI Audio Researcher</p>
955
+ </div>
956
+ </div>
957
+ </div>
958
+
959
+ {/* Review 2 */}
960
+ <div className="bg-glass p-6 rounded-2xl border border-white/5 flex flex-col gap-4 justify-between">
961
+ <p className="text-xs text-[#8e9bb5] leading-relaxed italic">
962
+ "Building responsive conversational pipelines is exceptionally straightforward. Standard WebSockets streaming audio chunks under 250ms is exactly what SaaS developers need."
963
+ </p>
964
+ <div className="flex items-center gap-3 border-t border-white/5 pt-4">
965
+ <div className="w-8 h-8 rounded-full bg-gradient-to-tr from-[#a259ff] to-[#ff5c8d] flex items-center justify-center text-[10px] font-bold text-black font-mono select-none">
966
+ KL
967
+ </div>
968
+ <div>
969
+ <h4 className="text-xs font-bold text-white">Karen Lim</h4>
970
+ <p className="text-[9px] text-[#8e9bb5] uppercase">Vocal Startup Founder</p>
971
+ </div>
972
+ </div>
973
+ </div>
974
+
975
+ {/* Review 3 */}
976
+ <div className="bg-glass p-6 rounded-2xl border border-white/5 flex flex-col gap-4 justify-between">
977
+ <p className="text-xs text-[#8e9bb5] leading-relaxed italic">
978
+ "The local CPU inference speeds are a massive paradigm shift. Generating multi-speaker podcasts without scaling complex Kubernetes GPU clusters completely changed our billing models."
979
+ </p>
980
+ <div className="flex items-center gap-3 border-t border-white/5 pt-4">
981
+ <div className="w-8 h-8 rounded-full bg-gradient-to-tr from-[#ff5c8d] to-[#ffb703] flex items-center justify-center text-[10px] font-bold text-black font-mono select-none">
982
+ MB
983
+ </div>
984
+ <div>
985
+ <h4 className="text-xs font-bold text-white">Marcus Brody</h4>
986
+ <p className="text-[9px] text-[#8e9bb5] uppercase">Podcast Platform Creator</p>
987
+ </div>
988
+ </div>
989
+ </div>
990
+
991
+ {/* Review 4 */}
992
+ <div className="bg-glass p-6 rounded-2xl border border-white/5 flex flex-col gap-4 justify-between">
993
+ <p className="text-xs text-[#8e9bb5] leading-relaxed italic">
994
+ "Being able to review training scripts and inspect decoupled prosody token architectures is brilliant. This model is exceptionally clean and well-structured for university research projects."
995
+ </p>
996
+ <div className="flex items-center gap-3 border-t border-white/5 pt-4">
997
+ <div className="w-8 h-8 rounded-full bg-gradient-to-tr from-[#00f2fe] to-[#00e673] flex items-center justify-center text-[10px] font-bold text-black font-mono select-none">
998
+ SC
999
+ </div>
1000
+ <div>
1001
+ <h4 className="text-xs font-bold text-white">Sonia Chen</h4>
1002
+ <p className="text-[9px] text-[#8e9bb5] uppercase">ML PhD Scholar</p>
1003
+ </div>
1004
+ </div>
1005
+ </div>
1006
+
1007
+ </div>
1008
+ </section>
1009
+
1010
+ {/* 10. FAQ SECTION */}
1011
+ <section id="faq" className="relative z-10 py-24 border-t border-white/5 max-w-[850px] mx-auto px-6 text-left">
1012
+ <div className="text-center max-w-[700px] mx-auto mb-16 flex flex-col gap-3">
1013
+ <span className="text-xs font-bold uppercase tracking-widest text-[#a259ff] flex items-center justify-center gap-1.5">
1014
+ <HelpCircle className="w-3.5 h-3.5 text-[#a259ff]" /> Common Questions
1015
+ </span>
1016
+ <h2 className="text-3xl md:text-5xl font-extrabold text-white">Frequently Asked Questions</h2>
1017
+ <p className="text-sm text-[#8e9bb5]">Everything you need to know about setting up and running VibeVoice AI.</p>
1018
+ </div>
1019
+
1020
+ <div className="flex flex-col gap-4">
1021
+ {faqs.map((faq, idx) => (
1022
+ <div
1023
+ key={idx}
1024
+ className="bg-glass rounded-2xl border border-white/5 hover:border-white/10 overflow-hidden transition-colors duration-200"
1025
+ >
1026
+ <button
1027
+ onClick={() => setActiveFaq(activeFaq === idx ? null : idx)}
1028
+ className="w-full flex justify-between items-center p-6 text-left font-bold text-white text-sm md:text-base cursor-pointer focus:outline-none"
1029
+ >
1030
+ <span>{faq.q}</span>
1031
+ <ChevronDown className={`w-5 h-5 text-[#8e9bb5] transition-transform duration-300 ${activeFaq === idx ? 'transform rotate-180 text-neon-cyan' : ''}`} />
1032
+ </button>
1033
+
1034
+ <AnimatePresence initial={false}>
1035
+ {activeFaq === idx && (
1036
+ <motion.div
1037
+ initial={{ height: 0, opacity: 0 }}
1038
+ animate={{ height: "auto", opacity: 1 }}
1039
+ exit={{ height: 0, opacity: 0 }}
1040
+ transition={{ duration: 0.25, ease: "easeInOut" }}
1041
+ >
1042
+ <div className="p-6 pt-0 border-t border-white/4 text-xs md:text-sm text-[#8e9bb5] leading-relaxed">
1043
+ {faq.a}
1044
+ </div>
1045
+ </motion.div>
1046
+ )}
1047
+ </AnimatePresence>
1048
+ </div>
1049
+ ))}
1050
+ </div>
1051
+ </section>
1052
+
1053
+ {/* 11. FINAL CTA SECTION */}
1054
+ <section className="relative z-10 py-28 border-t border-white/5 max-w-[1300px] mx-auto px-6 text-center">
1055
+ <div className="bg-glass rounded-3xl p-12 md:p-20 border border-white/8 relative overflow-hidden bg-gradient-to-b from-[#101830]/40 to-transparent flex flex-col items-center gap-6 shadow-3xl">
1056
+
1057
+ <div className="absolute inset-0 pointer-events-none z-0">
1058
+ <div className="absolute w-[450px] h-[450px] rounded-full top-[-20%] left-[-10%] bg-gradient-to-br from-[#00f2fe]/10 to-transparent blur-[120px] animate-pulse"></div>
1059
+ <div className="absolute w-[450px] h-[450px] rounded-full bottom-[-20%] right-[-10%] bg-gradient-to-br from-[#a259ff]/10 to-transparent blur-[120px] animate-pulse"></div>
1060
+ </div>
1061
+
1062
+ <span className="relative z-10 text-xs font-bold uppercase tracking-widest text-[#00f2fe]">Experience It Live</span>
1063
+ <h2 className="relative z-10 text-4xl md:text-6xl font-extrabold text-white tracking-tight max-w-[700px] leading-tight">
1064
+ Build the Future of Conversational Voice AI
1065
+ </h2>
1066
+ <p className="relative z-10 text-sm md:text-base text-[#8e9bb5] max-w-[520px] leading-relaxed">
1067
+ Instantly record dynamic speakers, compile realistic podcast dialogues, and master local WAV tracks completely in your browser.
1068
+ </p>
1069
+
1070
+ <div className="relative z-10 flex flex-col sm:flex-row gap-4 pt-4 w-full justify-center max-w-[450px]">
1071
+ <button
1072
+ onClick={onTryDemo}
1073
+ className="flex-1 flex items-center justify-center gap-2.5 px-6 py-4 rounded-xl bg-gradient-to-r from-[#00f2fe] to-[#a259ff] text-black font-extrabold text-sm transition duration-200 cursor-pointer hover:shadow-lg active:scale-95 shadow-[0_4px_15px_rgba(0,242,254,0.25)]"
1074
+ >
1075
+ Get Started
1076
+ </button>
1077
+ <a
1078
+ href="https://github.com"
1079
+ target="_blank"
1080
+ rel="noopener noreferrer"
1081
+ className="flex-1 flex items-center justify-center gap-2.5 px-6 py-4 rounded-xl bg-white/4 border border-white/8 hover:bg-white/8 text-white font-bold text-sm transition duration-200 cursor-pointer active:scale-95"
1082
+ >
1083
+ Explore GitHub
1084
+ </a>
1085
+ </div>
1086
+ </div>
1087
+ </section>
1088
+
1089
+ {/* 12. FOOTER */}
1090
+ <footer className="relative z-10 border-t border-white/5 py-12 max-w-[1300px] mx-auto px-6 text-left">
1091
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-12">
1092
+
1093
+ <div className="flex flex-col gap-4">
1094
+ <div className="flex items-center gap-3">
1095
+ <Sparkles className="w-6 h-6 text-neon-cyan" />
1096
+ <span className="font-bold text-white text-base">VibeVoice AI</span>
1097
+ </div>
1098
+ <p className="text-[11px] text-[#8e9bb5] leading-relaxed max-w-[240px]">
1099
+ Open-source neural acoustics model for hyper-realistic conversational turn-taking, dialogue flows, and zero-shot voice cloning.
1100
+ </p>
1101
+ </div>
1102
+
1103
+ <div className="flex flex-col gap-3">
1104
+ <h4 className="text-xs font-bold text-white uppercase tracking-wider">Models &amp; Weights</h4>
1105
+ <div className="flex flex-col gap-2 text-xs text-[#8e9bb5]">
1106
+ <a href="https://huggingface.co" target="_blank" rel="noopener noreferrer" className="hover:text-neon-cyan transition">VibeVoice-0.5B Checkpoint</a>
1107
+ <a href="https://huggingface.co" target="_blank" rel="noopener noreferrer" className="hover:text-neon-cyan transition">Acoustic Tokenizer 24kHz</a>
1108
+ <a href="https://huggingface.co" target="_blank" rel="noopener noreferrer" className="hover:text-neon-cyan transition">Prosody Footprint Encoder</a>
1109
+ </div>
1110
+ </div>
1111
+
1112
+ <div className="flex flex-col gap-3">
1113
+ <h4 className="text-xs font-bold text-white uppercase tracking-wider">Resources</h4>
1114
+ <div className="flex flex-col gap-2 text-xs text-[#8e9bb5]">
1115
+ <a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-neon-purple transition">Research Publications</a>
1116
+ <a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-neon-purple transition">Inference CLI Recipes</a>
1117
+ <a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-neon-purple transition">Fine-Tuning Documentation</a>
1118
+ </div>
1119
+ </div>
1120
+
1121
+ <div className="flex flex-col gap-3">
1122
+ <h4 className="text-xs font-bold text-white uppercase tracking-wider">Developer Community</h4>
1123
+ <div className="flex flex-col gap-2 text-xs text-[#8e9bb5]">
1124
+ <a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-[#ff5c8d] transition">GitHub Ecosystem</a>
1125
+ <a href="https://huggingface.co" target="_blank" rel="noopener noreferrer" className="hover:text-[#ff5c8d] transition">Hugging Face Discussions</a>
1126
+ <a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-[#ff5c8d] transition">Contribute Code</a>
1127
+ </div>
1128
+ </div>
1129
+
1130
+ </div>
1131
+
1132
+ <div className="border-t border-white/5 pt-8 flex flex-col md:flex-row justify-between items-center gap-4 text-xs text-[#8e9bb5]">
1133
+ <span>© {new Date().getFullYear()} Microsoft VibeVoice AI. Research-grade open-source software license.</span>
1134
+ <div className="flex items-center gap-4">
1135
+ <a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-white transition flex items-center gap-1">
1136
+ <Github className="w-3.5 h-3.5" /> GitHub
1137
+ </a>
1138
+ <span>•</span>
1139
+ <span className="flex items-center gap-1 text-[#ff5c8d]">
1140
+ Made for AI Researchers <Heart className="w-3.5 h-3.5 fill-current text-[#ff5c8d]" />
1141
+ </span>
1142
+ </div>
1143
+ </div>
1144
+ </footer>
1145
+
1146
+ </div>
1147
+ );
1148
+ }
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/index.css ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ /* ==========================================================================
4
+ VibeVoice React Studio CSS Styling
5
+ Combines Tailwind v4 utility engine with custom high-end visuals and glows
6
+ ========================================================================== */
7
+
8
+ @layer utilities {
9
+ .text-neon-cyan {
10
+ color: hsl(185, 95%, 48%);
11
+ text-shadow: 0 0 10px hsla(185, 95%, 48%, 0.4);
12
+ }
13
+
14
+ .text-neon-purple {
15
+ color: hsl(265, 88%, 64%);
16
+ text-shadow: 0 0 10px hsla(265, 88%, 64%, 0.4);
17
+ }
18
+
19
+ .text-neon-pink {
20
+ color: hsl(330, 92%, 60%);
21
+ text-shadow: 0 0 10px hsla(330, 92%, 60%, 0.4);
22
+ }
23
+
24
+ .bg-glass {
25
+ background: rgba(13, 17, 28, 0.45);
26
+ border: 1px solid rgba(255, 255, 255, 0.08);
27
+ backdrop-filter: blur(24px);
28
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
29
+ }
30
+
31
+ .bg-glass-card {
32
+ background: rgba(255, 255, 255, 0.02);
33
+ border: 1px solid rgba(255, 255, 255, 0.04);
34
+ }
35
+
36
+ .bg-glass-card:hover {
37
+ background: rgba(255, 255, 255, 0.03);
38
+ border-color: rgba(255, 255, 255, 0.08);
39
+ }
40
+ }
41
+
42
+ /* Animations */
43
+ @keyframes orbPulse {
44
+ 0% { transform: scale(1); }
45
+ 50% { transform: scale(1.08); }
46
+ 100% { transform: scale(1); }
47
+ }
48
+
49
+ @keyframes orbSpin {
50
+ 0% { transform: rotate(0deg); }
51
+ 100% { transform: rotate(360deg); }
52
+ }
53
+
54
+ @keyframes orbGlowScale {
55
+ 0% { transform: scale(0.95); opacity: 0.4; }
56
+ 100% { transform: scale(1.2); opacity: 0.75; }
57
+ }
58
+
59
+ @keyframes waveExpand {
60
+ 0% { transform: scale(0.9); opacity: 0.9; }
61
+ 100% { transform: scale(1.8); opacity: 0; }
62
+ }
63
+
64
+ @keyframes floatGlow {
65
+ 0% { transform: translate(0, 0) scale(1); }
66
+ 50% { transform: translate(100px, 80px) scale(1.15); }
67
+ 100% { transform: translate(-50px, 150px) scale(0.9); }
68
+ }
69
+
70
+ /* Classes for visual states */
71
+ .animate-float-glow-1 {
72
+ animation: floatGlow 25s infinite alternate ease-in-out;
73
+ }
74
+
75
+ .animate-float-glow-2 {
76
+ animation: floatGlow 30s infinite alternate-reverse ease-in-out;
77
+ }
78
+
79
+ /* Custom Orb styling states */
80
+ .orb-state-idle {
81
+ background: radial-gradient(circle, #00f2fe 0%, rgba(0, 150, 255, 0.3) 100%);
82
+ box-shadow: 0 0 20px rgba(0, 242, 254, 0.35);
83
+ animation: orbPulse 4s infinite ease-in-out;
84
+ }
85
+
86
+ .orb-state-thinking {
87
+ background: radial-gradient(circle, #fff 0%, #a259ff 70%, #00f2fe 100%);
88
+ box-shadow: 0 0 35px rgba(162, 89, 255, 0.35);
89
+ animation: orbSpin 2s infinite linear, orbPulse 1s infinite ease-in-out;
90
+ }
91
+
92
+ .orb-state-speaking {
93
+ background: radial-gradient(circle, #fff 0%, #ff5c8d 60%, #a259ff 100%);
94
+ box-shadow: 0 0 30px rgba(255, 92, 141, 0.35);
95
+ animation: orbPulse 0.4s infinite ease-in-out;
96
+ }
97
+
98
+ .orb-state-listening {
99
+ background: radial-gradient(circle, #fff 0%, #00e673 70%);
100
+ box-shadow: 0 0 25px rgba(0, 230, 115, 0.4);
101
+ animation: orbPulse 1.2s infinite ease-in-out;
102
+ }
103
+
104
+ /* Scrollbar customizations */
105
+ ::-webkit-scrollbar {
106
+ width: 5px;
107
+ height: 5px;
108
+ }
109
+
110
+ ::-webkit-scrollbar-track {
111
+ background: transparent;
112
+ }
113
+
114
+ ::-webkit-scrollbar-thumb {
115
+ background: rgba(255, 255, 255, 0.08);
116
+ border-radius: 10px;
117
+ }
118
+
119
+ ::-webkit-scrollbar-thumb:hover {
120
+ background: rgba(255, 255, 255, 0.15);
121
+ }
122
+
123
+ /* Custom Modal animations */
124
+ @keyframes fadeIn {
125
+ from { opacity: 0; }
126
+ to { opacity: 1; }
127
+ }
128
+
129
+ @keyframes scaleUp {
130
+ from { transform: scale(0.95); opacity: 0; }
131
+ to { transform: scale(1); opacity: 1; }
132
+ }
133
+
134
+ .animate-fade-in {
135
+ animation: fadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
136
+ }
137
+
138
+ .animate-scale-up {
139
+ animation: scaleUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
140
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/supabaseClient.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js'
2
+
3
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
4
+ const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
5
+
6
+ export const supabase = createClient(supabaseUrl, supabaseAnonKey)
frontend/vite.config.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [
8
+ react(),
9
+ tailwindcss(),
10
+ ],
11
+ server: {
12
+ proxy: {
13
+ '/api': {
14
+ target: 'http://127.0.0.1:8000',
15
+ changeOrigin: true,
16
+ ws: true,
17
+ },
18
+ '/static': {
19
+ target: 'http://127.0.0.1:8000',
20
+ changeOrigin: true,
21
+ },
22
+ },
23
+ },
24
+ })
25
+
26
+
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.100.0
2
+ uvicorn>=0.20.0
3
+ python-multipart>=0.0.6
4
+ soundfile>=0.12.1
5
+ torch>=2.0.0
6
+ transformers>=4.36.0
7
+ numpy>=1.20.0
8
+ websockets>=12.0
9
+ pyttsx3>=2.90
10
+ comtypes>=1.4.0
run.bat ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ title VibeVoice Studio Fullstack Launcher
3
+ echo ===================================================
4
+ echo VibeVoice React + FastAPI Studio Launcher
5
+ echo ===================================================
6
+ echo.
7
+ echo [1/3] Starting VibeVoice FastAPI Backend...
8
+ start "VibeVoice API Server" cmd /k "python app.py"
9
+
10
+ echo.
11
+ echo [2/3] Starting Vite React + Tailwind Dev Server...
12
+ start "VibeVoice React Frontend" cmd /k "cd frontend && npm run dev"
13
+
14
+ echo.
15
+ echo ===================================================
16
+ echo FastAPI API server running on: http://127.0.0.1:8000
17
+ echo React Dev frontend running on: http://localhost:5173
18
+ echo ===================================================
19
+ echo.
20
+ echo This launcher window can now be safely closed.
21
+ pause
static/app.js ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * VibeVoice Studio - Front-End Audio Engine & UI Controller
3
+ * Handles WebSocket PCM streaming, gapless scheduling, Web Audio API,
4
+ * microphone recording, canvas visualization, and dashboard state management.
5
+ */
6
+
7
+ // Global App State
8
+ let audioCtx = null;
9
+ let analyserNode = null;
10
+ let gainNode = null;
11
+ let ws = null;
12
+ let currentAudioSource = null;
13
+
14
+ // Streaming Audio Queue Variables (Gapless Playback)
15
+ let playbackQueue = [];
16
+ let nextPlayTime = 0;
17
+ let isStreamingActive = false;
18
+ let scheduledBufferSources = [];
19
+ const SAMPLING_RATE = 24000; // VibeVoice standard samplerate
20
+
21
+ // Voice Recording State (Voice Cloning)
22
+ let mediaRecorders = [null, null];
23
+ let audioChunks = [[], []];
24
+ let voicePaths = [null, null];
25
+
26
+ // HTML Elements Selection
27
+ const modelStatusVal = document.getElementById("modelStatusVal");
28
+ const modelDot = document.getElementById("modelDot");
29
+ const btnToggleModel = document.getElementById("btnToggleModel");
30
+ const connBadge = document.getElementById("connBadge");
31
+ const connIcon = document.getElementById("connIcon");
32
+ const connText = document.getElementById("connText");
33
+
34
+ const txtScript = document.getElementById("txtScript");
35
+ const chkStreamMode = document.getElementById("chkStreamMode");
36
+ const btnSynthesize = document.getElementById("btnSynthesize");
37
+ const btnStop = document.getElementById("btnStop");
38
+
39
+ const agentOrb = document.getElementById("agentOrb");
40
+ const agentStateText = document.getElementById("agentStateText");
41
+ const agentStateSub = document.getElementById("agentStateSub");
42
+
43
+ const canvasWaveform = document.getElementById("canvasWaveform");
44
+ const canvasCtx = canvasWaveform.getContext("2d");
45
+
46
+ // Playback Toolbar Elements
47
+ const audioToolbar = document.getElementById("audioToolbar");
48
+ const btnToolbarPlay = document.getElementById("btnToolbarPlay");
49
+ const sliderTimeline = document.getElementById("sliderTimeline");
50
+ const progressTimelineFill = document.getElementById("progressTimelineFill");
51
+ const lblTimeStart = document.getElementById("lblTimeStart");
52
+ const lblTimeEnd = document.getElementById("lblTimeEnd");
53
+ const sliderVolume = document.getElementById("sliderVolume");
54
+ const iconVolume = document.getElementById("iconVolume");
55
+ const selectSpeed = document.getElementById("selectSpeed");
56
+
57
+ // Voice Cloning Slots Elements
58
+ const spkName0 = document.getElementById("spkName0");
59
+ const btnRecord0 = document.getElementById("btnRecord0");
60
+ const fileVoice0 = document.getElementById("fileVoice0");
61
+ const clonedStatus0 = document.getElementById("clonedStatus0");
62
+ const audioPreview0 = document.getElementById("audioPreview0");
63
+
64
+ const spkName1 = document.getElementById("spkName1");
65
+ const btnRecord1 = document.getElementById("btnRecord1");
66
+ const fileVoice1 = document.getElementById("fileVoice1");
67
+ const clonedStatus1 = document.getElementById("clonedStatus1");
68
+ const audioPreview1 = document.getElementById("audioPreview1");
69
+
70
+ const consoleBody = document.getElementById("consoleBody");
71
+ const btnClearLogs = document.getElementById("btnClearLogs");
72
+
73
+ // Presets Elements
74
+ const presetPodcast = document.getElementById("presetPodcast");
75
+ const presetAssistant = document.getElementById("presetAssistant");
76
+ const presetNews = document.getElementById("presetNews");
77
+
78
+ // ==========================================================================
79
+ // Initialization & Server Sync
80
+ // ==========================================================================
81
+
82
+ window.addEventListener("DOMContentLoaded", () => {
83
+ log("system", "Initializing Audio Engines...");
84
+ checkServerStatus();
85
+ setupCanvasVisualizer();
86
+ bindEvents();
87
+ });
88
+
89
+ // Logs messages into the built-in Console Log card on UI
90
+ function log(type, message) {
91
+ const timeStr = new Date().toLocaleTimeString();
92
+ const line = document.createElement("div");
93
+ line.className = `log-line ${type}`;
94
+ line.innerHTML = `[${timeStr}] ${message}`;
95
+ consoleBody.appendChild(line);
96
+ consoleBody.scrollTop = consoleBody.scrollHeight;
97
+ }
98
+
99
+ let statusInterval = null;
100
+
101
+ // Queries server backend to find if model weights are loaded
102
+ async function checkServerStatus() {
103
+ try {
104
+ const res = await fetch("/api/status");
105
+ const data = await res.json();
106
+
107
+ updateModelUI(data.use_real_model, data.loaded, data.loading, data.error);
108
+
109
+ // Auto poll if loading
110
+ if (data.loading) {
111
+ if (!statusInterval) {
112
+ statusInterval = setInterval(checkServerStatus, 3000);
113
+ }
114
+ } else {
115
+ if (statusInterval) {
116
+ clearInterval(statusInterval);
117
+ statusInterval = null;
118
+ }
119
+ }
120
+
121
+ // Try opening websocket streaming channel if not already connected/connecting
122
+ if (!ws || ws.readyState === WebSocket.CLOSED) {
123
+ initWebSocket();
124
+ }
125
+ } catch (e) {
126
+ log("error", "Failed to connect to VibeVoice backend API server.");
127
+ updateModelUI(false, false, false, "Offline");
128
+ if (statusInterval) {
129
+ clearInterval(statusInterval);
130
+ statusInterval = null;
131
+ }
132
+ }
133
+ }
134
+
135
+ function updateModelUI(useReal, loaded, loading, error) {
136
+ if (useReal) {
137
+ if (loading) {
138
+ modelStatusVal.innerText = "Downloading AI weights (1.2GB)...";
139
+ modelDot.className = "status-dot yellow animate-pulse";
140
+ btnToggleModel.classList.add("active");
141
+ log("info", "VibeVoice Model weights downloading from Hugging Face. Please stand by...");
142
+ } else if (loaded) {
143
+ modelStatusVal.innerText = "VibeVoice-0.5B AI";
144
+ modelDot.className = "status-dot green";
145
+ btnToggleModel.classList.add("active");
146
+ log("success", "VibeVoice Model weights loaded successfully. Zero-shot cloning activated!");
147
+ } else if (error) {
148
+ modelStatusVal.innerText = "Error Loading AI";
149
+ modelDot.className = "status-dot red";
150
+ btnToggleModel.classList.remove("active");
151
+ log("error", `Model Load Error: ${error}`);
152
+ }
153
+ } else {
154
+ modelStatusVal.innerText = "Demo Synthesizer";
155
+ modelDot.className = "status-dot green";
156
+ btnToggleModel.classList.remove("active");
157
+ }
158
+ }
159
+
160
+ // Handles switching model modes on clicking the Toggle button
161
+ btnToggleModel.addEventListener("click", async () => {
162
+ const isActive = btnToggleModel.classList.contains("active");
163
+ const enable = !isActive;
164
+ log("info", `Requesting server mode toggle: ${enable ? "Enable AI Model" : "Enable Demo Synthesizer"}`);
165
+
166
+ try {
167
+ btnToggleModel.disabled = true;
168
+ const res = await fetch(`/api/toggle-model?enable=${enable}`, { method: "POST" });
169
+ const data = await res.json();
170
+
171
+ btnToggleModel.disabled = false;
172
+ checkServerStatus();
173
+ } catch (e) {
174
+ btnToggleModel.disabled = false;
175
+ log("error", "Error toggling AI Model mode.");
176
+ }
177
+ });
178
+
179
+ // Initialize real-time WebSockets
180
+ function initWebSocket() {
181
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
182
+ const host = window.location.host;
183
+ const wsUrl = `${protocol}//${host}/api/stream`;
184
+
185
+ log("system", `Opening WebSocket stream canal: ${wsUrl}`);
186
+ ws = new WebSocket(wsUrl);
187
+
188
+ ws.onopen = () => {
189
+ connBadge.className = "connection-badge online";
190
+ connIcon.className = "fa-solid fa-link";
191
+ connText.innerText = "Connected";
192
+ log("success", "Real-time streaming audio WebSocket channel ESTABLISHED.");
193
+ };
194
+
195
+ ws.onclose = () => {
196
+ connBadge.className = "connection-badge";
197
+ connIcon.className = "fa-solid fa-link-slash";
198
+ connText.innerText = "Offline";
199
+ log("error", "Real-time streaming WebSocket channel disconnected.");
200
+ // Try reconnecting in 5 seconds
201
+ setTimeout(initWebSocket, 5000);
202
+ };
203
+
204
+ ws.onerror = (e) => {
205
+ log("error", "WebSocket channel encountered error. Resetting...");
206
+ };
207
+
208
+ ws.onmessage = async (event) => {
209
+ // Handle incoming WebSocket messages (could be text status or binary audio)
210
+ if (typeof event.data === "string") {
211
+ const data = JSON.parse(event.data);
212
+ if (data.type === "word") {
213
+ log("word", `Agent spoke word: <strong style="font-size:12px;">${data.word}</strong>`);
214
+ } else if (data.type === "done") {
215
+ log("success", "WebSocket audio stream finished downloading.");
216
+ } else if (data.type === "error") {
217
+ log("error", `Server Stream Error: ${data.message}`);
218
+ stopPlayback();
219
+ }
220
+ } else {
221
+ // Audio chunk arrived as binary raw data
222
+ const arrayBuffer = await event.data.arrayBuffer();
223
+ // Convert 16-bit signed PCM buffer to Float32 array
224
+ const float32Array = convertPCM16ToFloat32(new Int16Array(arrayBuffer));
225
+
226
+ if (isStreamingActive) {
227
+ playbackQueue.push(float32Array);
228
+ scheduleNextChunk();
229
+ }
230
+ }
231
+ };
232
+ }
233
+
234
+ // Helper to convert 16-bit integer bytes to floating values between -1.0 and 1.0
235
+ function convertPCM16ToFloat32(int16Array) {
236
+ const float32 = new Float32Array(int16Array.length);
237
+ for (let i = 0; i < int16Array.length; i++) {
238
+ // Divide by max positive value of 16-bit integer (32767)
239
+ float32[i] = int16Array[i] / 32767.0;
240
+ }
241
+ return float32;
242
+ }
243
+
244
+ // ==========================================================================
245
+ // Web Audio Context & Playback Engine (Pillar 2 & 3)
246
+ // ==========================================================================
247
+
248
+ function initAudioContext() {
249
+ if (audioCtx) return;
250
+
251
+ // Create modern AudioContext inside browser
252
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLING_RATE });
253
+
254
+ // Create Analyser Node for the Oscilloscope Waveform Canvas
255
+ analyserNode = audioCtx.createAnalyser();
256
+ analyserNode.fftSize = 256;
257
+
258
+ // Create Gain Node for volume adjustments
259
+ gainNode = audioCtx.createGain();
260
+ setVolume(sliderVolume.value / 100);
261
+
262
+ // Connect nodes together: Source -> Gain -> Analyser -> Output Speakers
263
+ gainNode.connect(analyserNode);
264
+ analyserNode.connect(audioCtx.destination);
265
+
266
+ log("system", `Browser AudioContext initiated at ${SAMPLING_RATE}Hz.`);
267
+ }
268
+
269
+ function setVolume(val) {
270
+ if (gainNode && audioCtx) {
271
+ gainNode.gain.setValueAtTime(val, audioCtx.currentTime);
272
+ // Toggle volume icon based on level
273
+ if (val === 0) iconVolume.className = "fa-solid fa-volume-mute";
274
+ else if (val < 0.5) iconVolume.className = "fa-solid fa-volume-low";
275
+ else iconVolume.className = "fa-solid fa-volume-high";
276
+ }
277
+ }
278
+
279
+ // Streaming Scheduling: Gapless stitched playing of successive floats
280
+ function startStreamingPlayback() {
281
+ initAudioContext();
282
+ if (audioCtx.state === "suspended") {
283
+ audioCtx.resume();
284
+ }
285
+
286
+ playbackQueue = [];
287
+ scheduledBufferSources = [];
288
+ nextPlayTime = audioCtx.currentTime + 0.15; // Brief offset padding buffer to start
289
+ isStreamingActive = true;
290
+
291
+ btnSynthesize.disabled = true;
292
+ btnStop.disabled = false;
293
+
294
+ setAgentOrbState("speaking", "STREAMING VOICE", "Synthesizing and playing real-time speech...");
295
+ log("info", "Playback stream started. Waiting for speech chunks...");
296
+ }
297
+
298
+ function scheduleNextChunk() {
299
+ if (!isStreamingActive || playbackQueue.length === 0) return;
300
+
301
+ const chunkFloats = playbackQueue.shift();
302
+ if (chunkFloats.length === 0) return;
303
+
304
+ // Create custom Audio Buffer for the float data
305
+ const buffer = audioCtx.createBuffer(1, chunkFloats.length, SAMPLING_RATE);
306
+ buffer.copyToChannel(chunkFloats, 0);
307
+
308
+ // Create Source Node
309
+ const sourceNode = audioCtx.createBufferSource();
310
+ sourceNode.buffer = buffer;
311
+
312
+ // Apply playback speed modifier selected by user
313
+ sourceNode.playbackRate.value = parseFloat(selectSpeed.value);
314
+
315
+ // Stitch chunk directly onto Gain node
316
+ sourceNode.connect(gainNode);
317
+
318
+ // Precise scheduling time calculation
319
+ const startTime = Math.max(audioCtx.currentTime, nextPlayTime);
320
+ sourceNode.start(startTime);
321
+
322
+ // Keep track of active playing nodes so we can cut off audio if requested
323
+ scheduledBufferSources.push(sourceNode);
324
+
325
+ // Update scheduler deadline
326
+ const chunkDuration = buffer.duration / sourceNode.playbackRate.value;
327
+ nextPlayTime = startTime + chunkDuration;
328
+
329
+ // Release reference on completion
330
+ sourceNode.onended = () => {
331
+ const idx = scheduledBufferSources.indexOf(sourceNode);
332
+ if (idx > -1) scheduledBufferSources.splice(idx, 1);
333
+
334
+ // If queue is exhausted and no active audio is left, reset agent to idle
335
+ if (scheduledBufferSources.length === 0 && playbackQueue.length === 0 && isStreamingActive) {
336
+ stopPlayback();
337
+ log("info", "Agent finished speaking.");
338
+ }
339
+ };
340
+ }
341
+
342
+ // Stops all active sounds and resets the dashboard execution state
343
+ function stopPlayback() {
344
+ isStreamingActive = false;
345
+ playbackQueue = [];
346
+
347
+ // Halt all audio buffers immediately
348
+ scheduledBufferSources.forEach(source => {
349
+ try { source.stop(); } catch(e) {}
350
+ });
351
+ scheduledBufferSources = [];
352
+
353
+ // Stop standard player if active
354
+ if (currentAudioSource) {
355
+ try { currentAudioSource.stop(); } catch(e) {}
356
+ currentAudioSource = null;
357
+ }
358
+
359
+ btnSynthesize.disabled = false;
360
+ btnStop.disabled = true;
361
+ setAgentOrbState("idle", "AGENT READY", "Configure scripts and trigger generation below");
362
+ audioToolbar.classList.add("hidden");
363
+
364
+ log("system", "Agent audio playback halted.");
365
+ }
366
+
367
+ btnStop.addEventListener("click", stopPlayback);
368
+
369
+ // Sets the visual status and glowing styles of the central agent orb
370
+ function setAgentOrbState(stateClass, title, subtitle) {
371
+ agentOrb.className = `agent-orb ${stateClass}`;
372
+ agentStateText.innerText = title;
373
+ agentStateSub.innerText = subtitle;
374
+ }
375
+
376
+ // ==========================================================================
377
+ // Canvas Oscilloscope Visualizer Rendering (Pillar 3)
378
+ // ==========================================================================
379
+
380
+ function setupCanvasVisualizer() {
381
+ const width = canvasWaveform.width;
382
+ const height = canvasWaveform.height;
383
+
384
+ function draw() {
385
+ requestAnimationFrame(draw);
386
+
387
+ canvasCtx.fillStyle = "rgba(7, 9, 14, 0.25)"; // Semitransparent sweep back
388
+ canvasCtx.fillRect(0, 0, width, height);
389
+
390
+ let dataArray = null;
391
+ let bufferLength = 0;
392
+
393
+ if (analyserNode && isAudioPlaying()) {
394
+ bufferLength = analyserNode.frequencyBinCount;
395
+ dataArray = new Uint8Array(bufferLength);
396
+ analyserNode.getByteTimeDomainData(dataArray);
397
+ }
398
+
399
+ canvasCtx.lineWidth = 2.5;
400
+ // Apply beautiful purple-to-cyan gradient stroke to waveform curves
401
+ const gradient = canvasCtx.createLinearGradient(0, 0, width, 0);
402
+ gradient.addColorStop(0, "hsl(265, 88%, 64%)"); // Purple
403
+ gradient.addColorStop(0.5, "hsl(185, 95%, 48%)"); // Cyan
404
+ gradient.addColorStop(1, "hsl(330, 92%, 60%)"); // Pink
405
+ canvasCtx.strokeStyle = gradient;
406
+
407
+ canvasCtx.shadowBlur = isAudioPlaying() ? 8 : 0;
408
+ canvasCtx.shadowColor = "rgba(0, 242, 254, 0.4)";
409
+
410
+ canvasCtx.beginPath();
411
+
412
+ if (dataArray) {
413
+ // Draw actual audio ripples
414
+ const sliceWidth = width / bufferLength;
415
+ let x = 0;
416
+
417
+ for (let i = 0; i < bufferLength; i++) {
418
+ const v = dataArray[i] / 128.0;
419
+ const y = (v * height) / 2;
420
+
421
+ if (i === 0) {
422
+ canvasCtx.moveTo(x, y);
423
+ } else {
424
+ canvasCtx.lineTo(x, y);
425
+ }
426
+
427
+ x += sliceWidth;
428
+ }
429
+ } else {
430
+ // Draw a calm breathing idle baseline
431
+ const points = 40;
432
+ const sliceWidth = width / points;
433
+ let x = 0;
434
+ const time = Date.now() * 0.003;
435
+
436
+ for (let i = 0; i < points; i++) {
437
+ // Subtle sine rhythm for visual engagement when silent
438
+ const breathingFactor = Math.sin(time) * 0.2 + 0.8;
439
+ const y = height / 2 + Math.sin(i * 0.15 + time * 2) * (2.5 * breathingFactor);
440
+
441
+ if (i === 0) {
442
+ canvasCtx.moveTo(x, y);
443
+ } else {
444
+ canvasCtx.lineTo(x, y);
445
+ }
446
+ x += sliceWidth;
447
+ }
448
+ }
449
+
450
+ canvasCtx.lineTo(width, height / 2);
451
+ canvasCtx.stroke();
452
+ }
453
+
454
+ draw();
455
+ }
456
+
457
+ function isAudioPlaying() {
458
+ return isStreamingActive || currentAudioSource !== null;
459
+ }
460
+
461
+ // ==========================================================================
462
+ // Microphone Recorder & Speaker Voice Cloning (Pillar 4)
463
+ // ==========================================================================
464
+
465
+ async function setupRecording(speakerId) {
466
+ const btnRecord = speakerId === 0 ? btnRecord0 : btnRecord1;
467
+ const isRecording = btnRecord.classList.contains("recording");
468
+
469
+ if (isRecording) {
470
+ // Stop active recording
471
+ if (mediaRecorders[speakerId]) {
472
+ mediaRecorders[speakerId].stop();
473
+ btnRecord.classList.remove("recording");
474
+ btnRecord.innerHTML = `<i class="fa-solid fa-microphone"></i> Record Mic`;
475
+ setAgentOrbState("idle", "AGENT READY", "Voice profile recorded. Transmitting to backend...");
476
+ }
477
+ } else {
478
+ // Start recording
479
+ try {
480
+ log("info", `Requesting microphone access for Speaker ${speakerId}...`);
481
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
482
+
483
+ initAudioContext();
484
+ setAgentOrbState("listening", "LISTENING...", `Say a short 10-15s dialogue sentence to clone voice profile.`);
485
+
486
+ audioChunks[speakerId] = [];
487
+ const mediaRecorder = new MediaRecorder(stream);
488
+ mediaRecorders[speakerId] = mediaRecorder;
489
+
490
+ mediaRecorder.ondataavailable = (event) => {
491
+ audioChunks[speakerId].push(event.data);
492
+ };
493
+
494
+ mediaRecorder.onstop = async () => {
495
+ log("info", "Processing audio chunks...");
496
+ const audioBlob = new Blob(audioChunks[speakerId], { type: "audio/wav" });
497
+
498
+ // Release microphore stream tracks
499
+ stream.getTracks().forEach(track => track.stop());
500
+
501
+ // Upload recorded WAV profile to server
502
+ const spkName = speakerId === 0 ? spkName0.value : spkName1.value;
503
+ await uploadVoiceProfile(audioBlob, speakerId, spkName);
504
+ };
505
+
506
+ mediaRecorder.start();
507
+ btnRecord.classList.add("recording");
508
+ btnRecord.innerHTML = `<i class="fa-solid fa-square"></i> Stop Mic`;
509
+ log("success", "Microphone recording started. Speak clearly now!");
510
+ } catch (e) {
511
+ log("error", `Could not record voice: ${e.message}`);
512
+ setAgentOrbState("idle", "AGENT READY", "Microphone permission denied or source invalid.");
513
+ }
514
+ }
515
+ }
516
+
517
+ // Transmit Voice profile file to FastAPI backend
518
+ async function uploadVoiceProfile(blob, speakerId, name) {
519
+ const formData = new FormData();
520
+ formData.append("file", blob, `mic_speaker_${speakerId}.wav`);
521
+ formData.append("speaker_name", name);
522
+
523
+ try {
524
+ log("info", "Uploading voice clone prompt to server database...");
525
+ const res = await fetch("/api/upload-voice", {
526
+ method: "POST",
527
+ body: formData
528
+ });
529
+ const data = await res.json();
530
+
531
+ if (data.status === "success") {
532
+ voicePaths[speakerId] = data.voice_path;
533
+
534
+ // Show preview player
535
+ const previewPlayer = speakerId === 0 ? audioPreview0 : audioPreview1;
536
+ const statusBox = speakerId === 0 ? clonedStatus0 : clonedStatus1;
537
+
538
+ previewPlayer.src = `/static/cloned_voices/${data.filename}`;
539
+ statusBox.classList.remove("hidden");
540
+
541
+ log("success", `Voice cloned successfully under speaker code "${name}"!`);
542
+ }
543
+ } catch(e) {
544
+ log("error", "Error uploading voice cloning profile.");
545
+ }
546
+ }
547
+
548
+ // Wire File Selector uploads for pre-existing WAV voice samples
549
+ async function handleFileSelect(e, speakerId) {
550
+ const file = e.target.files[0];
551
+ if (!file) return;
552
+
553
+ log("info", `Uploading selected file: ${file.name}`);
554
+ const name = speakerId === 0 ? spkName0.value : spkName1.value;
555
+ await uploadVoiceProfile(file, speakerId, name);
556
+ }
557
+
558
+ // ==========================================================================
559
+ // Speech Synthesis Triggers (REST vs WebSocket Stream)
560
+ // ==========================================================================
561
+
562
+ async function triggerSynthesis() {
563
+ const text = txtScript.value.trim();
564
+ if (!text) {
565
+ log("error", "Cannot synthesize speech. Script text field is empty!");
566
+ return;
567
+ }
568
+
569
+ initAudioContext();
570
+ stopPlayback(); // Clean up existing audio cycles
571
+
572
+ const isStream = chkStreamMode.checked;
573
+
574
+ const speaker_voices = {};
575
+ if (voicePaths[0]) speaker_voices["0"] = voicePaths[0];
576
+ if (voicePaths[1]) speaker_voices["1"] = voicePaths[1];
577
+
578
+ if (isStream) {
579
+ // Mode A: Real-Time WebSockets Streaming
580
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
581
+ log("error", "WebSocket channel is not active. Streaming offline.");
582
+ return;
583
+ }
584
+
585
+ startStreamingPlayback();
586
+
587
+ // Push script parameters to WebSocket
588
+ ws.send(JSON.stringify({
589
+ text: text,
590
+ voice_sample_path: voicePaths[0], // primary voice clone prompt path
591
+ speaker_id: 0,
592
+ speaker_voices: speaker_voices
593
+ }));
594
+
595
+ } else {
596
+ // Mode B: Standard Full WAV synthesis
597
+ try {
598
+ log("info", "Initiating complete script compilation... Please wait...");
599
+ setAgentOrbState("thinking", "COMPILING SCRIPT", "Executing neural modeling and saving audio...");
600
+ btnSynthesize.disabled = true;
601
+
602
+ const res = await fetch("/api/generate", {
603
+ method: "POST",
604
+ headers: { "Content-Type": "application/json" },
605
+ body: JSON.stringify({
606
+ text: text,
607
+ voice_sample_path: voicePaths[0],
608
+ speaker_id: 0,
609
+ speaker_voices: speaker_voices
610
+ })
611
+ });
612
+ const data = await res.json();
613
+
614
+ btnSynthesize.disabled = false;
615
+
616
+ if (data.status === "success") {
617
+ log("success", `Script generated successfully using engine mode: [${data.mode}]`);
618
+ loadTraditionalPlayer(data.audio_url);
619
+ } else {
620
+ throw new Error("API return code was not successful.");
621
+ }
622
+ } catch (e) {
623
+ btnSynthesize.disabled = false;
624
+ setAgentOrbState("idle", "AGENT READY", "Dialogue generation failed.");
625
+ log("error", `Synthesis generation failed: ${e.message}`);
626
+ }
627
+ }
628
+ }
629
+
630
+ // Controls standard player execution for downloaded WAV files
631
+ async function loadTraditionalPlayer(audioUrl) {
632
+ try {
633
+ log("info", "Loading generated voice track into audio player...");
634
+ const res = await fetch(audioUrl);
635
+ const arrayBuffer = await res.arrayBuffer();
636
+
637
+ audioCtx.decodeAudioData(arrayBuffer, (decodedBuffer) => {
638
+ currentAudioSource = audioCtx.createBufferSource();
639
+ currentAudioSource.buffer = decodedBuffer;
640
+
641
+ currentAudioSource.connect(gainNode);
642
+ currentAudioSource.start(0);
643
+
644
+ setAgentOrbState("speaking", "PLAYING AUDIO", "Playing compiled high-fidelity voice track...");
645
+ audioToolbar.classList.remove("hidden");
646
+ btnStop.disabled = false;
647
+ btnSynthesize.disabled = true;
648
+
649
+ // Set up player timeline meters
650
+ lblTimeStart.innerText = "00:00";
651
+ lblTimeEnd.innerText = formatTime(decodedBuffer.duration);
652
+ sliderTimeline.max = Math.floor(decodedBuffer.duration);
653
+ sliderTimeline.value = 0;
654
+
655
+ const startTime = audioCtx.currentTime;
656
+ const updateTimeline = () => {
657
+ if (currentAudioSource) {
658
+ const elapsed = audioCtx.currentTime - startTime;
659
+ sliderTimeline.value = Math.floor(elapsed);
660
+ progressTimelineFill.style.width = `${(elapsed / decodedBuffer.duration) * 100}%`;
661
+ lblTimeStart.innerText = formatTime(elapsed);
662
+
663
+ if (elapsed < decodedBuffer.duration) {
664
+ requestAnimationFrame(updateTimeline);
665
+ }
666
+ }
667
+ };
668
+
669
+ updateTimeline();
670
+
671
+ currentAudioSource.onended = () => {
672
+ stopPlayback();
673
+ };
674
+ }, (err) => {
675
+ log("error", "Error decoding audio buffer track.");
676
+ });
677
+ } catch(e) {
678
+ log("error", "Failed to compile compiled WAV player.");
679
+ }
680
+ }
681
+
682
+ function formatTime(seconds) {
683
+ const m = Math.floor(seconds / 60).toString().padStart(2, '0');
684
+ const s = Math.floor(seconds % 60).toString().padStart(2, '0');
685
+ return `${m}:${s}`;
686
+ }
687
+
688
+ // ==========================================================================
689
+ // Event Binding & Presets Loading (Dashboard HUD controls)
690
+ // ==========================================================================
691
+
692
+ function bindEvents() {
693
+ // Primary execution actions
694
+ btnSynthesize.addEventListener("click", triggerSynthesis);
695
+
696
+ // Voice profiling recording hooks
697
+ btnRecord0.addEventListener("click", () => setupRecording(0));
698
+ btnRecord1.addEventListener("click", () => setupRecording(1));
699
+
700
+ fileVoice0.addEventListener("change", (e) => handleFileSelect(e, 0));
701
+ fileVoice1.addEventListener("change", (e) => handleFileSelect(e, 1));
702
+
703
+ // Standard audio adjustment nodes
704
+ sliderVolume.addEventListener("input", (e) => {
705
+ setVolume(e.target.value / 100);
706
+ });
707
+
708
+ btnClearLogs.addEventListener("click", () => {
709
+ consoleBody.innerHTML = `<div class="log-line system">[System] Console buffer cleared.</div>`;
710
+ });
711
+
712
+ // Presets loaders
713
+ presetPodcast.addEventListener("click", () => {
714
+ txtScript.value = `Speaker 0: Welcome back to the Vibe Voice podcast segment. Today we are cloning human expressions.
715
+ Speaker 1: That is right! The 0.5B model manages this dialogue transition on a local CPU extremely fast.
716
+ Speaker 0: Incredible! It sounds like two real people sitting in this glassmorphic studio together.`;
717
+ log("info", "Loaded preset script: [Podcast Dialogue]");
718
+ });
719
+
720
+ presetAssistant.addEventListener("click", () => {
721
+ txtScript.value = `Speaker 0: Hello, I am your personalized voice assistant. How can I help you customize your Dine Direct orders today?`;
722
+ log("info", "Loaded preset script: [AI Voice Assistant]");
723
+ });
724
+
725
+ presetNews.addEventListener("click", () => {
726
+ txtScript.value = `Speaker 1: Breaking news from Microsoft Research. The Vibe Voice speech synthesis models are now live on local devices.`;
727
+ log("info", "Loaded preset script: [News Broadcast]");
728
+ });
729
+ }
static/index.css ADDED
@@ -0,0 +1,1067 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==========================================================================
2
+ VibeVoice Studio CSS Stylesheet
3
+ Design Paradigm: Deep Space Dark Mode Glassmorphism with Neon Accents
4
+ ========================================================================== */
5
+
6
+ :root {
7
+ --bg-dark: #07090e;
8
+ --card-bg: rgba(13, 17, 28, 0.45);
9
+ --card-border: rgba(255, 255, 255, 0.08);
10
+ --text-primary: #f0f3fa;
11
+ --text-secondary: #8e9bb5;
12
+
13
+ /* Neon Accents HSL */
14
+ --accent-cyan: hsl(185, 95%, 48%);
15
+ --accent-cyan-glow: hsla(185, 95%, 48%, 0.35);
16
+ --accent-purple: hsl(265, 88%, 64%);
17
+ --accent-purple-glow: hsla(265, 88%, 64%, 0.35);
18
+ --accent-pink: hsl(330, 92%, 60%);
19
+ --accent-pink-glow: hsla(330, 92%, 60%, 0.35);
20
+ --accent-green: hsl(145, 85%, 50%);
21
+
22
+ /* Transition Timings */
23
+ --ease-smooth: cubic-bezier(0.25, 0.8, 0.25, 1);
24
+ --transition-quick: 0.2s var(--ease-smooth);
25
+ --transition-mid: 0.4s var(--ease-smooth);
26
+ }
27
+
28
+ /* Reset & Core Styling */
29
+ * {
30
+ box-sizing: border-box;
31
+ margin: 0;
32
+ padding: 0;
33
+ }
34
+
35
+ body {
36
+ background-color: var(--bg-dark);
37
+ color: var(--text-primary);
38
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
39
+ min-height: 100vh;
40
+ overflow-x: hidden;
41
+ position: relative;
42
+ }
43
+
44
+ /* Stunning Floating Background Gradients */
45
+ .glow-bg {
46
+ position: fixed;
47
+ top: 0;
48
+ left: 0;
49
+ right: 0;
50
+ bottom: 0;
51
+ z-index: -1;
52
+ overflow: hidden;
53
+ }
54
+
55
+ .glow-bg::before, .glow-bg::after {
56
+ content: '';
57
+ position: absolute;
58
+ border-radius: 50%;
59
+ filter: blur(140px);
60
+ opacity: 0.15;
61
+ pointer-events: none;
62
+ }
63
+
64
+ .glow-bg::before {
65
+ background: radial-gradient(circle, var(--accent-cyan) 0%, transparent 70%);
66
+ width: 600px;
67
+ height: 600px;
68
+ top: -200px;
69
+ left: -150px;
70
+ animation: floatGlow 25s infinite alternate ease-in-out;
71
+ }
72
+
73
+ .glow-bg::after {
74
+ background: radial-gradient(circle, var(--accent-purple) 0%, transparent 70%);
75
+ width: 700px;
76
+ height: 700px;
77
+ bottom: -250px;
78
+ right: -150px;
79
+ animation: floatGlow 30s infinite alternate-reverse ease-in-out;
80
+ }
81
+
82
+ @keyframes floatGlow {
83
+ 0% { transform: translate(0, 0) scale(1); }
84
+ 50% { transform: translate(100px, 80px) scale(1.15); }
85
+ 100% { transform: translate(-50px, 150px) scale(0.9); }
86
+ }
87
+
88
+ /* Layout Containers */
89
+ .container {
90
+ width: 100%;
91
+ max-width: 1300px;
92
+ margin: 0 auto;
93
+ padding: 24px 16px;
94
+ display: flex;
95
+ flex-direction: column;
96
+ min-height: 100vh;
97
+ }
98
+
99
+ /* ==========================================================================
100
+ Header Styling
101
+ ========================================================================== */
102
+ .app-header {
103
+ display: flex;
104
+ justify-content: space-between;
105
+ align-items: center;
106
+ padding: 16px 24px;
107
+ background: rgba(10, 14, 26, 0.4);
108
+ border-radius: 16px;
109
+ border: 1px solid var(--card-border);
110
+ backdrop-filter: blur(16px);
111
+ margin-bottom: 24px;
112
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
113
+ }
114
+
115
+ .logo-area {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 14px;
119
+ }
120
+
121
+ .logo-icon {
122
+ font-size: 28px;
123
+ background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-purple) 100%);
124
+ -webkit-background-clip: text;
125
+ -webkit-text-fill-color: transparent;
126
+ filter: drop-shadow(0 0 10px var(--accent-cyan-glow));
127
+ }
128
+
129
+ .logo-text h1 {
130
+ font-size: 22px;
131
+ font-weight: 700;
132
+ letter-spacing: -0.5px;
133
+ color: var(--text-primary);
134
+ line-height: 1.1;
135
+ }
136
+
137
+ .logo-text h1 span {
138
+ background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-purple) 100%);
139
+ -webkit-background-clip: text;
140
+ -webkit-text-fill-color: transparent;
141
+ }
142
+
143
+ .logo-text p {
144
+ font-size: 11px;
145
+ color: var(--text-secondary);
146
+ text-transform: uppercase;
147
+ letter-spacing: 1px;
148
+ }
149
+
150
+ .status-controls {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 16px;
154
+ }
155
+
156
+ /* Model Loading Card widget */
157
+ .model-status-card {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 12px;
161
+ background: rgba(255, 255, 255, 0.04);
162
+ padding: 8px 14px;
163
+ border-radius: 12px;
164
+ border: 1px solid rgba(255, 255, 255, 0.05);
165
+ }
166
+
167
+ .status-dot {
168
+ width: 8px;
169
+ height: 8px;
170
+ border-radius: 50%;
171
+ display: inline-block;
172
+ }
173
+
174
+ .status-dot.green {
175
+ background-color: var(--accent-green);
176
+ box-shadow: 0 0 8px var(--accent-green);
177
+ }
178
+
179
+ .status-dot.yellow {
180
+ background-color: #ffb703;
181
+ box-shadow: 0 0 8px #ffb703;
182
+ }
183
+
184
+ .status-dot.red {
185
+ background-color: var(--accent-pink);
186
+ box-shadow: 0 0 8px var(--accent-pink);
187
+ }
188
+
189
+ .status-lbl {
190
+ font-size: 12px;
191
+ color: var(--text-secondary);
192
+ }
193
+
194
+ .status-lbl strong {
195
+ color: var(--text-primary);
196
+ }
197
+
198
+ /* Online/Offline connection badge */
199
+ .connection-badge {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 8px;
203
+ font-size: 12px;
204
+ font-weight: 500;
205
+ padding: 8px 14px;
206
+ border-radius: 12px;
207
+ background: rgba(255, 255, 255, 0.04);
208
+ border: 1px solid rgba(255, 255, 255, 0.05);
209
+ color: var(--text-secondary);
210
+ }
211
+
212
+ .connection-badge.online {
213
+ color: var(--accent-cyan);
214
+ background: rgba(0, 242, 254, 0.06);
215
+ border-color: rgba(0, 242, 254, 0.2);
216
+ filter: drop-shadow(0 0 4px rgba(0, 242, 254, 0.1));
217
+ }
218
+
219
+ /* ==========================================================================
220
+ Dashboard Grid Layout
221
+ ========================================================================== */
222
+ .dashboard-grid {
223
+ display: grid;
224
+ grid-template-columns: 360px 1fr;
225
+ gap: 24px;
226
+ flex: 1;
227
+ }
228
+
229
+ /* Premium Glassmorphic Cards */
230
+ .glass-card {
231
+ background: var(--card-bg);
232
+ border: 1px solid var(--card-border);
233
+ border-radius: 20px;
234
+ padding: 24px;
235
+ backdrop-filter: blur(24px);
236
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
237
+ position: relative;
238
+ overflow: hidden;
239
+ }
240
+
241
+ .panel-header {
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 12px;
245
+ margin-bottom: 8px;
246
+ }
247
+
248
+ .panel-header i {
249
+ font-size: 20px;
250
+ background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-purple) 100%);
251
+ -webkit-background-clip: text;
252
+ -webkit-text-fill-color: transparent;
253
+ }
254
+
255
+ .panel-header h2 {
256
+ font-size: 18px;
257
+ font-weight: 600;
258
+ }
259
+
260
+ .panel-desc {
261
+ font-size: 13px;
262
+ color: var(--text-secondary);
263
+ line-height: 1.5;
264
+ margin-bottom: 20px;
265
+ }
266
+
267
+ /* ==========================================================================
268
+ Speaker Profiles / Voice Cloning Center
269
+ ========================================================================== */
270
+ .speaker-profile-box {
271
+ background: rgba(255, 255, 255, 0.02);
272
+ border: 1px solid rgba(255, 255, 255, 0.04);
273
+ border-radius: 16px;
274
+ padding: 16px;
275
+ margin-bottom: 16px;
276
+ transition: var(--transition-mid);
277
+ }
278
+
279
+ .speaker-profile-box:hover {
280
+ border-color: rgba(255, 255, 255, 0.08);
281
+ background: rgba(255, 255, 255, 0.03);
282
+ }
283
+
284
+ .speaker-badge-header {
285
+ display: flex;
286
+ justify-content: space-between;
287
+ align-items: center;
288
+ margin-bottom: 14px;
289
+ }
290
+
291
+ .speaker-num {
292
+ font-size: 13px;
293
+ font-weight: 700;
294
+ color: var(--text-primary);
295
+ }
296
+
297
+ .badge {
298
+ font-size: 10px;
299
+ font-weight: 600;
300
+ padding: 3px 8px;
301
+ border-radius: 20px;
302
+ text-transform: uppercase;
303
+ }
304
+
305
+ .cyan-glow {
306
+ background: rgba(0, 242, 254, 0.08);
307
+ border: 1px solid rgba(0, 242, 254, 0.2);
308
+ color: var(--accent-cyan);
309
+ }
310
+
311
+ .purple-glow {
312
+ background: rgba(162, 89, 255, 0.08);
313
+ border: 1px solid rgba(162, 89, 255, 0.2);
314
+ color: var(--accent-purple);
315
+ }
316
+
317
+ .speaker-info {
318
+ display: flex;
319
+ flex-direction: column;
320
+ gap: 6px;
321
+ margin-bottom: 14px;
322
+ }
323
+
324
+ .speaker-info label {
325
+ font-size: 11px;
326
+ color: var(--text-secondary);
327
+ text-transform: uppercase;
328
+ letter-spacing: 0.5px;
329
+ }
330
+
331
+ .form-input {
332
+ background: rgba(0, 0, 0, 0.2);
333
+ border: 1px solid rgba(255, 255, 255, 0.06);
334
+ border-radius: 8px;
335
+ padding: 8px 12px;
336
+ font-size: 13px;
337
+ color: var(--text-primary);
338
+ transition: var(--transition-quick);
339
+ }
340
+
341
+ .form-input:focus {
342
+ border-color: var(--accent-cyan);
343
+ box-shadow: 0 0 8px var(--accent-cyan-glow);
344
+ outline: none;
345
+ }
346
+
347
+ .cloning-options {
348
+ display: grid;
349
+ grid-template-columns: 1fr 1fr;
350
+ gap: 8px;
351
+ }
352
+
353
+ .cloned-status {
354
+ display: flex;
355
+ flex-direction: column;
356
+ gap: 8px;
357
+ margin-top: 12px;
358
+ padding-top: 12px;
359
+ border-top: 1px dashed rgba(255, 255, 255, 0.06);
360
+ }
361
+
362
+ .lbl-status {
363
+ font-size: 11px;
364
+ color: var(--accent-green);
365
+ font-weight: 500;
366
+ }
367
+
368
+ .mini-player {
369
+ width: 100%;
370
+ height: 28px;
371
+ border-radius: 4px;
372
+ outline: none;
373
+ }
374
+
375
+ /* Preset Buttons */
376
+ .preset-box {
377
+ margin-top: 24px;
378
+ }
379
+
380
+ .preset-box h3 {
381
+ font-size: 13px;
382
+ font-weight: 600;
383
+ color: var(--text-secondary);
384
+ margin-bottom: 10px;
385
+ }
386
+
387
+ .preset-buttons {
388
+ display: flex;
389
+ flex-direction: column;
390
+ gap: 8px;
391
+ }
392
+
393
+ /* Buttons System */
394
+ .btn {
395
+ display: inline-flex;
396
+ align-items: center;
397
+ justify-content: center;
398
+ gap: 8px;
399
+ border-radius: 12px;
400
+ font-size: 14px;
401
+ font-weight: 500;
402
+ cursor: pointer;
403
+ transition: var(--transition-quick);
404
+ border: 1px solid transparent;
405
+ user-select: none;
406
+ }
407
+
408
+ .btn-primary {
409
+ background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-purple) 100%);
410
+ color: #000;
411
+ font-weight: 700;
412
+ box-shadow: 0 4px 15px rgba(0, 242, 254, 0.25);
413
+ }
414
+
415
+ .btn-primary:hover:not(:disabled) {
416
+ transform: translateY(-2px);
417
+ box-shadow: 0 6px 20px rgba(0, 242, 254, 0.4);
418
+ }
419
+
420
+ .btn-primary:active:not(:disabled) {
421
+ transform: translateY(0);
422
+ }
423
+
424
+ .btn-secondary {
425
+ background: rgba(255, 255, 255, 0.05);
426
+ color: var(--text-primary);
427
+ border: 1px solid rgba(255, 255, 255, 0.08);
428
+ }
429
+
430
+ .btn-secondary:hover:not(:disabled) {
431
+ background: rgba(255, 255, 255, 0.1);
432
+ border-color: rgba(255, 255, 255, 0.15);
433
+ }
434
+
435
+ .btn-outline {
436
+ background: transparent;
437
+ color: var(--text-primary);
438
+ border: 1px solid rgba(255, 255, 255, 0.1);
439
+ }
440
+
441
+ .btn-outline:hover:not(:disabled) {
442
+ border-color: var(--accent-cyan);
443
+ color: var(--accent-cyan);
444
+ background: rgba(0, 242, 254, 0.02);
445
+ }
446
+
447
+ .btn-toggle {
448
+ background: linear-gradient(135deg, rgba(162, 89, 255, 0.1) 0%, rgba(255, 92, 141, 0.1) 100%);
449
+ border: 1px solid rgba(162, 89, 255, 0.25);
450
+ color: var(--text-primary);
451
+ padding: 6px 12px;
452
+ font-size: 11px;
453
+ }
454
+
455
+ .btn-toggle:hover {
456
+ border-color: var(--accent-pink);
457
+ box-shadow: 0 0 8px var(--accent-pink-glow);
458
+ }
459
+
460
+ .btn-toggle.active {
461
+ background: linear-gradient(135deg, var(--accent-purple) 0%, var(--accent-pink) 100%);
462
+ color: #fff;
463
+ border-color: transparent;
464
+ }
465
+
466
+ .btn-preset {
467
+ width: 100%;
468
+ padding: 10px;
469
+ background: rgba(255, 255, 255, 0.02);
470
+ border: 1px solid rgba(255, 255, 255, 0.05);
471
+ justify-content: flex-start;
472
+ font-size: 12px;
473
+ color: var(--text-secondary);
474
+ }
475
+
476
+ .btn-preset:hover {
477
+ background: rgba(255, 255, 255, 0.05);
478
+ border-color: rgba(255, 255, 255, 0.1);
479
+ color: var(--text-primary);
480
+ }
481
+
482
+ .btn-sm {
483
+ padding: 8px 12px;
484
+ font-size: 12px;
485
+ border-radius: 8px;
486
+ }
487
+
488
+ .btn-xs {
489
+ padding: 6px 10px;
490
+ font-size: 11px;
491
+ border-radius: 6px;
492
+ }
493
+
494
+ .btn-lg {
495
+ padding: 14px 28px;
496
+ font-size: 15px;
497
+ border-radius: 14px;
498
+ }
499
+
500
+ .btn:disabled {
501
+ opacity: 0.4;
502
+ cursor: not-allowed;
503
+ box-shadow: none !important;
504
+ }
505
+
506
+ .btn-file {
507
+ position: relative;
508
+ overflow: hidden;
509
+ }
510
+
511
+ /* ==========================================================================
512
+ Execution Panel Right Column
513
+ ========================================================================== */
514
+ .execution-panel {
515
+ display: flex;
516
+ flex-direction: column;
517
+ gap: 24px;
518
+ }
519
+
520
+ /* ==========================================================================
521
+ Visualizer Core & Holographic Orb
522
+ ========================================================================== */
523
+ .visualizer-container {
524
+ display: flex;
525
+ flex-direction: column;
526
+ align-items: center;
527
+ justify-content: center;
528
+ padding: 30px;
529
+ min-height: 250px;
530
+ background: radial-gradient(circle at center, rgba(16, 24, 48, 0.3) 0%, var(--card-bg) 100%);
531
+ }
532
+
533
+ .agent-orb-wrapper {
534
+ display: flex;
535
+ align-items: center;
536
+ gap: 36px;
537
+ margin-bottom: 20px;
538
+ width: 100%;
539
+ max-width: 500px;
540
+ justify-content: center;
541
+ }
542
+
543
+ .agent-details {
544
+ display: flex;
545
+ flex-direction: column;
546
+ gap: 4px;
547
+ }
548
+
549
+ .agent-state-text {
550
+ font-family: 'Orbitron', monospace;
551
+ font-size: 18px;
552
+ font-weight: 700;
553
+ letter-spacing: 1px;
554
+ color: var(--text-primary);
555
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
556
+ transition: var(--transition-mid);
557
+ }
558
+
559
+ .agent-state-sub {
560
+ font-size: 12px;
561
+ color: var(--text-secondary);
562
+ }
563
+
564
+ /* The Animated Holographic Orb */
565
+ .agent-orb {
566
+ width: 90px;
567
+ height: 90px;
568
+ border-radius: 50%;
569
+ position: relative;
570
+ display: flex;
571
+ align-items: center;
572
+ justify-content: center;
573
+ transition: var(--transition-mid);
574
+ }
575
+
576
+ .orb-inner {
577
+ width: 60px;
578
+ height: 60px;
579
+ border-radius: 50%;
580
+ position: relative;
581
+ z-index: 5;
582
+ transition: var(--transition-mid);
583
+ }
584
+
585
+ .orb-wave {
586
+ position: absolute;
587
+ top: 0;
588
+ left: 0;
589
+ right: 0;
590
+ bottom: 0;
591
+ border-radius: 50%;
592
+ opacity: 0;
593
+ pointer-events: none;
594
+ z-index: 2;
595
+ }
596
+
597
+ .orb-glow {
598
+ position: absolute;
599
+ top: -15px;
600
+ left: -15px;
601
+ right: -15px;
602
+ bottom: -15px;
603
+ border-radius: 50%;
604
+ filter: blur(25px);
605
+ z-index: 1;
606
+ opacity: 0.5;
607
+ transition: var(--transition-mid);
608
+ }
609
+
610
+ /* ORB STATE: Idle (Slow Breathing) */
611
+ .agent-orb.idle .orb-inner {
612
+ background: radial-gradient(circle, var(--accent-cyan) 0%, rgba(0, 150, 255, 0.3) 100%);
613
+ box-shadow: 0 0 20px var(--accent-cyan-glow);
614
+ animation: orbPulse 4s infinite ease-in-out;
615
+ }
616
+
617
+ .agent-orb.idle .orb-glow {
618
+ background: var(--accent-cyan);
619
+ opacity: 0.3;
620
+ }
621
+
622
+ /* ORB STATE: Thinking (Vibrant Spinning Glow) */
623
+ .agent-orb.thinking .orb-inner {
624
+ background: radial-gradient(circle, #fff 0%, var(--accent-purple) 70%, var(--accent-cyan) 100%);
625
+ box-shadow: 0 0 35px var(--accent-purple-glow);
626
+ animation: orbSpin 2s infinite linear, orbPulse 1s infinite ease-in-out;
627
+ }
628
+
629
+ .agent-orb.thinking .orb-glow {
630
+ background: var(--accent-purple);
631
+ opacity: 0.6;
632
+ animation: orbGlowScale 1.5s infinite alternate ease-in-out;
633
+ }
634
+
635
+ /* ORB STATE: Speaking (Dynamic Ring Waves) */
636
+ .agent-orb.speaking .orb-inner {
637
+ background: radial-gradient(circle, #fff 0%, var(--accent-pink) 60%, var(--accent-purple) 100%);
638
+ box-shadow: 0 0 30px var(--accent-pink-glow);
639
+ animation: orbPulse 0.4s infinite ease-in-out;
640
+ }
641
+
642
+ .agent-orb.speaking .orb-glow {
643
+ background: var(--accent-pink);
644
+ opacity: 0.7;
645
+ }
646
+
647
+ .agent-orb.speaking .orb-wave {
648
+ border: 2.5px solid var(--accent-pink);
649
+ box-shadow: 0 0 10px var(--accent-pink-glow);
650
+ animation: waveExpand 1.2s infinite cubic-bezier(0.1, 0.8, 0.3, 1);
651
+ opacity: 1;
652
+ }
653
+
654
+ .agent-orb.speaking .orb-wave.wave-2 {
655
+ animation-delay: 0.6s;
656
+ }
657
+
658
+ /* ORB STATE: Listening (Green organic pulses) */
659
+ .agent-orb.listening .orb-inner {
660
+ background: radial-gradient(circle, #fff 0%, var(--accent-green) 70%);
661
+ box-shadow: 0 0 25px rgba(0, 230, 115, 0.4);
662
+ animation: orbPulse 1.2s infinite ease-in-out;
663
+ }
664
+
665
+ .agent-orb.listening .orb-glow {
666
+ background: var(--accent-green);
667
+ opacity: 0.4;
668
+ }
669
+
670
+ /* Keyframes animations */
671
+ @keyframes orbPulse {
672
+ 0% { transform: scale(1); }
673
+ 50% { transform: scale(1.08); }
674
+ 100% { transform: scale(1); }
675
+ }
676
+
677
+ @keyframes orbSpin {
678
+ 0% { transform: rotate(0deg); }
679
+ 100% { transform: rotate(360deg); }
680
+ }
681
+
682
+ @keyframes orbGlowScale {
683
+ 0% { transform: scale(0.95); opacity: 0.4; }
684
+ 100% { transform: scale(1.2); opacity: 0.75; }
685
+ }
686
+
687
+ @keyframes waveExpand {
688
+ 0% { transform: scale(0.9); opacity: 0.9; }
689
+ 100% { transform: scale(1.8); opacity: 0; }
690
+ }
691
+
692
+ /* Real-Time Waveform visualizer container */
693
+ .visualizer-canvas-box {
694
+ width: 100%;
695
+ display: flex;
696
+ justify-content: center;
697
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
698
+ padding-top: 16px;
699
+ margin-top: 10px;
700
+ }
701
+
702
+ #canvasWaveform {
703
+ width: 100%;
704
+ max-width: 600px;
705
+ height: 75px;
706
+ }
707
+
708
+ /* ==========================================================================
709
+ Script Editor Studio
710
+ ========================================================================== */
711
+ .script-editor-card {
712
+ display: flex;
713
+ flex-direction: column;
714
+ gap: 16px;
715
+ }
716
+
717
+ .editor-header {
718
+ display: flex;
719
+ justify-content: space-between;
720
+ align-items: center;
721
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
722
+ padding-bottom: 14px;
723
+ }
724
+
725
+ .title-with-desc {
726
+ display: flex;
727
+ align-items: center;
728
+ gap: 10px;
729
+ }
730
+
731
+ .title-with-desc i {
732
+ font-size: 18px;
733
+ color: var(--accent-purple);
734
+ }
735
+
736
+ .title-with-desc h2 {
737
+ font-size: 16px;
738
+ font-weight: 600;
739
+ }
740
+
741
+ /* Real-time Streaming Switch */
742
+ .toggle-mode-container {
743
+ display: flex;
744
+ align-items: center;
745
+ gap: 10px;
746
+ cursor: pointer;
747
+ user-select: none;
748
+ }
749
+
750
+ .toggle-mode-container input {
751
+ display: none;
752
+ }
753
+
754
+ .toggle-slider {
755
+ width: 42px;
756
+ height: 22px;
757
+ background-color: rgba(255, 255, 255, 0.08);
758
+ border-radius: 20px;
759
+ position: relative;
760
+ border: 1px solid rgba(255, 255, 255, 0.1);
761
+ transition: var(--transition-quick);
762
+ }
763
+
764
+ .toggle-slider::before {
765
+ content: '';
766
+ position: absolute;
767
+ width: 16px;
768
+ height: 16px;
769
+ background-color: var(--text-primary);
770
+ border-radius: 50%;
771
+ top: 2px;
772
+ left: 2px;
773
+ transition: var(--transition-quick);
774
+ }
775
+
776
+ .toggle-mode-container input:checked + .toggle-slider {
777
+ background-color: var(--accent-cyan-glow);
778
+ border-color: var(--accent-cyan);
779
+ }
780
+
781
+ .toggle-mode-container input:checked + .toggle-slider::before {
782
+ transform: translateX(20px);
783
+ background-color: var(--accent-cyan);
784
+ box-shadow: 0 0 6px var(--accent-cyan);
785
+ }
786
+
787
+ .toggle-label {
788
+ font-size: 12px;
789
+ font-weight: 500;
790
+ color: var(--text-secondary);
791
+ }
792
+
793
+ .toggle-mode-container input:checked ~ .toggle-label {
794
+ color: var(--accent-cyan);
795
+ }
796
+
797
+ /* Script Box Elements */
798
+ .script-area-wrapper {
799
+ background: rgba(0, 0, 0, 0.25);
800
+ border: 1px solid var(--card-border);
801
+ border-radius: 12px;
802
+ overflow: hidden;
803
+ }
804
+
805
+ .script-textarea {
806
+ width: 100%;
807
+ height: 180px;
808
+ background: transparent;
809
+ border: none;
810
+ resize: none;
811
+ padding: 16px;
812
+ color: var(--text-primary);
813
+ font-family: 'Inter', monospace;
814
+ font-size: 14px;
815
+ line-height: 1.6;
816
+ outline: none;
817
+ }
818
+
819
+ .script-textarea::placeholder {
820
+ color: rgba(255, 255, 255, 0.2);
821
+ }
822
+
823
+ .execution-actions {
824
+ display: flex;
825
+ gap: 16px;
826
+ }
827
+
828
+ .execution-actions .btn {
829
+ flex: 1;
830
+ }
831
+
832
+ /* ==========================================================================
833
+ Premium Audio Timeline Player Toolbar
834
+ ========================================================================== */
835
+ .audio-control-bar {
836
+ padding: 16px 24px;
837
+ background: rgba(13, 17, 28, 0.7);
838
+ border-color: rgba(0, 242, 254, 0.15);
839
+ box-shadow: 0 8px 32px rgba(0, 242, 254, 0.05);
840
+ }
841
+
842
+ .playback-controls {
843
+ display: flex;
844
+ align-items: center;
845
+ gap: 20px;
846
+ width: 100%;
847
+ }
848
+
849
+ .playback-btn {
850
+ width: 36px;
851
+ height: 36px;
852
+ border-radius: 50%;
853
+ border: 1px solid rgba(255, 255, 255, 0.1);
854
+ background: rgba(255, 255, 255, 0.05);
855
+ color: var(--text-primary);
856
+ cursor: pointer;
857
+ display: flex;
858
+ align-items: center;
859
+ justify-content: center;
860
+ transition: var(--transition-quick);
861
+ }
862
+
863
+ .playback-btn:hover {
864
+ background: var(--accent-cyan);
865
+ border-color: transparent;
866
+ color: #000;
867
+ box-shadow: 0 0 10px var(--accent-cyan-glow);
868
+ }
869
+
870
+ .timeline-container {
871
+ display: flex;
872
+ align-items: center;
873
+ gap: 12px;
874
+ flex: 1;
875
+ }
876
+
877
+ .time-stamp {
878
+ font-family: 'Orbitron', monospace;
879
+ font-size: 11px;
880
+ color: var(--text-secondary);
881
+ min-width: 40px;
882
+ }
883
+
884
+ .progress-bar-wrapper {
885
+ position: relative;
886
+ flex: 1;
887
+ height: 6px;
888
+ display: flex;
889
+ align-items: center;
890
+ }
891
+
892
+ /* Sliders Styling (Cross-Browser) */
893
+ .slider-theme {
894
+ -webkit-appearance: none;
895
+ width: 100%;
896
+ height: 4px;
897
+ background: rgba(255, 255, 255, 0.08);
898
+ border-radius: 10px;
899
+ outline: none;
900
+ cursor: pointer;
901
+ position: relative;
902
+ z-index: 3;
903
+ }
904
+
905
+ .slider-theme::-webkit-slider-thumb {
906
+ -webkit-appearance: none;
907
+ width: 12px;
908
+ height: 12px;
909
+ border-radius: 50%;
910
+ background-color: var(--accent-cyan);
911
+ box-shadow: 0 0 6px var(--accent-cyan);
912
+ transition: 0.15s;
913
+ }
914
+
915
+ .slider-theme::-webkit-slider-thumb:hover {
916
+ transform: scale(1.3);
917
+ }
918
+
919
+ .progress-fill {
920
+ position: absolute;
921
+ left: 0;
922
+ top: 50%;
923
+ transform: translateY(-50%);
924
+ height: 4px;
925
+ background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));
926
+ border-radius: 10px;
927
+ pointer-events: none;
928
+ z-index: 1;
929
+ }
930
+
931
+ .volume-container {
932
+ display: flex;
933
+ align-items: center;
934
+ gap: 10px;
935
+ width: 130px;
936
+ }
937
+
938
+ .volume-container i {
939
+ color: var(--text-secondary);
940
+ font-size: 13px;
941
+ width: 16px;
942
+ }
943
+
944
+ .slider-volume {
945
+ width: 90px;
946
+ }
947
+
948
+ .speed-selector {
949
+ background: rgba(255, 255, 255, 0.05);
950
+ border: 1px solid rgba(255, 255, 255, 0.08);
951
+ border-radius: 8px;
952
+ padding: 6px 10px;
953
+ font-size: 12px;
954
+ color: var(--text-primary);
955
+ outline: none;
956
+ cursor: pointer;
957
+ }
958
+
959
+ .speed-selector option {
960
+ background-color: var(--bg-dark);
961
+ }
962
+
963
+ .hidden {
964
+ display: none !important;
965
+ }
966
+
967
+ /* ==========================================================================
968
+ Console Log Viewer
969
+ ========================================================================== */
970
+ .console-card {
971
+ background: rgba(4, 5, 8, 0.7);
972
+ border: 1px solid rgba(255, 255, 255, 0.04);
973
+ }
974
+
975
+ .console-header {
976
+ display: flex;
977
+ justify-content: space-between;
978
+ align-items: center;
979
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
980
+ padding-bottom: 8px;
981
+ margin-bottom: 10px;
982
+ font-size: 12px;
983
+ font-weight: 600;
984
+ color: var(--text-secondary);
985
+ }
986
+
987
+ .console-header i {
988
+ color: var(--accent-cyan);
989
+ margin-right: 6px;
990
+ }
991
+
992
+ .console-body {
993
+ height: 110px;
994
+ overflow-y: auto;
995
+ font-family: 'Orbitron', monospace;
996
+ font-size: 11px;
997
+ line-height: 1.8;
998
+ display: flex;
999
+ flex-direction: column;
1000
+ gap: 4px;
1001
+ padding-right: 6px;
1002
+ }
1003
+
1004
+ /* Custom Scrollbar for console and script text areas */
1005
+ .console-body::-webkit-scrollbar, .script-textarea::-webkit-scrollbar {
1006
+ width: 5px;
1007
+ }
1008
+
1009
+ .console-body::-webkit-scrollbar-track, .script-textarea::-webkit-scrollbar-track {
1010
+ background: transparent;
1011
+ }
1012
+
1013
+ .console-body::-webkit-scrollbar-thumb, .script-textarea::-webkit-scrollbar-thumb {
1014
+ background: rgba(255, 255, 255, 0.08);
1015
+ border-radius: 10px;
1016
+ }
1017
+
1018
+ .log-line {
1019
+ padding: 2px 6px;
1020
+ border-radius: 4px;
1021
+ word-break: break-all;
1022
+ }
1023
+
1024
+ .log-line.system {
1025
+ color: var(--text-secondary);
1026
+ background: rgba(255, 255, 255, 0.02);
1027
+ }
1028
+
1029
+ .log-line.info {
1030
+ color: var(--accent-cyan);
1031
+ background: rgba(0, 242, 254, 0.02);
1032
+ }
1033
+
1034
+ .log-line.success {
1035
+ color: var(--accent-green);
1036
+ background: rgba(0, 230, 115, 0.02);
1037
+ }
1038
+
1039
+ .log-line.error {
1040
+ color: var(--accent-pink);
1041
+ background: rgba(255, 92, 141, 0.02);
1042
+ }
1043
+
1044
+ .log-line.word {
1045
+ color: #fff;
1046
+ font-weight: 500;
1047
+ text-shadow: 0 0 6px rgba(255, 255, 255, 0.2);
1048
+ }
1049
+
1050
+ /* ==========================================================================
1051
+ Footer
1052
+ ========================================================================== */
1053
+ .app-footer {
1054
+ text-align: center;
1055
+ margin-top: 40px;
1056
+ padding-top: 20px;
1057
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
1058
+ font-size: 12px;
1059
+ color: var(--text-secondary);
1060
+ }
1061
+
1062
+ /* Responsive Scaling */
1063
+ @media (max-width: 1024px) {
1064
+ .dashboard-grid {
1065
+ grid-template-columns: 1fr;
1066
+ }
1067
+ }
static/index.html ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VibeVoice Agent - Microsoft Research Voice Synthesis Studio</title>
7
+ <meta name="description" content="A state-of-the-art voice agent cloning and synthesis platform powered by Microsoft VibeVoice models with real-time streaming and custom speaker cloning.">
8
+ <!-- Google Fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Orbitron:wght@500;700;900&display=swap" rel="stylesheet">
12
+ <!-- FontAwesome Icons -->
13
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
14
+ <!-- Main Style Sheet -->
15
+ <link rel="stylesheet" href="/index.css">
16
+ </head>
17
+ <body>
18
+ <div class="glow-bg"></div>
19
+ <div class="container">
20
+
21
+ <!-- Header Section -->
22
+ <header class="app-header">
23
+ <div class="logo-area">
24
+ <i class="fa-solid fa-wand-magic-sparkles logo-icon"></i>
25
+ <div class="logo-text">
26
+ <h1>VibeVoice<span>Studio</span></h1>
27
+ <p>Microsoft Frontier Speech Synthesis</p>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="status-controls">
32
+ <!-- AI Model Loader Status -->
33
+ <div class="model-status-card" id="modelStatusCard">
34
+ <span class="status-dot green animate-pulse" id="modelDot"></span>
35
+ <span class="status-lbl">Model: <strong id="modelStatusVal">Demo Synthesizer</strong></span>
36
+ <button class="btn btn-toggle" id="btnToggleModel" title="Switch between fast local synthesizer & full VibeVoice weights">
37
+ <i class="fa-solid fa-power-off"></i> Toggle AI Model
38
+ </button>
39
+ </div>
40
+
41
+ <!-- Websocket Connection Status -->
42
+ <div class="connection-badge" id="connBadge">
43
+ <i class="fa-solid fa-link-slash icon" id="connIcon"></i>
44
+ <span id="connText">Offline</span>
45
+ </div>
46
+ </div>
47
+ </header>
48
+
49
+ <!-- Main Dashboard Layout -->
50
+ <main class="dashboard-grid">
51
+
52
+ <!-- Left Column: Voice Profiles & Configurations -->
53
+ <section class="control-panel glass-card">
54
+ <div class="panel-header">
55
+ <i class="fa-solid fa-user-gear"></i>
56
+ <h2>Voice Cloning Center</h2>
57
+ </div>
58
+
59
+ <p class="panel-desc">Clone any voice in seconds. Record or upload a 10-30s audio sample to create a speaker profile prompt.</p>
60
+
61
+ <!-- Speaker Settings Slot 1 -->
62
+ <div class="speaker-profile-box" id="speakerSlot0">
63
+ <div class="speaker-badge-header">
64
+ <span class="speaker-num">Speaker 0</span>
65
+ <div class="badge cyan-glow">Primary Speaker</div>
66
+ </div>
67
+
68
+ <div class="speaker-info">
69
+ <label for="spkName0">Speaker Name</label>
70
+ <input type="text" id="spkName0" value="Alex" class="form-input">
71
+ </div>
72
+
73
+ <div class="cloning-options">
74
+ <button class="btn btn-sm btn-outline" id="btnRecord0">
75
+ <i class="fa-solid fa-microphone"></i> Record Mic
76
+ </button>
77
+ <label class="btn btn-sm btn-outline btn-file">
78
+ <i class="fa-solid fa-upload"></i> Upload WAV
79
+ <input type="file" id="fileVoice0" accept="audio/wav,audio/mp3" style="display: none;">
80
+ </label>
81
+ </div>
82
+
83
+ <div class="cloned-status hidden" id="clonedStatus0">
84
+ <span class="lbl-status"><i class="fa-solid fa-check-double"></i> Voice Cloned:</span>
85
+ <audio id="audioPreview0" controls class="mini-player"></audio>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Speaker Settings Slot 2 -->
90
+ <div class="speaker-profile-box" id="speakerSlot1">
91
+ <div class="speaker-badge-header">
92
+ <span class="speaker-num">Speaker 1</span>
93
+ <div class="badge purple-glow">Dialogue Partner</div>
94
+ </div>
95
+
96
+ <div class="speaker-info">
97
+ <label for="spkName1">Speaker Name</label>
98
+ <input type="text" id="spkName1" value="Sarah" class="form-input">
99
+ </div>
100
+
101
+ <div class="cloning-options">
102
+ <button class="btn btn-sm btn-outline" id="btnRecord1">
103
+ <i class="fa-solid fa-microphone"></i> Record Mic
104
+ </button>
105
+ <label class="btn btn-sm btn-outline btn-file">
106
+ <i class="fa-solid fa-upload"></i> Upload WAV
107
+ <input type="file" id="fileVoice1" accept="audio/wav,audio/mp3" style="display: none;">
108
+ </label>
109
+ </div>
110
+
111
+ <div class="cloned-status hidden" id="clonedStatus1">
112
+ <span class="lbl-status"><i class="fa-solid fa-check-double"></i> Voice Cloned:</span>
113
+ <audio id="audioPreview1" controls class="mini-player"></audio>
114
+ </div>
115
+ </div>
116
+
117
+ <!-- Presets & Sample Scripts -->
118
+ <div class="preset-box">
119
+ <h3>Dialogue Presets</h3>
120
+ <div class="preset-buttons">
121
+ <button class="btn btn-xs btn-preset" id="presetPodcast">
122
+ <i class="fa-solid fa-podcast"></i> Podcast Host
123
+ </button>
124
+ <button class="btn btn-xs btn-preset" id="presetAssistant">
125
+ <i class="fa-solid fa-headset"></i> AI Voice Agent
126
+ </button>
127
+ <button class="btn btn-xs btn-preset" id="presetNews">
128
+ <i class="fa-solid fa-newspaper"></i> News Broadcast
129
+ </button>
130
+ </div>
131
+ </div>
132
+ </section>
133
+
134
+ <!-- Right Column: Interactive Script Synthesizer & Visualizer -->
135
+ <section class="execution-panel">
136
+
137
+ <!-- Main Visualizer Core Box -->
138
+ <div class="visualizer-container glass-card">
139
+ <!-- Holographic Pulsing Agent Orb -->
140
+ <div class="agent-orb-wrapper">
141
+ <div class="agent-orb idle" id="agentOrb">
142
+ <div class="orb-inner"></div>
143
+ <div class="orb-wave"></div>
144
+ <div class="orb-wave wave-2"></div>
145
+ <div class="orb-glow"></div>
146
+ </div>
147
+ <div class="agent-details">
148
+ <span class="agent-state-text" id="agentStateText">AGENT READY</span>
149
+ <p class="agent-state-sub" id="agentStateSub">Configure scripts and trigger generation below</p>
150
+ </div>
151
+ </div>
152
+
153
+ <!-- Canvas Audio Visualizer -->
154
+ <div class="visualizer-canvas-box">
155
+ <canvas id="canvasWaveform" width="600" height="90"></canvas>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Script Editor Studio -->
160
+ <div class="script-editor-card glass-card">
161
+ <div class="editor-header">
162
+ <div class="title-with-desc">
163
+ <i class="fa-solid fa-pen-nib"></i>
164
+ <h2>Podcast Script Studio</h2>
165
+ </div>
166
+
167
+ <!-- Synthesis Type Controls -->
168
+ <div class="synthesis-mode-selector">
169
+ <label class="toggle-mode-container" title="Streams audio in real-time as words are spoken">
170
+ <input type="checkbox" id="chkStreamMode" checked>
171
+ <span class="toggle-slider"></span>
172
+ <span class="toggle-label"><i class="fa-solid fa-bolt"></i> Real-time Streaming</span>
173
+ </label>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="script-area-wrapper">
178
+ <textarea id="txtScript" placeholder="Write speaker turn-taking script...
179
+ Format: Speaker ID: Text content
180
+ Example:
181
+ Speaker 0: Hello, how are you?
182
+ Speaker 1: I am doing fantastic! Let's clone some voices." class="script-textarea">Speaker 0: Welcome to Vibe Voice Studio. I am cloned from your custom voice sample.
183
+ Speaker 1: That is incredible! The transition between speakers sounds so smooth and natural.
184
+ Speaker 0: Yes, Microsoft's continuous acoustic tokenizers make multi-speaker podcasts sound beautiful.</textarea>
185
+ </div>
186
+
187
+ <!-- Execution Buttons -->
188
+ <div class="execution-actions">
189
+ <button class="btn btn-primary btn-lg" id="btnSynthesize">
190
+ <i class="fa-solid fa-circle-play icon-play"></i> Synthesize Speech
191
+ </button>
192
+ <button class="btn btn-secondary btn-lg" id="btnStop" disabled>
193
+ <i class="fa-solid fa-circle-stop"></i> Stop Agent
194
+ </button>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Custom Audio Control Toolbar -->
199
+ <div class="audio-control-bar glass-card hidden" id="audioToolbar">
200
+ <div class="playback-controls">
201
+ <button class="playback-btn" id="btnToolbarPlay" title="Play / Pause"><i class="fa-solid fa-play"></i></button>
202
+
203
+ <div class="timeline-container">
204
+ <span class="time-stamp" id="lblTimeStart">00:00</span>
205
+ <div class="progress-bar-wrapper">
206
+ <input type="range" id="sliderTimeline" min="0" max="100" value="0" class="slider-theme">
207
+ <div class="progress-fill" id="progressTimelineFill"></div>
208
+ </div>
209
+ <span class="time-stamp" id="lblTimeEnd">00:00</span>
210
+ </div>
211
+
212
+ <!-- Volume Controls -->
213
+ <div class="volume-container">
214
+ <i class="fa-solid fa-volume-high" id="iconVolume"></i>
215
+ <input type="range" id="sliderVolume" min="0" max="100" value="80" class="slider-theme slider-volume">
216
+ </div>
217
+
218
+ <!-- Speed Controls -->
219
+ <select id="selectSpeed" class="speed-selector">
220
+ <option value="0.75">0.75x</option>
221
+ <option value="1.0" selected>1.0x</option>
222
+ <option value="1.25">1.25x</option>
223
+ <option value="1.5">1.5x</option>
224
+ </select>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Debug Console Log Viewer -->
229
+ <div class="console-card glass-card">
230
+ <div class="console-header">
231
+ <span><i class="fa-solid fa-terminal"></i> Agent Console Logs</span>
232
+ <button class="btn btn-xs btn-outline" id="btnClearLogs">Clear</button>
233
+ </div>
234
+ <div class="console-body" id="consoleBody">
235
+ <div class="log-line system">[System] Welcome to VibeVoice Agent Console. System ready.</div>
236
+ <div class="log-line info">[Info] Default mode: Simulated Synthesizer (runs instantly on CPU). Click 'Toggle AI Model' to load the full 0.5B neural net!</div>
237
+ </div>
238
+ </div>
239
+
240
+ </section>
241
+ </main>
242
+
243
+ <!-- Footer -->
244
+ <footer class="app-footer">
245
+ <p>VibeVoice Voice Synthesis Platform © 2026. Built with Microsoft Research VibeVoice Models & FastAPI.</p>
246
+ </footer>
247
+ </div>
248
+
249
+ <!-- Script assets -->
250
+ <script src="/app.js"></script>
251
+ </body>
252
+ </html>