18 Final Project: YouTube Transcript Translator

image.png Ծառավ հաֆո, լուսանկարի հղումը, Հեղինակ՝ Rafael Ishkhanyan

Open In Colab (ToDo)

🎦 Տեսադասեր + լրացուցիչ (ToDo)

ToDo 1. Տեսություն 2025
2. Տեսություն 2023 (ToDo)
3. Գործնական 2025
4. Գործնական 2023 (ToDo)
5. Որոշ տնայինների լուծումներ (ToDo)

Google Forms ToDo

📚 Նյութը

Դասընթացի առաջին կտորը ամփոփող հարցում։ Մերսի լրացնելու համար: Սա շնորհակալության նոտաներ։

Plan

Use all 4 pillars of OOP and data classes 1. Extract metadata for given video from youtube 2. Extract transcript 3. Translate the transcript

First of all, you may want to create a venv

  • conda create -n youtube
  • conda activate youtube

YouTube video metadata (title, description, etc.)

!pip install pytubefix 
ERROR: Invalid requirement: '#': Expected package name at the start of dependency specifier
    #
    ^

We use pytubefix, because the original pytube is not working anymore.

from typing import List
from datetime import datetime
from pytubefix import YouTube
from dataclasses import dataclass, asdict
sample_video = "https://www.youtube.com/watch?v=tERRFWuYG48"
yt = YouTube(sample_video)
dir(yt)
yt.__dir__()[15:20]
['fallback_clients',
 '_signature_timestamp',
 '_visitor_data',
 'stream_monostate',
 '_author']
yt.title, yt.video_id, yt.keywords, yt.views, yt.length
('Barfuß Am Klavier - AnnenMayKantereit',
 'tERRFWuYG48',
 ['AnnenMayKantereit',
  'Barfuß Am Klavier',
  'oft gefragt',
  'henning may',
  'klavier'],
 76843686,
 201)
@dataclass
class Video:
    video_id: str
    title: str
    keywords: List[str]
    views: int
    length: int
    # published: datetime
@dataclass()
class VideoInfo:
    video_id: str
    title: str
    keywords: List[str]
    publish_date: datetime  
    length_seconds: int
VideoInfo(video_id=1, title="Sample Video", keywords=["sample", "video"], publish_date="2023-10-01", length_seconds=300)
VideoInfo(video_id=1, title='Sample Video', keywords=['sample', 'video'], publish_date='2023-10-01', length_seconds=300)

Does not throw an error although the video_id is not an int. We’ll use pydantic to validate the data in future

class YouTubeVideo:
    def __init__(self, url: str):
        self.video = YouTube(url)

    def __str__(self): # polymorphism
        return f"{self.video.title} ({self.video.video_id})"

    def get_metadata(self) -> VideoInfo:
        return VideoInfo(
            video_id=self.video.video_id,
            title=self.video.title, 
            keywords=self.video.keywords,
            publish_date=self.video.publish_date,
            length_seconds=self.video.length
        )
    
video = YouTubeVideo(sample_video)
print(video)
Barfuß Am Klavier - AnnenMayKantereit (tERRFWuYG48)
vid_info = video.get_metadata()
asdict(vid_info)
{'video_id': 'tERRFWuYG48',
 'title': 'Barfuß Am Klavier - AnnenMayKantereit',
 'keywords': ['AnnenMayKantereit',
  'Barfuß Am Klavier',
  'oft gefragt',
  'henning may',
  'klavier'],
 'publish_date': datetime.datetime(2014, 10, 27, 4, 6, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))),
 'length_seconds': 201}
@dataclass()
class VideoInfo:
    video_id: str
    title: str
    keywords: List[str]
    publish_date: str  
    length_seconds: int
    
    @staticmethod
    def get_days_since_publish(publish_date) -> int:
        if isinstance(publish_date, datetime):
            publish_date = publish_date.strftime("%Y-%m-%d")
        publish_date = datetime.strptime(publish_date, "%Y-%m-%d")
        current_date = datetime.now()
        return (current_date - publish_date).days
        
        
    def __post_init__(self):
        if not isinstance(self.video_id, str):
            raise ValueError("video_id must be a string")
        if not isinstance(self.length_seconds, int):
            raise ValueError("length_seconds must be an integer")
        
        self.days_since_publish = self.get_days_since_publish(self.publish_date)
