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">×</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})();