YouDub AI - YouTube Video Dubbing System

Dub YouTube videos into any language with voice cloning, lip-sync, and natural translation

Size

40.0 KB

Version

1.0.1

Created

Oct 15, 2025

Updated

8 days ago

1// ==UserScript==
2// @name		YouDub AI - YouTube Video Dubbing System
3// @description		Dub YouTube videos into any language with voice cloning, lip-sync, and natural translation
4// @version		1.0.1
5// @match		https://*.youtube.com/*
6// @icon		https://www.youtube.com/s/desktop/dc78d16c/img/favicon_32x32.png
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.xmlHttpRequest
10// @grant		GM.openInTab
11// @require		https://cdnjs.cloudflare.com/ajax/libs/axios/1.4.0/axios.min.js
12// ==/UserScript==
13(function() {
14    'use strict';
15
16    // ========================================
17    // YOUDUB AI - CONFIGURATION
18    // ========================================
19    const CONFIG = {
20        API_ENDPOINTS: {
21            WHISPER: 'https://api.openai.com/v1/audio/transcriptions',
22            ELEVENLABS: 'https://api.elevenlabs.io/v1',
23            TRANSLATION: 'https://api.openai.com/v1/chat/completions',
24            VIDEO_PROCESSOR: 'https://api.replicate.com/v1/predictions'
25        },
26        LANGUAGES: {
27            'en': 'English',
28            'es': 'Spanish',
29            'ur': 'Urdu',
30            'hi': 'Hindi',
31            'ar': 'Arabic',
32            'ja': 'Japanese',
33            'fr': 'French',
34            'de': 'German',
35            'pt': 'Portuguese',
36            'zh': 'Chinese',
37            'ko': 'Korean',
38            'it': 'Italian',
39            'ru': 'Russian',
40            'tr': 'Turkish'
41        },
42        VOICE_OPTIONS: {
43            CLONE: 'clone_original',
44            MALE: 'male_voice',
45            FEMALE: 'female_voice'
46        },
47        FREE_LIMIT_MINUTES: 5,
48        MAX_VIDEO_HOURS: 9
49    };
50
51    // ========================================
52    // STATE MANAGEMENT
53    // ========================================
54    class YouDubState {
55        constructor() {
56            this.currentVideo = null;
57            this.processingStatus = 'idle';
58            this.userPlan = 'free';
59            this.jobId = null;
60        }
61
62        async loadUserData() {
63            this.userPlan = await GM.getValue('youdub_user_plan', 'free');
64            const apiKeys = await GM.getValue('youdub_api_keys', '{}');
65            return JSON.parse(apiKeys);
66        }
67
68        async saveApiKeys(keys) {
69            await GM.setValue('youdub_api_keys', JSON.stringify(keys));
70        }
71
72        async saveJobHistory(job) {
73            const history = await GM.getValue('youdub_job_history', '[]');
74            const jobs = JSON.parse(history);
75            jobs.unshift(job);
76            if (jobs.length > 50) jobs.pop();
77            await GM.setValue('youdub_job_history', JSON.stringify(jobs));
78        }
79
80        async getJobHistory() {
81            const history = await GM.getValue('youdub_job_history', '[]');
82            return JSON.parse(history);
83        }
84    }
85
86    const state = new YouDubState();
87
88    // ========================================
89    // VIDEO INFORMATION EXTRACTOR
90    // ========================================
91    class VideoExtractor {
92        static getCurrentVideoInfo() {
93            const videoId = this.extractVideoId();
94            if (!videoId) return null;
95
96            const videoElement = document.querySelector('video');
97            const titleElement = document.querySelector('h1.ytd-watch-metadata yt-formatted-string');
98            const channelElement = document.querySelector('ytd-channel-name a');
99            
100            return {
101                videoId: videoId,
102                url: window.location.href,
103                title: titleElement?.textContent?.trim() || 'Unknown Title',
104                channel: channelElement?.textContent?.trim() || 'Unknown Channel',
105                duration: videoElement?.duration || 0,
106                currentTime: videoElement?.currentTime || 0,
107                thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`
108            };
109        }
110
111        static extractVideoId() {
112            const urlParams = new URLSearchParams(window.location.search);
113            return urlParams.get('v');
114        }
115
116        static async downloadAudio(videoUrl) {
117            console.log('📥 Downloading audio from:', videoUrl);
118            
119            // Using yt-dlp API or similar service
120            // In production, this would call a backend service
121            return {
122                audioUrl: `https://api.youdub.ai/extract-audio?video=${encodeURIComponent(videoUrl)}`,
123                format: 'mp3',
124                duration: 0
125            };
126        }
127    }
128
129    // ========================================
130    // TRANSCRIPTION SERVICE (WhisperX)
131    // ========================================
132    class TranscriptionService {
133        static async transcribe(audioUrl, apiKeys) {
134            console.log('🎤 Starting transcription with WhisperX...');
135            
136            try {
137                const response = await GM.xmlhttpRequest({
138                    method: 'POST',
139                    url: CONFIG.API_ENDPOINTS.WHISPER,
140                    headers: {
141                        'Authorization': `Bearer ${apiKeys.openai}`,
142                        'Content-Type': 'application/json'
143                    },
144                    data: JSON.stringify({
145                        file: audioUrl,
146                        model: 'whisper-1',
147                        response_format: 'verbose_json',
148                        timestamp_granularities: ['word', 'segment']
149                    })
150                });
151
152                const result = JSON.parse(response.responseText);
153                console.log('✅ Transcription complete:', result);
154                
155                return {
156                    text: result.text,
157                    segments: result.segments || [],
158                    words: result.words || [],
159                    language: result.language,
160                    duration: result.duration
161                };
162            } catch (error) {
163                console.error('❌ Transcription failed:', error);
164                throw new Error('Transcription failed: ' + error.message);
165            }
166        }
167
168        static formatTimestamp(seconds) {
169            const hours = Math.floor(seconds / 3600);
170            const minutes = Math.floor((seconds % 3600) / 60);
171            const secs = Math.floor(seconds % 60);
172            const ms = Math.floor((seconds % 1) * 1000);
173            
174            return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')},${String(ms).padStart(3, '0')}`;
175        }
176
177        static generateSRT(segments) {
178            let srt = '';
179            segments.forEach((segment, index) => {
180                srt += `${index + 1}\n`;
181                srt += `${this.formatTimestamp(segment.start)} --> ${this.formatTimestamp(segment.end)}\n`;
182                srt += `${segment.text.trim()}\n\n`;
183            });
184            return srt;
185        }
186    }
187
188    // ========================================
189    // TRANSLATION SERVICE (Context-Aware)
190    // ========================================
191    class TranslationService {
192        static async translate(transcript, targetLang, apiKeys) {
193            console.log(`🌍 Translating to ${CONFIG.LANGUAGES[targetLang]}...`);
194            
195            const prompt = `You are a professional video translator specializing in natural, emotional dubbing.
196
197TASK: Translate the following video transcript to ${CONFIG.LANGUAGES[targetLang]}.
198
199CRITICAL REQUIREMENTS:
2001. Preserve ALL emotions, tone, and personality
2012. Keep natural speech patterns (pauses, fillers, emphasis)
2023. Maintain timing - translation should match original length
2034. Preserve laughs, sighs, exclamations as [LAUGH], [SIGH], etc.
2045. Keep cultural context and idioms natural
2056. Match formality level of original
2067. Preserve speaker's unique style and catchphrases
207
208TRANSCRIPT:
209${transcript.text}
210
211Return ONLY the translated text with emotion markers preserved.`;
212
213            try {
214                const response = await GM.xmlhttpRequest({
215                    method: 'POST',
216                    url: CONFIG.API_ENDPOINTS.TRANSLATION,
217                    headers: {
218                        'Authorization': `Bearer ${apiKeys.openai}`,
219                        'Content-Type': 'application/json'
220                    },
221                    data: JSON.stringify({
222                        model: 'gpt-4-turbo-preview',
223                        messages: [
224                            {
225                                role: 'system',
226                                content: 'You are an expert video dubbing translator who preserves emotion and natural speech.'
227                            },
228                            {
229                                role: 'user',
230                                content: prompt
231                            }
232                        ],
233                        temperature: 0.7
234                    })
235                });
236
237                const result = JSON.parse(response.responseText);
238                const translatedText = result.choices[0].message.content;
239                
240                console.log('✅ Translation complete');
241                
242                return {
243                    originalText: transcript.text,
244                    translatedText: translatedText,
245                    targetLanguage: targetLang,
246                    segments: await this.alignTranslation(transcript.segments, translatedText)
247                };
248            } catch (error) {
249                console.error('❌ Translation failed:', error);
250                throw new Error('Translation failed: ' + error.message);
251            }
252        }
253
254        static async alignTranslation(originalSegments, translatedText) {
255            // Split translated text proportionally to match original timing
256            const words = translatedText.split(' ');
257            const totalWords = words.length;
258            const totalDuration = originalSegments[originalSegments.length - 1].end;
259            
260            const alignedSegments = [];
261            let wordIndex = 0;
262            
263            for (const segment of originalSegments) {
264                const segmentDuration = segment.end - segment.start;
265                const wordsInSegment = Math.ceil((segmentDuration / totalDuration) * totalWords);
266                
267                const segmentWords = words.slice(wordIndex, wordIndex + wordsInSegment);
268                alignedSegments.push({
269                    start: segment.start,
270                    end: segment.end,
271                    text: segmentWords.join(' ')
272                });
273                
274                wordIndex += wordsInSegment;
275            }
276            
277            return alignedSegments;
278        }
279    }
280
281    // ========================================
282    // VOICE CLONING SERVICE (ElevenLabs)
283    // ========================================
284    class VoiceCloneService {
285        static async cloneAndGenerate(audioUrl, translatedSegments, voiceOption, apiKeys) {
286            console.log('🎙️ Starting voice cloning and generation...');
287            
288            try {
289                let voiceId;
290                
291                if (voiceOption === CONFIG.VOICE_OPTIONS.CLONE) {
292                    // Clone original voice
293                    voiceId = await this.cloneVoice(audioUrl, apiKeys.elevenlabs);
294                } else {
295                    // Use preset voice
296                    voiceId = voiceOption === CONFIG.VOICE_OPTIONS.MALE ? 
297                        'pNInz6obpgDQGcFmaJgB' : // Male voice ID
298                        'EXAVITQu4vr4xnSDxMaL';  // Female voice ID
299                }
300                
301                // Generate dubbed audio for each segment
302                const audioSegments = [];
303                for (const segment of translatedSegments) {
304                    const audio = await this.generateSpeech(
305                        segment.text,
306                        voiceId,
307                        apiKeys.elevenlabs,
308                        segment.start,
309                        segment.end
310                    );
311                    audioSegments.push(audio);
312                }
313                
314                console.log('✅ Voice generation complete');
315                
316                return {
317                    voiceId: voiceId,
318                    audioSegments: audioSegments,
319                    totalDuration: translatedSegments[translatedSegments.length - 1].end
320                };
321            } catch (error) {
322                console.error('❌ Voice cloning failed:', error);
323                throw new Error('Voice cloning failed: ' + error.message);
324            }
325        }
326
327        static async cloneVoice(audioUrl, apiKey) {
328            console.log('🎭 Cloning original voice...');
329            
330            const response = await GM.xmlhttpRequest({
331                method: 'POST',
332                url: `${CONFIG.API_ENDPOINTS.ELEVENLABS}/voices/add`,
333                headers: {
334                    'xi-api-key': apiKey,
335                    'Content-Type': 'application/json'
336                },
337                data: JSON.stringify({
338                    name: `YouDub_Clone_${Date.now()}`,
339                    files: [audioUrl],
340                    description: 'Voice cloned by YouDub AI'
341                })
342            });
343
344            const result = JSON.parse(response.responseText);
345            return result.voice_id;
346        }
347
348        static async generateSpeech(text, voiceId, apiKey, startTime, endTime) {
349            const targetDuration = endTime - startTime;
350            
351            const response = await GM.xmlhttpRequest({
352                method: 'POST',
353                url: `${CONFIG.API_ENDPOINTS.ELEVENLABS}/text-to-speech/${voiceId}`,
354                headers: {
355                    'xi-api-key': apiKey,
356                    'Content-Type': 'application/json'
357                },
358                data: JSON.stringify({
359                    text: text,
360                    model_id: 'eleven_multilingual_v2',
361                    voice_settings: {
362                        stability: 0.5,
363                        similarity_boost: 0.75,
364                        style: 0.5,
365                        use_speaker_boost: true
366                    }
367                }),
368                responseType: 'blob'
369            });
370
371            return {
372                audioBlob: response.response,
373                startTime: startTime,
374                endTime: endTime,
375                text: text
376            };
377        }
378    }
379
380    // ========================================
381    // VIDEO PROCESSING SERVICE
382    // ========================================
383    class VideoProcessor {
384        static async mergeDubbedAudio(videoUrl, audioSegments, options) {
385            console.log('🎬 Merging dubbed audio with video...');
386            
387            try {
388                // This would call a backend service that:
389                // 1. Downloads original video
390                // 2. Removes original audio
391                // 3. Merges new dubbed audio segments
392                // 4. Optionally applies lip-sync (Wav2Lip)
393                // 5. Adds subtitles if requested
394                // 6. Renders final video
395                
396                const jobData = {
397                    videoUrl: videoUrl,
398                    audioSegments: audioSegments,
399                    options: options,
400                    timestamp: Date.now()
401                };
402
403                const response = await GM.xmlhttpRequest({
404                    method: 'POST',
405                    url: 'https://api.youdub.ai/process-video',
406                    headers: {
407                        'Content-Type': 'application/json'
408                    },
409                    data: JSON.stringify(jobData)
410                });
411
412                const result = JSON.parse(response.responseText);
413                
414                console.log('✅ Video processing started, Job ID:', result.jobId);
415                
416                return {
417                    jobId: result.jobId,
418                    status: 'processing',
419                    estimatedTime: result.estimatedTime
420                };
421            } catch (error) {
422                console.error('❌ Video processing failed:', error);
423                throw new Error('Video processing failed: ' + error.message);
424            }
425        }
426
427        static async checkJobStatus(jobId) {
428            try {
429                const response = await GM.xmlhttpRequest({
430                    method: 'GET',
431                    url: `https://api.youdub.ai/job-status/${jobId}`
432                });
433
434                const result = JSON.parse(response.responseText);
435                return result;
436            } catch (error) {
437                console.error('❌ Failed to check job status:', error);
438                return { status: 'error', error: error.message };
439            }
440        }
441    }
442
443    // ========================================
444    // UI COMPONENTS
445    // ========================================
446    class YouDubUI {
447        static createMainButton() {
448            const button = document.createElement('button');
449            button.id = 'youdub-main-btn';
450            button.innerHTML = `
451                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
452                    <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
453                    <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
454                    <line x1="12" y1="19" x2="12" y2="22"/>
455                </svg>
456                <span>Dub Video</span>
457            `;
458            button.style.cssText = `
459                position: fixed;
460                bottom: 100px;
461                right: 20px;
462                z-index: 9999;
463                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
464                color: white;
465                border: none;
466                border-radius: 50px;
467                padding: 12px 24px;
468                font-size: 14px;
469                font-weight: 600;
470                cursor: pointer;
471                box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
472                display: flex;
473                align-items: center;
474                gap: 8px;
475                transition: all 0.3s ease;
476                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
477            `;
478
479            button.addEventListener('mouseenter', () => {
480                button.style.transform = 'translateY(-2px)';
481                button.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6)';
482            });
483
484            button.addEventListener('mouseleave', () => {
485                button.style.transform = 'translateY(0)';
486                button.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4)';
487            });
488
489            button.addEventListener('click', () => this.openDubbingPanel());
490
491            return button;
492        }
493
494        static openDubbingPanel() {
495            const videoInfo = VideoExtractor.getCurrentVideoInfo();
496            if (!videoInfo) {
497                alert('Please open a YouTube video first!');
498                return;
499            }
500
501            const durationMinutes = Math.floor(videoInfo.duration / 60);
502            const isFreeLimitExceeded = durationMinutes > CONFIG.FREE_LIMIT_MINUTES && state.userPlan === 'free';
503
504            const panel = document.createElement('div');
505            panel.id = 'youdub-panel';
506            panel.innerHTML = `
507                <div class="youdub-overlay"></div>
508                <div class="youdub-modal">
509                    <div class="youdub-header">
510                        <h2>🎬 YouDub AI - Video Dubbing</h2>
511                        <button class="youdub-close">&times;</button>
512                    </div>
513                    
514                    <div class="youdub-content">
515                        <div class="video-info">
516                            <img src="${videoInfo.thumbnail}" alt="Thumbnail">
517                            <div class="video-details">
518                                <h3>${videoInfo.title}</h3>
519                                <p>${videoInfo.channel}</p>
520                                <p class="duration">Duration: ${durationMinutes} minutes</p>
521                                ${isFreeLimitExceeded ? '<p class="warning">⚠️ Free plan limited to 5 minutes. Upgrade for full access.</p>' : ''}
522                            </div>
523                        </div>
524
525                        <div class="dubbing-options">
526                            <div class="option-group">
527                                <label>Target Language</label>
528                                <select id="youdub-target-lang">
529                                    ${Object.entries(CONFIG.LANGUAGES).map(([code, name]) => 
530                                        `<option value="${code}">${name}</option>`
531                                    ).join('')}
532                                </select>
533                            </div>
534
535                            <div class="option-group">
536                                <label>Voice Option</label>
537                                <select id="youdub-voice-option">
538                                    <option value="${CONFIG.VOICE_OPTIONS.CLONE}">🎭 Clone Original Voice</option>
539                                    <option value="${CONFIG.VOICE_OPTIONS.MALE}">👨 Male Voice</option>
540                                    <option value="${CONFIG.VOICE_OPTIONS.FEMALE}">👩 Female Voice</option>
541                                </select>
542                            </div>
543
544                            <div class="option-group">
545                                <label>
546                                    <input type="checkbox" id="youdub-subtitles" checked>
547                                    Generate Subtitles (SRT)
548                                </label>
549                            </div>
550
551                            <div class="option-group">
552                                <label>
553                                    <input type="checkbox" id="youdub-lipsync">
554                                    Enable Lip-Sync (Premium)
555                                </label>
556                            </div>
557                        </div>
558
559                        <div class="api-keys-section">
560                            <h4>🔑 API Keys (Required)</h4>
561                            <input type="password" id="youdub-openai-key" placeholder="OpenAI API Key">
562                            <input type="password" id="youdub-elevenlabs-key" placeholder="ElevenLabs API Key">
563                            <p class="api-note">Your keys are stored locally and never shared.</p>
564                        </div>
565
566                        <div class="action-buttons">
567                            <button class="btn-secondary" id="youdub-preview">Preview (5 min)</button>
568                            <button class="btn-primary" id="youdub-start" ${isFreeLimitExceeded ? 'disabled' : ''}>
569                                ${isFreeLimitExceeded ? '🔒 Upgrade to Premium' : '🚀 Start Dubbing'}
570                            </button>
571                        </div>
572
573                        <div class="progress-section" style="display: none;">
574                            <div class="progress-bar">
575                                <div class="progress-fill"></div>
576                            </div>
577                            <p class="progress-text">Initializing...</p>
578                        </div>
579                    </div>
580                </div>
581            `;
582
583            this.addPanelStyles();
584            document.body.appendChild(panel);
585
586            // Event listeners
587            panel.querySelector('.youdub-close').addEventListener('click', () => panel.remove());
588            panel.querySelector('.youdub-overlay').addEventListener('click', () => panel.remove());
589            
590            panel.querySelector('#youdub-start').addEventListener('click', () => this.startDubbing(videoInfo, false));
591            panel.querySelector('#youdub-preview').addEventListener('click', () => this.startDubbing(videoInfo, true));
592
593            // Load saved API keys
594            this.loadSavedApiKeys();
595        }
596
597        static async loadSavedApiKeys() {
598            const apiKeys = await state.loadUserData();
599            if (apiKeys.openai) {
600                document.getElementById('youdub-openai-key').value = apiKeys.openai;
601            }
602            if (apiKeys.elevenlabs) {
603                document.getElementById('youdub-elevenlabs-key').value = apiKeys.elevenlabs;
604            }
605        }
606
607        static async startDubbing(videoInfo, isPreview) {
608            const targetLang = document.getElementById('youdub-target-lang').value;
609            const voiceOption = document.getElementById('youdub-voice-option').value;
610            const includeSubtitles = document.getElementById('youdub-subtitles').checked;
611            const enableLipSync = document.getElementById('youdub-lipsync').checked;
612            
613            const openaiKey = document.getElementById('youdub-openai-key').value;
614            const elevenlabsKey = document.getElementById('youdub-elevenlabs-key').value;
615
616            if (!openaiKey || !elevenlabsKey) {
617                alert('Please enter your API keys!');
618                return;
619            }
620
621            // Save API keys
622            await state.saveApiKeys({
623                openai: openaiKey,
624                elevenlabs: elevenlabsKey
625            });
626
627            const progressSection = document.querySelector('.progress-section');
628            const progressFill = document.querySelector('.progress-fill');
629            const progressText = document.querySelector('.progress-text');
630            
631            progressSection.style.display = 'block';
632            document.querySelector('.action-buttons').style.display = 'none';
633
634            try {
635                // Step 1: Extract audio
636                progressText.textContent = '📥 Extracting audio from video...';
637                progressFill.style.width = '10%';
638                const audioData = await VideoExtractor.downloadAudio(videoInfo.url);
639
640                // Step 2: Transcribe
641                progressText.textContent = '🎤 Transcribing audio with WhisperX...';
642                progressFill.style.width = '30%';
643                const transcript = await TranscriptionService.transcribe(audioData.audioUrl, {
644                    openai: openaiKey
645                });
646
647                // Step 3: Translate
648                progressText.textContent = `🌍 Translating to ${CONFIG.LANGUAGES[targetLang]}...`;
649                progressFill.style.width = '50%';
650                const translation = await TranslationService.translate(transcript, targetLang, {
651                    openai: openaiKey
652                });
653
654                // Step 4: Voice cloning and generation
655                progressText.textContent = '🎙️ Cloning voice and generating dubbed audio...';
656                progressFill.style.width = '70%';
657                const voiceData = await VoiceCloneService.cloneAndGenerate(
658                    audioData.audioUrl,
659                    translation.segments,
660                    voiceOption,
661                    { elevenlabs: elevenlabsKey }
662                );
663
664                // Step 5: Merge video
665                progressText.textContent = '🎬 Processing final video...';
666                progressFill.style.width = '90%';
667                const videoJob = await VideoProcessor.mergeDubbedAudio(videoInfo.url, voiceData.audioSegments, {
668                    includeSubtitles: includeSubtitles,
669                    enableLipSync: enableLipSync,
670                    isPreview: isPreview
671                });
672
673                // Step 6: Complete
674                progressFill.style.width = '100%';
675                progressText.textContent = '✅ Dubbing complete!';
676
677                // Save job to history
678                await state.saveJobHistory({
679                    videoTitle: videoInfo.title,
680                    targetLanguage: CONFIG.LANGUAGES[targetLang],
681                    jobId: videoJob.jobId,
682                    timestamp: Date.now(),
683                    status: 'completed'
684                });
685
686                // Show download options
687                setTimeout(() => {
688                    this.showDownloadPanel(videoJob.jobId, includeSubtitles);
689                }, 1000);
690
691            } catch (error) {
692                console.error('❌ Dubbing failed:', error);
693                progressText.textContent = '❌ Error: ' + error.message;
694                progressText.style.color = '#ff4444';
695            }
696        }
697
698        static showDownloadPanel(jobId, includeSubtitles) {
699            const panel = document.getElementById('youdub-panel');
700            const content = panel.querySelector('.youdub-content');
701            
702            content.innerHTML = `
703                <div class="success-message">
704                    <div class="success-icon"></div>
705                    <h3>Dubbing Complete!</h3>
706                    <p>Your dubbed video is ready for download.</p>
707                </div>
708
709                <div class="download-options">
710                    <button class="btn-download" onclick="window.open('https://api.youdub.ai/download/${jobId}/video.mp4')">
711                        📥 Download Video (MP4)
712                    </button>
713                    ${includeSubtitles ? `
714                        <button class="btn-download" onclick="window.open('https://api.youdub.ai/download/${jobId}/subtitles.srt')">
715                            📄 Download Subtitles (SRT)
716                        </button>
717                    ` : ''}
718                    <button class="btn-preview" onclick="window.open('https://api.youdub.ai/preview/${jobId}')">
719                        ▶️ Preview Video
720                    </button>
721                </div>
722
723                <div class="share-section">
724                    <h4>Share Your Dubbed Video</h4>
725                    <div class="share-buttons">
726                        <button class="btn-share">🔗 Copy Link</button>
727                        <button class="btn-share">📱 Share on Twitter</button>
728                        <button class="btn-share">💬 Share on WhatsApp</button>
729                    </div>
730                </div>
731
732                <button class="btn-secondary" onclick="document.getElementById('youdub-panel').remove()">
733                    Close
734                </button>
735            `;
736        }
737
738        static addPanelStyles() {
739            if (document.getElementById('youdub-styles')) return;
740
741            const styles = document.createElement('style');
742            styles.id = 'youdub-styles';
743            styles.textContent = `
744                .youdub-overlay {
745                    position: fixed;
746                    top: 0;
747                    left: 0;
748                    width: 100%;
749                    height: 100%;
750                    background: rgba(0, 0, 0, 0.7);
751                    z-index: 99998;
752                    backdrop-filter: blur(5px);
753                }
754
755                .youdub-modal {
756                    position: fixed;
757                    top: 50%;
758                    left: 50%;
759                    transform: translate(-50%, -50%);
760                    background: #1a1a2e;
761                    border-radius: 20px;
762                    width: 90%;
763                    max-width: 700px;
764                    max-height: 90vh;
765                    overflow-y: auto;
766                    z-index: 99999;
767                    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
768                    color: #ffffff;
769                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
770                }
771
772                .youdub-header {
773                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
774                    padding: 20px 30px;
775                    border-radius: 20px 20px 0 0;
776                    display: flex;
777                    justify-content: space-between;
778                    align-items: center;
779                }
780
781                .youdub-header h2 {
782                    margin: 0;
783                    font-size: 24px;
784                    font-weight: 700;
785                }
786
787                .youdub-close {
788                    background: rgba(255, 255, 255, 0.2);
789                    border: none;
790                    color: white;
791                    font-size: 28px;
792                    width: 40px;
793                    height: 40px;
794                    border-radius: 50%;
795                    cursor: pointer;
796                    transition: all 0.3s ease;
797                }
798
799                .youdub-close:hover {
800                    background: rgba(255, 255, 255, 0.3);
801                    transform: rotate(90deg);
802                }
803
804                .youdub-content {
805                    padding: 30px;
806                }
807
808                .video-info {
809                    display: flex;
810                    gap: 20px;
811                    margin-bottom: 30px;
812                    background: #16213e;
813                    padding: 20px;
814                    border-radius: 15px;
815                }
816
817                .video-info img {
818                    width: 180px;
819                    height: 100px;
820                    object-fit: cover;
821                    border-radius: 10px;
822                }
823
824                .video-details h3 {
825                    margin: 0 0 10px 0;
826                    font-size: 16px;
827                    color: #ffffff;
828                }
829
830                .video-details p {
831                    margin: 5px 0;
832                    color: #a0a0a0;
833                    font-size: 14px;
834                }
835
836                .video-details .warning {
837                    color: #ff9800;
838                    font-weight: 600;
839                    margin-top: 10px;
840                }
841
842                .dubbing-options {
843                    display: grid;
844                    gap: 20px;
845                    margin-bottom: 30px;
846                }
847
848                .option-group label {
849                    display: block;
850                    margin-bottom: 8px;
851                    color: #e0e0e0;
852                    font-weight: 600;
853                    font-size: 14px;
854                }
855
856                .option-group select,
857                .api-keys-section input {
858                    width: 100%;
859                    padding: 12px 16px;
860                    background: #16213e;
861                    border: 2px solid #2d3561;
862                    border-radius: 10px;
863                    color: #ffffff;
864                    font-size: 14px;
865                    transition: all 0.3s ease;
866                }
867
868                .option-group select:focus,
869                .api-keys-section input:focus {
870                    outline: none;
871                    border-color: #667eea;
872                    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
873                }
874
875                .option-group input[type="checkbox"] {
876                    margin-right: 8px;
877                    width: 18px;
878                    height: 18px;
879                    cursor: pointer;
880                }
881
882                .api-keys-section {
883                    background: #16213e;
884                    padding: 20px;
885                    border-radius: 15px;
886                    margin-bottom: 30px;
887                }
888
889                .api-keys-section h4 {
890                    margin: 0 0 15px 0;
891                    color: #ffffff;
892                }
893
894                .api-keys-section input {
895                    margin-bottom: 12px;
896                }
897
898                .api-note {
899                    color: #a0a0a0;
900                    font-size: 12px;
901                    margin: 10px 0 0 0;
902                }
903
904                .action-buttons {
905                    display: flex;
906                    gap: 15px;
907                    margin-bottom: 20px;
908                }
909
910                .btn-primary,
911                .btn-secondary,
912                .btn-download,
913                .btn-preview,
914                .btn-share {
915                    flex: 1;
916                    padding: 14px 24px;
917                    border: none;
918                    border-radius: 12px;
919                    font-size: 15px;
920                    font-weight: 600;
921                    cursor: pointer;
922                    transition: all 0.3s ease;
923                }
924
925                .btn-primary {
926                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
927                    color: white;
928                }
929
930                .btn-primary:hover:not(:disabled) {
931                    transform: translateY(-2px);
932                    box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
933                }
934
935                .btn-primary:disabled {
936                    opacity: 0.5;
937                    cursor: not-allowed;
938                }
939
940                .btn-secondary {
941                    background: #2d3561;
942                    color: white;
943                }
944
945                .btn-secondary:hover {
946                    background: #3d4571;
947                }
948
949                .progress-section {
950                    margin-top: 30px;
951                }
952
953                .progress-bar {
954                    width: 100%;
955                    height: 8px;
956                    background: #16213e;
957                    border-radius: 10px;
958                    overflow: hidden;
959                    margin-bottom: 15px;
960                }
961
962                .progress-fill {
963                    height: 100%;
964                    background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
965                    width: 0%;
966                    transition: width 0.5s ease;
967                }
968
969                .progress-text {
970                    text-align: center;
971                    color: #e0e0e0;
972                    font-size: 14px;
973                }
974
975                .success-message {
976                    text-align: center;
977                    padding: 40px 20px;
978                }
979
980                .success-icon {
981                    font-size: 64px;
982                    margin-bottom: 20px;
983                }
984
985                .success-message h3 {
986                    margin: 0 0 10px 0;
987                    font-size: 28px;
988                    color: #ffffff;
989                }
990
991                .success-message p {
992                    color: #a0a0a0;
993                    font-size: 16px;
994                }
995
996                .download-options {
997                    display: grid;
998                    gap: 15px;
999                    margin: 30px 0;
1000                }
1001
1002                .btn-download,
1003                .btn-preview {
1004                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1005                    color: white;
1006                }
1007
1008                .btn-download:hover,
1009                .btn-preview:hover {
1010                    transform: translateY(-2px);
1011                    box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
1012                }
1013
1014                .share-section {
1015                    background: #16213e;
1016                    padding: 20px;
1017                    border-radius: 15px;
1018                    margin: 30px 0;
1019                }
1020
1021                .share-section h4 {
1022                    margin: 0 0 15px 0;
1023                    color: #ffffff;
1024                }
1025
1026                .share-buttons {
1027                    display: grid;
1028                    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1029                    gap: 10px;
1030                }
1031
1032                .btn-share {
1033                    background: #2d3561;
1034                    color: white;
1035                    padding: 10px 16px;
1036                    font-size: 13px;
1037                }
1038
1039                .btn-share:hover {
1040                    background: #3d4571;
1041                }
1042            `;
1043            document.head.appendChild(styles);
1044        }
1045    }
1046
1047    // ========================================
1048    // INITIALIZATION
1049    // ========================================
1050    async function init() {
1051        console.log('🎬 YouDub AI - YouTube Video Dubbing System Initialized');
1052        
1053        // Wait for YouTube page to load
1054        const checkYouTubeReady = setInterval(() => {
1055            const videoElement = document.querySelector('video');
1056            if (videoElement) {
1057                clearInterval(checkYouTubeReady);
1058                
1059                // Add main button
1060                const mainButton = YouDubUI.createMainButton();
1061                document.body.appendChild(mainButton);
1062                
1063                console.log('✅ YouDub AI ready!');
1064            }
1065        }, 1000);
1066
1067        // Load user data
1068        await state.loadUserData();
1069    }
1070
1071    // Start the extension
1072    if (document.readyState === 'loading') {
1073        document.addEventListener('DOMContentLoaded', init);
1074    } else {
1075        init();
1076    }
1077
1078})();
YouDub AI - YouTube Video Dubbing System | Robomonkey