vid_info = video.get_metadata()
vid_info.days_since_publish
3894

YouTube transcript

yt = YouTube(sample_video)
yt.captions
{'de': <Caption lang="German" code="de">}
captions = yt.captions.get("de")

# captions.generate_txt_captions()
# dir(captions)
captions.generate_srt_captions()
'1\n00:00:11,139 --> 00:00:13,330\nIch sitz schon wieder barfuß am Klavier.\n\n2\n00:00:17,300 --> 00:00:24,340\nIch träume Liebeslieder und sing dabei von dir.\n\n3\n00:00:24,340 --> 00:00:33,650\nDu und ich, wir waren wunderlich.\n\n4\n00:00:33,650 --> 00:00:35,950\nNicht für mich.\n\n5\n00:00:35,950 --> 00:00:41,449\nFür die, die es störte, wenn man uns nachts hörte.\n\n6\n00:00:41,449 --> 00:00:47,520\nIch hab mit dir gemeinsam einsam rumgesessen und geschwiegen.\n\n7\n00:00:47,520 --> 00:00:52,300\nIch erinnere mich am Besten ans gemeinsam einsam Liegen.\n\n8\n00:00:52,300 --> 00:01:01,960\nJeden Morgen danach bei dir; du nackt im Bett – und ich barfuß am Klavier.\n\n9\n00:01:02,960 --> 00:01:08,920\nUnd ich sitz schon wieder barfuß am Klavier.\n\n10\n00:01:08,920 --> 00:01:19,369\nIch träume Liebeslieder und sing dabei von dir.\n\n11\n00:01:19,369 --> 00:01:26,990\nDu und ich, das ging so nicht.\n\n12\n00:01:33,610 --> 00:01:38,180\nDu wolltest alles wissen und das hat mich vertrieben.\n\n13\n00:01:38,180 --> 00:01:43,630\nEigentlich dich, du bist nicht länger geblieben; bei mir.\n\n14\n00:01:43,630 --> 00:01:49,759\nAlso sitz ich, um zu lieben, lieber barfuß am Klavier.\n\n15\n00:01:49,759 --> 00:01:58,909\nUnd ich sitz schon wieder barfuß am Klavier.\n\n16\n00:01:58,909 --> 00:02:07,340\nIch träume Liebeslieder und sing dabei von dir.\n\n17\n00:02:07,340 --> 00:02:38,420\nDu und ich, wir waren zu wenig.\n\n18\n00:02:38,420 --> 00:02:46,400\nIch sitz schon wieder barfuß am Klavier.\n\n19\n00:02:46,500 --> 00:02:49,230\nUnd träum dabei von dir.\n\n20\n00:02:49,230 --> 00:03:21,500\nIch träum dabei von dir.'
captions.download(title=yt.title, output_path=yt.video_id)
'c:\\Users\\hayk_\\OneDrive\\Desktop\\python_math_ml_course\\python\\tERRFWuYG48\\Barfuß Am Klavier - AnnenMayKantereit (de).srt'

Class

class YouTubeVideo:
    def __init__(self, url: str):
        self.video = YouTube(url)

    def get_metadata(self) -> VideoInfo:
        return VideoInfo(
            video_id=self.video.video_id,
            title=self.video.title, 
            keywords=self.video.keywords,
            publish_date=self.video.publish_date,
            length_seconds=self.video.length
        )
    
    def get_transcript(self, language: str = "de") -> str:
        captions = self.video.captions.get(language)
        if not captions:
            raise ValueError(f"No captions available for language: {language}")
        self.text = captions.generate_txt_captions()
        return self.text
    
    def download_transcript(self, language: str = "de", title: str = "transcript", output_path: str = "transcript.txt") -> None:
        captions = self.video.captions.get(language)
        if not captions:
            raise ValueError(f"No captions available for language: {language}")
        captions.download(title=title, output_path=output_path)
        
