Spaces:
Sleeping
Sleeping
File size: 13,454 Bytes
b8b3edf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
import React, { useState, useEffect, useMemo } from 'react';
import {
Globe,
Volume2,
Mic2,
Settings2,
Loader2,
Zap,
Sparkles,
MessageSquare,
ArrowRight,
Users,
ChevronRight,
Activity,
ShieldCheck,
Search,
Filter,
X
} from 'lucide-react';
import { synthesizeSpeech, TTS_VOICES, TTS_LANGUAGES, callGemini } from '../services/geminiService';
const Broadcast: React.FC = () => {
const [activeTab, setActiveTab] = useState<'single' | 'multi'>('single');
const [selectedLang, setSelectedLang] = useState(TTS_LANGUAGES.find(l => l.code === 'en')!);
const [selectedVoice, setSelectedVoice] = useState(TTS_VOICES[0]);
const [directorNotes, setDirectorNotes] = useState('Professional, authoritative corporate tone.');
const [transcript, setTranscript] = useState("Greetings, this is a global institutional broadcast via the Lumina Quantum Node. System parity achieved.");
const [isSynthesizing, setIsSynthesizing] = useState(false);
// Multi-speaker state
const [msConfig, setMsConfig] = useState({
speaker1: 'Alex',
voice1: 'Kore',
speaker2: 'Lumina',
voice2: 'Zephyr'
});
const [searchTerm, setSearchTerm] = useState('');
const filteredLangs = useMemo(() =>
TTS_LANGUAGES.filter(l => l.name.toLowerCase().includes(searchTerm.toLowerCase())),
[searchTerm]);
const handleGenerateNotes = async () => {
try {
const response = await callGemini('gemini-3-flash-preview',
`Generate professional, detailed director's notes for a TTS reading in ${selectedLang.name}. The theme is "Quantum Financial Hub Status Update". Keep it to 2 sentences. tone should be ${selectedVoice.style}.`
);
setDirectorNotes(response.text || '');
} catch (e) {
console.error(e);
}
};
const handleBroadcast = async () => {
setIsSynthesizing(true);
const success = await synthesizeSpeech({
text: transcript,
voiceName: selectedVoice.name,
directorNotes: directorNotes,
multiSpeaker: activeTab === 'multi' ? msConfig : undefined
});
setIsSynthesizing(false);
};
return (
<div className="space-y-10 animate-in fade-in duration-700 pb-20 max-w-7xl mx-auto">
{/* Header */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div>
<h2 className="text-4xl font-black text-white italic tracking-tighter uppercase mb-2">Global <span className="text-blue-500 not-italic">Broadcast</span></h2>
<p className="text-zinc-500 text-[10px] font-black uppercase tracking-[0.3em] flex items-center gap-2">
<Globe size={12} className="text-blue-500" />
Neural Voice Synthesis • 80+ Protocols Online
</p>
</div>
<div className="flex bg-black p-1.5 rounded-2xl border border-zinc-900">
<button
onClick={() => setActiveTab('single')}
className={`px-8 py-2.5 rounded-xl text-[10px] font-black transition-all uppercase tracking-widest flex items-center gap-3 ${activeTab === 'single' ? 'bg-zinc-100 text-black' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<Mic2 size={14} /> Single Node
</button>
<button
onClick={() => setActiveTab('multi')}
className={`px-8 py-2.5 rounded-xl text-[10px] font-black transition-all uppercase tracking-widest flex items-center gap-3 ${activeTab === 'multi' ? 'bg-zinc-100 text-black' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<Users size={14} /> Multi Speaker
</button>
</div>
</div>
<div className="grid grid-cols-12 gap-8">
{/* Left: Language & Voice Registry */}
<div className="col-span-12 lg:col-span-4 space-y-8">
<div className="bg-zinc-950 border border-zinc-900 rounded-[3rem] p-8 shadow-2xl h-[calc(100vh-350px)] flex flex-col relative overflow-hidden">
<div className="absolute top-0 right-0 p-6 opacity-[0.02]">
<Globe size={180} />
</div>
<div className="relative z-10 mb-8 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-white font-black text-xs uppercase tracking-widest italic">Protocol Registry</h3>
<span className="text-[9px] font-black text-zinc-500 bg-zinc-900 px-3 py-1 rounded-full">{filteredLangs.length} Nodes</span>
</div>
<div className="relative">
<Search size={14} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600" />
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search Language Node..."
className="w-full bg-black border border-zinc-800 rounded-xl py-3 pl-11 pr-4 text-white text-[10px] font-black uppercase tracking-widest outline-none focus:border-blue-500 transition-all"
/>
</div>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar pr-2 space-y-2 relative z-10">
{filteredLangs.map(lang => (
<button
key={lang.code}
onClick={() => setSelectedLang(lang)}
className={`w-full flex items-center justify-between p-4 rounded-2xl border transition-all ${selectedLang.code === lang.code ? 'bg-blue-600/10 border-blue-500/50 text-blue-400' : 'bg-black border-zinc-900 text-zinc-600 hover:border-zinc-700'}`}
>
<span className="text-[10px] font-black uppercase tracking-widest">{lang.name}</span>
<span className="text-[9px] mono font-bold opacity-40">{lang.code.toUpperCase()}</span>
</button>
))}
</div>
</div>
</div>
{/* Center: Command Center */}
<div className="col-span-12 lg:col-span-8 space-y-8">
<div className="bg-zinc-950 border border-zinc-900 rounded-[3.5rem] p-10 shadow-2xl relative overflow-hidden group">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_0%,_#1e1b4b_0%,_transparent_60%)] opacity-30"></div>
<div className="relative z-10 space-y-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<div className="w-16 h-16 bg-blue-600/10 text-blue-500 border border-blue-500/20 rounded-3xl flex items-center justify-center">
<Activity size={32} className="animate-pulse" />
</div>
<div>
<h4 className="text-2xl font-black text-white italic tracking-tighter uppercase leading-none">{selectedLang.name} Link</h4>
<p className="text-[10px] text-zinc-500 font-black uppercase tracking-widest mt-2">Active Node: {selectedLang.code.toUpperCase()}_PARITY</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="px-6 py-2 bg-black border border-zinc-900 rounded-full flex items-center gap-3">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-[9px] font-black text-zinc-500 uppercase tracking-widest">Neural Sync Stable</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<div className="space-y-3">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Voice Profile</label>
<select
value={selectedVoice.name}
onChange={(e) => setSelectedVoice(TTS_VOICES.find(v => v.name === e.target.value)!)}
className="w-full bg-black border border-zinc-900 rounded-2xl py-4 px-6 text-white text-[11px] font-black uppercase tracking-widest outline-none focus:border-blue-500 transition-all appearance-none cursor-pointer"
>
{TTS_VOICES.map(v => (
<option key={v.name} value={v.name}>{v.name} — {v.style}</option>
))}
</select>
</div>
<div className="space-y-3">
<div className="flex justify-between items-end mb-1">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Directorial Notes</label>
<button onClick={handleGenerateNotes} className="text-[9px] font-black text-blue-500 uppercase tracking-widest hover:text-white transition-colors flex items-center gap-2">
<Sparkles size={10} /> Neural Architect
</button>
</div>
<textarea
value={directorNotes}
onChange={(e) => setDirectorNotes(e.target.value)}
className="w-full bg-black border border-zinc-900 rounded-2xl p-6 text-zinc-300 text-[11px] font-medium leading-relaxed italic h-32 outline-none focus:border-blue-500 transition-all resize-none"
/>
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Transmission Script</label>
<div className="relative">
<textarea
value={transcript}
onChange={(e) => setTranscript(e.target.value)}
className="w-full bg-black border-2 border-zinc-900 rounded-[2.5rem] p-8 text-white text-base font-bold leading-relaxed tracking-tight h-[280px] outline-none focus:border-blue-500 transition-all resize-none placeholder:text-zinc-800"
placeholder="Input the global broadcast content..."
/>
<div className="absolute bottom-6 right-8 text-[9px] font-black text-zinc-700 uppercase tracking-widest">
{transcript.length} Characters
</div>
</div>
</div>
</div>
<div className="pt-4">
<button
onClick={handleBroadcast}
disabled={isSynthesizing || !transcript.trim()}
className="w-full py-7 bg-blue-600 hover:bg-blue-500 disabled:bg-zinc-900 disabled:text-zinc-700 text-white rounded-[2.5rem] font-black text-sm uppercase tracking-[0.4em] transition-all flex items-center justify-center gap-6 shadow-2xl shadow-blue-900/40 group"
>
{isSynthesizing ? (
<>
<Loader2 className="animate-spin" size={24} />
<span>Generating Global Waveform...</span>
</>
) : (
<>
<Zap size={24} className="group-hover:scale-125 transition-transform" />
<span>Execute Multi-Node Broadcast</span>
<ArrowRight size={20} className="group-hover:translate-x-3 transition-transform" />
</>
)}
</button>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="bg-zinc-950 border border-zinc-900 rounded-[2.5rem] p-8 shadow-xl flex items-center gap-6 group hover:border-blue-500/20 transition-all">
<div className="p-4 bg-blue-600/10 text-blue-500 rounded-2xl group-hover:scale-110 transition-transform">
<ShieldCheck size={24} />
</div>
<div>
<h5 className="text-white font-black text-xs uppercase italic">Verified Logic</h5>
<p className="text-[10px] text-zinc-600 font-bold uppercase mt-1">Zero-persistence audio packet shredding enabled.</p>
</div>
</div>
<div className="bg-zinc-950 border border-zinc-900 rounded-[2.5rem] p-8 shadow-xl flex items-center gap-6 group hover:border-emerald-500/20 transition-all">
<div className="p-4 bg-emerald-500/10 text-emerald-500 rounded-2xl group-hover:scale-110 transition-transform">
<Users size={24} />
</div>
<div>
<h5 className="text-white font-black text-xs uppercase italic">Directorial Access</h5>
<p className="text-[10px] text-zinc-600 font-bold uppercase mt-1">Fine-grained accent and pace control active.</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Broadcast;
|