# Project export: YouDub

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: Watch videos on YouTube with new English voices to make listening more pleasant. Enables accessibility to videos created solely in different languages with its automatic English translate feature.
- Devpost: https://devpost.com/software/youdub
- GitHub: https://github.com/rondawgx3/YouDub_public
- Video: https://www.youtube.com/embed/lzSXoemu5ME?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Ronald McInnes (2 commits)

## Devpost submission (written by the team)

### Overview

Our inspiration for developing YouDub came from a unique blend of experiences within our diverse team. With three members having Hindi as their mother tongue and one member being an English speaker, a casual conversation revealed an interesting challenge. Our English-speaking teammate shared his fondness for math tutorials taught by Indian educators, who often explain concepts in an incredibly straightforward way. However, these videos were mostly in Hindi, which meant he had to rely on subtitles to follow along. This sparked the idea: What if we could create a Chrome extension that adds voice-overs to YouTube videos, allowing viewers to hear a dubbed version of the content in their preferred language? Not only would this help break language barriers, but it would also assist students who struggle with videos where the accent is hard to understand, the pace is too fast, or the explanation style is unclear. By providing customizable voice-overs, YouDub enhances accessibility, making it easier for users to engage with educational content. What We Learned Throughout this journey, we learned a lot. On the technical side, we gained expertise in designing and building a Chrome extension, integrating APIs, and refining our use of Git for collaborative development. Beyond the code, we learned the value of teamwork, developed stronger friendships, and discovered fascinating things about each other’s perspectives and working styles. Stack and Functionality YouDub is built using JavaScript, HTML, and CSS. We leveraged ElevenLabs API, first feeding transcription data from YouTube videos into Google Cloud API's translate feature, and then into Eleven’s voice models, generating synchronized audio in the user’s chosen voice. This opens up various use cases, such as replacing the voice of a content creator whose delivery might be off-putting, or translating and dubbing tutorial videos, allowing users to immerse themselves in the content without relying on subtitles. The result is a more engaging experience and access to a broader range of content on YouTube. Challenges We Faced Building YouDub wasn’t without its challenges. One of the biggest hurdles was defining the exact scope of what we wanted to achieve. YouTube’s frequent updates to its site security also presented difficulties, causing multiple console errors that required extensive debugging. Additionally, the demanding pace of development, especially during CalHacks, pushed us out of our comfort zones, requiring long hours and constant problem-solving. Conclusion &

### What's next

The experience was incredibly rewarding. We leave CalHacks not only with a functioning product but also with a sense of pride and accomplishment in what we’ve achieved. In the end, YouDub empowers users to engage with YouTube content in new and exciting ways, breaking language barriers and enhancing the viewing experience for people around the globe. With our extension in review to be publicly available on the Chrome Web Store, our product will soon be available to users around the world!

## README (from the GitHub repository)

## Setup

1. 'git clone https://github.com/rondawgx3/YouDub_public' in your terminal
2. Copy `config.sample.js` to `config.js`
3. Replace the placeholder API keys in `config.js` with your actual API keys from ElevenLabs and Google Cloud
4. Go to chrome://extensions/ and click on 'load unpacked'
5. Drag and drop the YouDub_public folder and pin your new extension!
6. Go to a YouTube video and click 'Enable Alternative Audio'!
7. Enjoy :)


## Detected evidence (automated analysis)