video = YouTubeVideo(sample_video)

transcript = video.get_transcript(language="hy")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [62], in <cell line: 3>()
      1 video = YouTubeVideo(sample_video)
----> 3 transcript = video.get_transcript(language="hy")

Input In [61], in YouTubeVideo.get_transcript(self, language)
     15 captions = self.video.captions.get(language)
     16 if not captions:
---> 17     raise ValueError(f"No captions available for language: {language}")
     18 self.text = captions.generate_txt_captions()
     19 return self.text

ValueError: No captions available for language: hy
transcript = video.get_transcript(language="de")

Audio, video

yt = YouTube(sample_video)
# yt.streams.filter(file_extension="mp4").desc().first().download()
<Stream: itag="18" mime_type="video/mp4" res="360p" fps="25fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" sabr="False" type="video">
yt.streams.get_audio_only()
<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" sabr="False" type="audio">
output_path = f"{yt.video_id}.mp3"

yt.streams.get_audio_only().download(filename=output_path)
'c:\\Users\\hayk_\\OneDrive\\Desktop\\python_math_ml_course\\python\\tERRFWuYG48.mp3'
output_path = f"{yt.video_id}.mp4"

yt.streams.get_lowest_resolution().download(filename=output_path)
'c:\\Users\\hayk_\\OneDrive\\Desktop\\python_math_ml_course\\python\\tERRFWuYG48.mp4'
class YouTubeVideo:
    def __init__(self, url: str):
        self.video = YouTube(url)

    def get_metadata(self) -> VideoInfo:
        return VideoInfo(
            video_id=self.video.video_id,
            title=self.video.title, 
            keywords=self.video.keywords,
            publish_date=self.video.publish_date,
            length_seconds=self.video.length
        )
    
    def get_transcript(self, language: str = "de") -> str:
        captions = self.video.captions.get(language)
        if not captions:
            raise ValueError(f"No captions available for language: {language}")
        self.text = captions.generate_txt_captions()
        return self.text
    
    def download_transcript(self, language: str = "de", title: str = "transcript", output_path: str = "transcript.txt") -> None:
        captions = self.video.captions.get(language)
        if not captions:
            raise ValueError(f"No captions available for language: {language}")
        captions.download(title=title, output_path=output_path)
    
    def download_audio(self, output_path=None) -> None:
        """Download the audio stream of the YouTube video.

        Args:
            output_path (_type_, optional): _description_. Defaults to None.

        Raises:
            ValueError: _description_
        """
        if output_path is None:
            output_path = self.video.video_id
        
        audio_stream = self.video.streams.get_audio_only()
        if not audio_stream:
            raise ValueError("No audio stream available")
        audio_stream.download(output_path=output_path)
        
    def download_video(self, output_path=None) -> None:
        if output_path is None:
            output_path = self.video.video_id
        
        video_stream = self.video.streams.get_lowest_resolution()
        if not video_stream:
            raise ValueError("No video stream available")
        video_stream.download(output_path=output_path)
yt = YouTubeVideo(sample_video)
yt.download_audio("panir")
yt.download_video()

Translate

We’re gonna use googletrans and DeepL. Maybe more stuff later.

Google Translate

!pip install googletrans 
import googletrans
from googletrans import Translator

# https://github.com/ssut/py-googletrans/tree/main
# https://stackoverflow.com/questions/55409641/asyncio-run-cannot-be-called-from-a-running-event-loop-when-using-jupyter-no

async def translate_text(text):
    async with Translator() as translator:
        result = await translator.translate(text, dest='hy')
        print(result)  # Translated(src=en, dest=hy, text=պանիր, pronunciation=panir, extra_data="{'translat...")

