Spaces:
Sleeping
Sleeping
| 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; | |