Indexed codebase: 6 recognized source files, 21 KB.
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- CSS (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (7 of 7)

```
background.js
config.sample.js
content.js
manifest.json
popup.html
popup.js
README.md
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Update README.md
- made private repo public with limited setup needed!

## Key source files (fetched from GitHub, selected and truncated for size)

### config.sample.js

```javascript
const config = {
    ELEVENLABS_API_KEY: 'YOUR_ELEVENLABS_API_KEY_HERE',
    GOOGLE_TRANSLATE_API_KEY: 'YOUR_GOOGLE_TRANSLATE_API_KEY_HERE'
  };
```

### popup.html

```html
<!DOCTYPE html>
<html>
<head>
  <title>YouDube</title>
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
  <style>
    body {
      width: 300px;
      padding: 30px;
      font-family: 'Montserrat', sans-serif;
      position: relative;
      background-color: #040404;
      color: #f07b27; /* Orange text */
      border: 5px solid #0ca4eb; /* Blue border */
      box-sizing: border-box; 
    }
    h1 {
      margin-top: 0;
    }
    #logo {
      position: absolute;
      top: 10px;
      right: 10px;
      width: 50px;
      height: 50px;
    }
    #voiceSelect {
      width: 100%;
      margin-bottom: 15px;
      padding: 5px;
      background-color: #040404;
      color: #f07b27;
      border: 1px solid #f07b27;
    }
    #toggleExtension {
      width: 20px;
      height: 20px;
      margin-right: 10px;
      accent-color: #f07b27;
    }
    #status {
      margin-top: 15px;
      font-weight: 700;
    }
    label {
      color: #f07b27;
    }
    /* Style the dropdown arrow */
    #voiceSelect::-ms-expand {
      color: #0ca4eb;
    }
    #voiceSelect option {
      background-color: #040404;
    }
    #toggleExtension input{
      margin-right: 10px;
      accent-color: #f07b27;
      font-weight: 700;
    }
    .checkbox-container {
      display: flex;
      align-items: center;
      margin-top: 15px;
    }
    .checkbox-container label {
      font-size: 14px;
      font-weight: bold;
      cursor: pointer;
      transition: color 0.3s ease;
    }
    /* Optional: Style the checkbox when it's checked */
    #toggleExtension:checked + label {
      color: #0ca4eb;
    }
    #toggleText.enabling {
      color: #0ca4eb; /* Blue color for "Enabling..." */
    }
    #toggleText.enabled {
      color: #4CAF50; /* Green color for "Enabled Alt Audio!" */
    }
    #youtubeMessage {
      display: none;
    }
    #extensionControls {
      display: none;
    }
  </style>
</head>
<body>
  <img id="logo" src="icons/logo2.png" alt="YouDub Logo">
  <h1>YouDub</h1>
  <div id="youtubeMessage">
    <p>Looks like you're not on YouTube. Go find a video to dub here:</p>
    <a href="https://www.youtube.com" target="_blank">YouTube.com</a>
  </div>
  <div id="extensionControls">
    <select id="voiceSelect">
      <option value="default">Default - Robert</option>
      <option value="Grandpa Spuds Oxley">Grandpa Spuds Oxley</option>
      <option value="Boston Bob">Boston Bob</option>
      <option value="David Martin">David Martin</option>
      <option value="Mark - Robust">Mark - Robust</option>
      <option value="Ana - British">Ana - British</option>
      <option value="Ayesha - Energetic">Ayesha - Energetic</option>
      <option value="Farshid - Turkish">Farshid - Turkish</option>
      <option value="Mademoiselle">Mademoiselle</option>
      <option value="vNiraj - Hindi">Niraj - Hindi</option>
      <option value="Donny - NY">Donny - NY</option>
    </select>
    <br>
    <div class="checkbox-container">
      <input type="checkbox" id="toggleExtension">
      <label for="toggleExtension">
        <span id="toggleText">Enable Alternative Audio</span>
      </label>
    </div>
    <div id="status"></div>
  </div>
  <script src="popup.js"></script>
</body>
</html>

```

### popup.js

```javascript
console.log('Popup script loaded');

document.addEventListener('DOMContentLoaded', function() {
    console.log('DOM Content Loaded');
    const toggleExtension = document.getElementById('toggleExtension');
    const toggleText = document.getElementById('toggleText');
    const voiceSelect = document.getElementById('voiceSelect');
    const status = document.getElementById('status');
    const youtubeMessage = document.getElementById('youtubeMessage');
    const extensionControls = document.getElementById('extensionControls');

    // Check if we're on a YouTube page
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
        if (tabs[0] && tabs[0].url.includes('youtube.com')) {
            // We're on a YouTube page
            extensionControls.style.display = 'block';
            youtubeMessage.style.display = 'none';
            initializeExtension();
        } else {
            // We're not on a YouTube page
            extensionControls.style.display = 'none';
            youtubeMessage.style.display = 'block';
        }
    });

    function initializeExtension() {
        // Load saved voice selection and extension state
        chrome.storage.sync.get(['selectedVoice', 'extensionEnabled'], function(data) {
            if (data.selectedVoice) {
                voiceSelect.value = data.selectedVoice;
            }
            if (data.extensionEnabled) {
                toggleExtension.checked = true;
                toggleText.textContent = 'Enabled Alt Audio!';
                toggleText.classList.add('enabled');
            }
        });

        // Save voice selection when changed
        voiceSelect.addEventListener('change', function() {
            chrome.storage.sync.set({selectedVoice: voiceSelect.value});
        });

        toggleExtension.addEventListener('change', function() {
            const action = this.checked ? 'activate' : 'deactivate';
            
            if (this.checked) {
                toggleText.textContent = 'Enabling...';
                toggleText.classList.add('enabling');
                toggleText.classList.remove('enabled');
            } else {
                toggleText.textContent = 'Enable Alternative Audio';
                toggleText.classList.remove('enabling', 'enabled');
            }

            chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
                if (tabs[0]) {
                    chrome.tabs.sendMessage(tabs[0].id, {
                        action: action,
                        selectedVoice: voiceSelect.value
                    }, function(response) {
                        if (chrome.runtime.lastError) {
                            status.textContent = 'Error: ' + chrome.runtime.lastError.message;
                            toggleExtension.checked = false;
                            toggleText.textContent = 'Enable Alternative Audio';
                            toggleText.classList.remove('enabling', 'enabled');
                        } else if (response && response.success) {
                            status.textContent = 'Success!';
                            if (action === 'activate') {
                                toggleText.textContent = 'Enabled Alt Audio!';
                                toggleText.classList.remove('enabling');
                                toggleText.classList.add('enabled');
                            }
                            chrome.storage.sync.set({extensionEnabled: toggleExtension.checked});
                        } else {
                            status.textContent = 'Failed to ' + action;
                            toggleExtension.checked = false;
                            toggleText.textContent = 'Enable Alternative Audio';
                            toggleText.classList.remove('enabling', 'enabled');
                        }
                    });
                } else {
                    status.textContent = 'No active tab found';
                }
            });
        });
    }
});