await translate_text(text="cheese")
Translated(src=en, dest=hy, text=պանիր, pronunciation=panir, extra_data="{'translat...")
# googletrans.LANGUAGES

DeepL

SETX DEEPL_API_KEY some_string

SUCCESS: Specified value was saved.
echo %DEEPL_API_KEY%
%DEEPL_API_KEY%
import os
os.environ
os.environ.get("DEEPL_API_KEY")
import deepl

auth_key = os.getenv('DEEPL_API_KEY')
assert auth_key is not None, "Please set the DEEPL_API_KEY environment variable."

deepl_client = deepl.Translator(auth_key)
deepl_client.get_source_languages()
print("Source languages:")
for language in deepl_client.get_source_languages():
    print(f"{language.name} ({language.code})")  # Example: "German (DE)"

print("Target languages:")
for language in deepl_client.get_target_languages():
    if language.supports_formality:
        print
        
        (f"{language.name} ({language.code}) supports formality")
        # Example: "Italian (IT) supports formality"
    else:
        print(f"{language.name} ({language.code})")
        # Example: "Lithuanian (LT)"

վայ, հայերեն չկար :)

result = deepl_client.translate_text("Ich liebe dich", 
                                     target_lang="EN-US")
print(result.text) 
I love you

Class

from abc import ABC, abstractmethod
class BaseTranslator(ABC):
    @abstractmethod
    def translate(self, text: str, target_language: str) -> str:
        pass
    
    @abstractmethod
    def detect_language(self, text: str) -> str:
        pass
    
    @abstractmethod
    def get_supported_languages(self) -> List[str]:
        pass

Google Translate

class GoogleTranslator(BaseTranslator):
    """Google Translate API implementation of BaseTranslator."""
    @staticmethod
    async def translate(text, target_lang):
        async with Translator() as translator:
            result = await translator.translate(text, dest=target_lang)
            return result.text        

    @staticmethod
    def detect_language(text: str) -> str:
        pass 
    
    @staticmethod   
    def get_supported_languages() -> List[str]:
        return googletrans.LANGUAGES 
gt = GoogleTranslator()
gt.get_supported_languages()
gt = GoogleTranslator()

# gt.translate(text="Ich liebe dich", target="EN-US")
await gt.translate(text="Ich liebe dich", target_lang="en")
'I love you'

DeepL

class DeepLTranslator(BaseTranslator):
    def __init__(self, api_key) -> None:
        try:
            deepl_client = deepl.DeepLClient(api_key)
        except:
            raise ValueError("Invalid DeepL API key provided.")
        self._client = deepl_client # procted
        
    def translate(self, text: str, target_lang: str) -> str:
        result = self._client.translate_text(text, target_lang=target_lang)
        return result.text  
    
    def detect_language(self, text: str) -> str:
        pass 
    
    def get_supported_languages(self) -> List[str]:
        pass
dl = DeepLTranslator(api_key=os.getenv('DEEPL_API_KEY'))
dl.translate(text="Ich liebe dich", target_lang="EN-US")
'I love you'

Putting everything together

import json

class Pipeline(YouTubeVideo):
    def __init__(self, url: str, deepl_api_key: str):
        YouTubeVideo.__init__(self, url) # super().__init__(url)
        self.dl_translator = DeepLTranslator(api_key=deepl_api_key)
        self.google_translator = GoogleTranslator()
        
        self.result = None
    
    async def get_translated_transcript(self) -> str:
        self.transcript = self.get_transcript()
        
        if not self.transcript:
            raise ValueError("No transcript available for this video.")
        
        text_google = await self.google_translator.translate(text=self.transcript, target_lang="en")
        text_deepl = self.dl_translator.translate(text=self.transcript, target_lang="EN-US")

        self.result = {
            "original": self.transcript,
            "google": text_google,
            "deepl": text_deepl
        }
        
        return self.result
    
    def save_transcript_json(self, output_path: str = "transcript.json") -> None:
        if self.result is None:
            self.get_translated_transcript()
        
        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(self.result, f, ensure_ascii=False, indent=4)