```

### background.js

```javascript
import { config } from './config.js';

const elevenlabsApiKey = config.ELEVENLABS_API_KEY;
const googleTranslateApiKey = config.GOOGLE_TRANSLATE_API_KEY;

// Voice ID mapping
const voiceIdMap = {
    'default': 'BtWabtumIemAotTjP5sk',
    'Grandpa Spuds Oxley': 'NOpBlnGInO9m6vDvFkFC',
    'Boston Bob': 'Gf1KYedBUv2F4rCJhVFJ',
    'David Martin': 'y6WtESLj18d0diFRruBs',
    'Mark - Robust': 'uQvlBZB2vlJePJeb1waf',
    'Ana - British': 'wJqPPQ618aTW29mptyoc',
    'Ayesha - Energetic': '0ZOhGcBopt9S6GBK8tnj',
    'Farshid - Turkish': 'Emv9kLcuxJRj7yEDU8CT',
    'Mademoiselle': 'QbsdzCokdlo98elkq4Pc',
    'Niraj - Hindi': 'zgqefOY5FPQ3bB7OZTVR',
    'Donny - NY': 'BfDbhCUVGzNgO4WXDRdy',
};

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    console.log('Background script received message:', request);
    if (request.action === 'get_audio') {
        handleGetAudio(request)
            .then(response => {
                console.log('Sending response back to content script:', response);
                sendResponse(response);
            })
            .catch(error => {
                console.error('Error in handleGetAudio:', error);
                sendResponse({ success: false, error: error.message });
            });
        return true; // Indicates that the response is sent asynchronously
    }
});

async function handleGetAudio(request) {
    console.log('Starting handleGetAudio');
    const transcript = request.transcript;
    const selectedVoice = request.selectedVoice;
    const translate = request.translate;

    try {
        let translatedTranscript = transcript;
        if (translate) {
            console.log('Translating transcript');
            translatedTranscript = await translateText(transcript);
            console.log('Translation complete');
        }

        const voiceId = voiceIdMap[selectedVoice] || voiceIdMap['default'];
        const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`;

        const data = {
            text: translatedTranscript,
            voice_settings: {
                stability: 0.5,
                similarity_boost: 0.5
            }
        };

        console.log('Sending request to ElevenLabs API');
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'xi-api-key': elevenlabsApiKey,
                'Content-Type': 'application/json',
                'accept': 'audio/mpeg'
            },
            body: JSON.stringify(data)
        });

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
        }

        console.log('Received response from ElevenLabs API');
        const buffer = await response.arrayBuffer();
        console.log('Processing audio data');
        const base64 = btoa(new Uint8Array(buffer).reduce((data, byte) => data + String.fromCharCode(byte), ''));
        const dataUrl = `data:audio/mpeg;base64,${base64}`;
        console.log('Audio data processed successfully');
        return { success: true, audioData: dataUrl };
    } catch (error) {
        console.error('Error in handleGetAudio:', error);
        return { success: false, error: error.message };
    }
}

async function translateText(text, targetLanguage = 'en') {
    console.log('Starting translation');
    const url = `https://translation.googleapis.com/language/translate/v2?key=${googleTranslateApiKey}`;
    
    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                q: text,
                target: targetLanguage
            })
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        console.log('Translation complete');
        return data.data.translations[0].translatedText;
    } catch (error) {
        console.error('Error in translation:', error);
        throw error;
    }
}