p = Pipeline(sample_video, deepl_api_key=os.getenv('DEEPL_API_KEY'))
p.get_transcript()
'Ich sitz schon wieder barfuß am Klavier. Ich träume Liebeslieder und sing dabei von dir. Du und ich, wir waren wunderlich. Nicht für mich. Für die, die es störte, wenn man uns nachts hörte. Ich hab mit dir gemeinsam einsam rumgesessen und geschwiegen. Ich erinnere mich am Besten ans gemeinsam einsam Liegen. Jeden Morgen danach bei dir; du nackt im Bett – und ich barfuß am Klavier. Und ich sitz schon wieder barfuß am Klavier. Ich träume Liebeslieder und sing dabei von dir. Du und ich, das ging so nicht. Du wolltest alles wissen und das hat mich vertrieben. Eigentlich dich, du bist nicht länger geblieben; bei mir. Also sitz ich, um zu lieben, lieber barfuß am Klavier. Und ich sitz schon wieder barfuß am Klavier. Ich träume Liebeslieder und sing dabei von dir. Du und ich, wir waren zu wenig. Ich sitz schon wieder barfuß am Klavier. Und träum dabei von dir. Ich träum dabei von dir.'
res = await p.get_translated_transcript()
res
{'original': 'Ich sitz schon wieder barfuß am Klavier. Ich träume Liebeslieder und sing dabei von dir. Du und ich, wir waren wunderlich. Nicht für mich. Für die, die es störte, wenn man uns nachts hörte. Ich hab mit dir gemeinsam einsam rumgesessen und geschwiegen. Ich erinnere mich am Besten ans gemeinsam einsam Liegen. Jeden Morgen danach bei dir; du nackt im Bett – und ich barfuß am Klavier. Und ich sitz schon wieder barfuß am Klavier. Ich träume Liebeslieder und sing dabei von dir. Du und ich, das ging so nicht. Du wolltest alles wissen und das hat mich vertrieben. Eigentlich dich, du bist nicht länger geblieben; bei mir. Also sitz ich, um zu lieben, lieber barfuß am Klavier. Und ich sitz schon wieder barfuß am Klavier. Ich träume Liebeslieder und sing dabei von dir. Du und ich, wir waren zu wenig. Ich sitz schon wieder barfuß am Klavier. Und träum dabei von dir. Ich träum dabei von dir.',
 'google': "I am already sitting barefoot on the piano again. I dream love songs and sing from you. You and me, we were wonderful. Not for me. For those who bothered when we were heard at night. I lonely lonely with you and kept silent. The best way to remember lonely remember. Every morning with you; You naked in bed - and I barefoot on the piano. And I'm already sitting barefoot on the piano again. I dream love songs and sing from you. You and me, it didn't work that way. You wanted to know everything and that drove me out. Actually you, you no longer stayed; with me. So to love, I prefer barefoot on the piano. And I'm already sitting barefoot on the piano again. I dream love songs and sing from you. You and me, we were too little. I am already sitting barefoot on the piano again. And dream of you. I dream of you.",
 'deepl': "I'm sitting barefoot at the piano again. I'm dreaming love songs and singing about you. You and I, we were strange. Not for me. For those who were disturbed by hearing us at night. I sat around with you, lonely and silent. I remember lying alone together best. Every morning afterwards with you; you naked in bed - and me barefoot at the piano. And I'm sitting barefoot at the piano again. I dream love songs and sing about you. You and I, that wasn't possible. You wanted to know everything and that drove me away. Actually you, you didn't stay with me any longer. So in order to love, I prefer to sit barefoot at the piano. And I'm sitting barefoot at the piano again. I dream love songs and sing about you. You and I, we weren't enough. I'm sitting barefoot at the piano again. And dreaming of you. I'm dreaming of you."}
p.save_transcript_json()

🎲 18

Flag Counter