```

### content.js

```javascript
(function() {
    let isActive = false;
    let synthesizedAudio = null;
    let selectedVoice = 'default';

    console.log('YouDub content script loaded');

    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
        console.log('Message received in content script:', request);
        if (request.action === 'activate') {
            console.log('Activating YouDube');
            selectedVoice = request.selectedVoice;
            startAlternativeAudio()
                .then(() => {
                    console.log('Alternative audio started successfully');
                    sendResponse({success: true, message: 'Activated successfully'});
                })
                .catch(error => {
                    console.error('Error in startAlternativeAudio:', error);
                    sendResponse({success: false, message: error.message});
                });
            return true; // Keeps the message channel open for asynchronous response
        } else if (request.action === 'deactivate') {
            isActive = false;
            console.log('Deactivating YouDube');
            stopAlternativeAudio();
            sendResponse({success: true, message: 'Deactivated'});
        } else {
            sendResponse({success: false, message: 'Unknown action'});
        }
    });

    async function startAlternativeAudio() {
        console.log('Starting alternative audio process');
        try {
            const transcript = await getTranscript();
            if (!transcript) {
                throw new Error('No transcript available. The video might not have captions.');
            }
            console.log('Transcript obtained:', transcript.substring(0, 100) + '...');

            const dataUrl = await getSynthesizedAudio(transcript);
            console.log('Synthesized audio obtained');

            // Create audio element
            synthesizedAudio = new Audio(dataUrl);

            const video = document.querySelector('video');
            if (!video) {
                throw new Error('No video element found.');
            }

            // Mute the video
            video.muted = true;

            // Sync playback
            video.addEventListener('play', onVideoPlay);
            video.addEventListener('pause', onVideoPause);
            video.addEventListener('seeked', onVideoSeeked);

            console.log('Alternative audio setup complete');
            // Notify the popup that the process is complete
            chrome.runtime.sendMessage({ action: 'process_complete' });
        } catch (error) {
            console.error('Error in startAlternativeAudio:', error);
            // Unmute the video in case of error
            const video = document.querySelector('video');
            if (video) {
                video.muted = false;
            }
            // Notify the popup that an error occurred
            chrome.runtime.sendMessage({ action: 'process_error', error: error.message });
        }
    }

    function stopAlternativeAudio() {
        const video = document.querySelector('video');
  
        // Unmute the video
        video.muted = false;
  
        if (synthesizedAudio) {
            synthesizedAudio.pause();
            synthesizedAudio.currentTime = 0;
            synthesizedAudio = null;
        }
  
        // Remove event listeners
        video.removeEventListener('play', onVideoPlay);
        video.removeEventListener('pause', onVideoPause);
        video.removeEventListener('seeked', onVideoSeeked);
    }

    function onVideoPlay() {
        if (synthesizedAudio) {
            synthesizedAudio.play().catch(error => console.error('Error playing audio:', error));
        }
    }

    function onVideoPause() {
        if (synthesizedAudio) {
            synthesizedAudio.pause();
        }
    }

    function onVideoSeeked() {
        if (synthesizedAudio) {
            synthesizedAudio.currentTime = video.currentTime % synthesizedAudio.duration;
        }
    }

    async function getTranscript() {
        console.log('Getting transcript');
        
        // Wait for the player to be ready
        await new Promise(resolve => {
            const checkPlayer = () => {
                if (window.ytInitialPlayerResponse || document.querySelector('#movie_player')) {
                    resolve();
                } else {
                    setTimeout(checkPlayer, 100);
                }
            };
            checkPlayer();
        });

        console.log('Player found, attempting to get player response');

        let playerResponse;
        
        // Try different methods to get the player response
        if (window.ytInitialPlayerResponse) {
            console.log('Using ytInitialPlayerResponse');
            playerResponse = window.ytInitialPlayerResponse;
        } else {
            console.log('Attempting to get player response from movie_player element');
            const playerElement = document.querySelector('#movie_player');
            if (playerElement && playerElement.getPlayerResponse) {
                playerResponse = playerElement.getPlayerResponse();
            }
        }

        if (!playerResponse) {
            console.log('Attempting to extract player response from page source');
            const pageSource = document.documentElement.outerHTML;
            const match = pageSource.match(/ytInitialPlayerResponse\s*=\s*({.+?});/);
            if (match) {
                try {
                    playerResponse = JSON.parse(match[1]);
                } catch (error) {
                    console.error('Error parsing player response from page source:', error);
                }
            }
        }

        if (!playerResponse) {
            console.error('No player response found');
            return null;
        }

[truncated — 2826 more characters]
```