chatGPTを使用して, Youtubeの字幕を要約する(2)

# 振り返り
前回, chatGPTのAPIを使用して字幕を取得してみた.
しかしうまく文章が纏められず, 自分の中ではGPT4を待ったほうが良いかという結論に至った

https://qiita.com/nagato0614/items/a5238962525c83159e00

しかし, 以下のような記事を見てプロンプトが問題だと分かったので修正して試してみた.

https://qiita.com/m-morohashi/items/391b350075ff91f4a694

あとgpt4で生成した気持ち悪い文章をやめてちゃんと書くことにしました.

# 目的

youtubeの内容を要約する.

#  方法

処理の流れ
1. youtubeの字幕を取得する.
2. 文字列を分割する.
    - chatGPTのトークン数制限を回避するため.
3. 分割した文字をchatGPTで要約していく.

# 大きな変更点
基本的に処理の流れは以前と一緒だがプロンプトが他の人のツールをベースにしている.
ここで{point}が出力する改行の数, {text}が要約したい文字列, {example}は箇条書きの例.
ほぼ一緒だが指示が変わっている.
引用した記事で書かれていたがだいぶ苦労しただけあり,非常に良い回答を出してくれる.

途中空白行があるがこれは意図的である.
これを入れる事によって正しく箇条書きしてくれる確率が上がった.
箇条書きの際に '-' を使用してほしかったが時々数字を入れてくるため置き換えている.


```python3
example = ""
for i in range(point):
    example += "- 箇条書き" + str(i + 1) + "\n"

input_text = """
### 指示 ###

今から渡す文章を, 日本語を使用して重要なポイントを{point}点で書いてください.

### 箇条書きの制約 ###

- 主語と述語を明確にする
- 日本語
- 最大{point}個　
- カンマは使用禁止
- 文末は . で終わる
- 文章の終わりは体言止め（体言止めの例： ｘｘを許容する ⇒ ｘｘを許容）

### 対象とする字幕の内容 ###

{text}

### 出力形式 ###

{example}
""".format(text=input_context, point=point, example=example)
- 箇条書き5
```

# 出力結果の考察

今回使用する動画は以下の動画

https://www.youtube.com/watch?v=YyhfK-aBo-Y&ab_channel=GOTOConferences


```text:入力したテキスト
[Music]right so I'm it so where's architecturebegin for me architecture ultimatelybegins and ends in the code becausethat's the thing that is ultimately realand a lot of people tend to considercode to be a detail the problem is thatthat is exactly what software issoftware is lots and lots of details allput together in a particular way it'snot-it's not hand-waving we might dothat in meetings we might do that atwhiteboards it might help us thinksometimes it might prevent us fromthinking but ultimately it comes out inthe detail that's where it actuallyhappens and so I'm not so I have thiskind of issue here is that I'm in thesecurity track and you know apart fromfeeling reasonably secure within myselfthat's about as far as security goes forme I don't that's not an area ofspecialization but one thing that isspecialized or rather an area ofinterest in which eyes do specialize iscoding practice coding quality how thatbuilds up to be the larger thing and itturns out there is a surprising overlapbetween these two areas if you want toreduce the attack surface area of yoursoftware it's no good just looking inone direction and saying we've got itcovered by frameworks or these guys havegot our back we're using the rightoperating system it's a full 360 and Iwant to cover one aspect of that whichis often neglected so my background andI care deeply about the code and editedthis book a few 
```

```text:出力された要約
- アーキテクチャは、コードに始まり、コードに終わる。
- ソフトウェアは、細かいディテールが集まってできている。
- コーディングの質がセキュリティにも影響する。
- セキュリティ対策は、フレームワークだけでなく、コードのディテールにも注目が必要。
- コーディングの質を向上させることが、セキュリティ対策にもつながる。
```

それっぽい要約ができている.
しかし以下のようなカイトが出てくることがある.

```text:入力したテキスト
 right operating system it's a full 360 and I want to cover one aspect of that which is often neglected so my background and I care deeply about the code and edited this book a few years ago and I also care about the bigger picture so I've got a couple of books there with word software architecture in the title which immediately qualifies me to talk about this I believe that's all you need but there's something else I'm known for a few years ago I started taking photographs of software failures in public places and this is everything this is 2000 and six 2006 at Madrid Airport terminal 1 and this is quite a visible failure I think it's fair to say there's lots of people wandering through kind of looking at that and you can kind of spot the sort of the more nerdy people whenever this kind of things happen because they kind of slow down and try and work out what operating system is about what works turns out I'd misidentified this for years and somebody correctly pointed out but this was actually a 16-bit networking stack but they misidentified as as Windows I tracked it down further this is DOS this is a DOS message from a particular networking stack this is DOS in 2006 ok dos had been dead we were it was the it has been dead so long that we now 
```

```text:出力された要約
- 講師は、コードに深い関心を持ち、ソフトウェアアーキテクチャについての著書も持っている。
- 講師は、ソフトウェアの失敗を公共の場で写真に収めており、その経験から多くのことを学んでいる。
- 講師は、オペレーティングシステム全体をカバーするが、特に、しばしば見落とされる側面について説明する。
- 講師は、自分の経験を通じて、ソフトウェアの成功や失敗についての洞察を提供する。
- 講師は、聴衆が理解しやすいように、分かりやすい言葉で説明することに力を入れている。
```

講師という単語から何らかの講義であるということが認識されている.
しかし, 5行目は講義の内容というよりはどのような人物かという説明になってしまっている.
他にもこの講師は説明が上手であるということや, この講義は語彙楽屋,言語学に焦点が当てられた授業という説明がある.
これは分割されたことによってどのような講義なのか認識できなくなっていると思われる.


# コードの作成

最後に作成したコードを示す.

## youtubeから字幕を取得するコード.
基本は日本語で取得する.

```python3:youtube_downloader.py
from youtube_transcript_api import *
import openai
from janome.charfilter import *
import tiktoken
from tiktoken.core import Encoding
from pytube import YouTube


class YoutubeDownloader:
    """
    YouTubeの動画をダウンロードしたり字幕などを取得するクラス
    """
    url: str
    id: str
    transcript_list: youtube_transcript_api.TranscriptList
    title: str

    def __init__(self, url: str):
        """

        :param url: YouTubeの動画URL
        """
        self.url = url
        self.id = self.extract_youtube_id(self.url)
        self.transcript_list = self.fetch()


    def extract_youtube_id(self, url: str):
        """
        YouTubeの動画IDをURLから抽出する関数

        Args:
            url (str): YouTubeの動画URL

        Returns:
            str: YouTubeの動画ID
        """
        # YouTubeの動画IDの正規表現パターン
        regex_pattern = r"(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|[^\/]+\?.*v=))([a-zA-Z0-9_-]{11})"

        # 正規表現パターンでIDを検索する
        match = re.search(regex_pattern, url)

        # マッチオブジェクトからIDを抽出する
        if match:
            return match.group(1)
        else:
            return None

    def find_ja_or_en_transcript(self):
        """
        指定された動画IDに対して、まず日本語の字幕を検索し、見つからない場合は英語の字幕を検索します。
        どちらの言語の字幕も見つからない場合は、何も返さずに終了します。

        Returns:
            Transcript: 見つかった字幕のTranscriptオブジェクト。どちらの言語の字幕も見つからない場合はNone。
        """
        transcript_list = YouTubeTranscriptApi.list_transcripts(self.id)

        try:
            # 日本語の字幕を探す
            ja_transcript = transcript_list.find_transcript(['ja'])
            return ja_transcript
        except NoTranscriptFound:
            try:
                # 日本語が見つからない場合は英語の字幕を探す
                en_transcript = transcript_list.find_transcript(['en'])
                return en_transcript
            except NoTranscriptFound:
                # 日本語と英語のどちらの字幕も見つからない場合はNoneを返す
                return None

    def fetch(self):
        """
        字幕のテキスト情報を抽出しアトリビュートに保存する
        :return: youtube_transcript_api.TranscriptList
        """
        transcript = self.find_ja_or_en_transcript()
        if transcript is None:
            return None
        self.transcript_list = transcript.fetch()
        return self.transcript_list

```

## 要約文を生成するコード

```python:summarizer.py
import tiktoken
import openai
from tiktoken.core import *
import json
from decimal import Decimal
import os
import re
from janome.tokenizer import Tokenizer


class GptSummarizer:
    """
    GPT-3を使って要約するクラス
    """

    # 分割されたテキスト
    split_text: list[str]

    # 要約されたテキストのリスト
    summary_text_list: list[str]

    # 受け取ったレスポンスのリスト
    response_list: list

    # 入力するテキスト
    input_text: str

    def __init__(self, text):
        """
        コンストラクタ
        :param text: 要約する文字列
        """
        self.text = text
        self.response_list = []

        self.split_into_many(1000)
        self.summarize()

    def save_summary(self, file_name: str):
        """
        要約したテキストリストを保存する
        :param file_name: 保存先のファイルパス
        :return:
        """

        # ディレクトリがなければ作成
        if not os.path.exists("../summary"):
            os.mkdir("../summary")

        # 時々箇条書きが数字になってしまうので '-' に置換する
        output_text_list = []
        for line in self.summary_text_list:
            pattern1 = r"(\d+\.)|(\d+\)) "
            replaced_text1 = re.sub(pattern1, "- ", line)
            output_text_list.append(replaced_text1)
        self.summary_text_list = output_text_list

        # ファイルに保存 utf-8で保存する
        save_file_path = "summary/" + file_name + ".txt"
        with open(save_file_path, mode='w', encoding='utf-8') as f:
            for line in self.summary_text_list:
                f.write(line + "\n")

    def get_token_number(self):
        """
        文章のトークン数を取得する関数
        :return: トークン数
        """

        encoding: Encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
        tokens = encoding.encode(self.text)
        tokens_count = len(tokens)
        return tokens_count

    def split_into_many(self, max_tokens=8191):
        """
        テキストを最大トークン数を超えないように分割する関数

        Args:
            max_tokens (int, optional): 分割後の各チャンクの最大トークン数。デフォルトはmax_tokens。

        Returns:
            list: 分割されたテキストのリスト
        """

        # テキストを単語に分割する
        tokenizer = Tokenizer()
        tokens = tokenizer.tokenize(self.text)
        words = [token.surface for token in tokens]

        # 各単語のトークン数を1として処理する
        n_tokens = [1 for _ in words]

        chunks = []
        tokens_so_far = 0
        chunk = []

        # 単語とトークンがタプルで結合されたものをループで処理する
        for word, token in zip(words, n_tokens):

            # これまでのトークン数と現在の単語のトークン数の合計が最大トークン数を超える場合、
            # チャンクをチャンクのリストに追加し、チャンクとこれまでのトークン数をリセットする
            if tokens_so_far + token > max_tokens:
                chunks.append("".join(chunk))
                chunk = []
                tokens_so_far = 0

            # 現在の単語のトークン数が最大トークン数を超える場合、次の単語に移動する
            if token > max_tokens:
                continue

            # それ以外の場合、単語をチャンクに追加し、トークン数を合計に追加する
            chunk.append(word)
            tokens_so_far += token + 1

        # 最後のチャンクを追加
        if chunk:
            chunks.append("".join(chunk))

        self.split_text = chunks
        return chunks

    def summarize(self):
        """
        分割されたテキストを要約する関数
        :return: 要約されたテキストのリスト
        """

        self.summary_text_list = []
        for split_t in self.split_text:
            summary = self.summarize_turbo(split_t)

            print("--------------------")
            print(split_t)
            print("--------------------")
            print(summary)

            self.summary_text_list.append(summary)
        return self.summary_text_list

    def generate_input_text(self, input_context, point=5):
        """
        GPT-3の入力テキストを生成する関数
        参考 : https://qiita.com/m-morohashi/items/391b350075ff91f4a694
        :param point: 生成する箇条書きの数
        :param input_context: 入力コンテキスト
        :return: GPT-3の入力テキスト
        """

        example = ""
        for i in range(point):
            example += "- 箇条書き" + str(i + 1) + "\n"

        self.input_text = """
### 指示 ###

今から渡す文章を, 日本語を使用して重要なポイントを{point}点で書いてください.

### 箇条書きの制約 ###

- 主語と述語を明確にする
- 日本語
- 最大{point}個　
- カンマは使用禁止
- 文末は . で終わる
- 文章の終わりは体言止め（体言止めの例： ｘｘを許容する ⇒ ｘｘを許容）

### 対象とする字幕の内容 ###

{text}

### 出力形式 ###

{example}
        """.format(text=input_context, point=point, example=example)

        return self.input_text

    def summarize_davinci(self, text):
        '''
        GPT-3のDavinciエンジンを使って文章を要約する関数
        :param text: 字幕のテキスト情報
        :return: 要約された字幕のテキスト情報
        '''

        def decimal_to_int(obj):
            if isinstance(obj, Decimal):
                return int(obj)

        input_text = self.generate_input_text(text)

        res = openai.Completion.create(
            engine="text-davinci-003",
            prompt=input_text,
            max_tokens=3000,
        )
        json.dumps(res, default=decimal_to_int, ensure_ascii=False)
        self.response_list.append(res)
        return res.choices[0].text

    def summarize_turbo(self, text):
        '''
        GPT-3のTurboエンジンを使って文章を要約する関数
        :param text: 要約する文章
        :return: 要約された文章
        '''

        input_text = self.generate_input_text(text)

        def decimal_to_int(obj):
            if isinstance(obj, Decimal):
                return int(obj)

        res = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {"role": "system", "content": "あなたは説明上手な講師です."},
                # 役割設定（省略可）
                {"role": "user", "content": input_text},
            ],
            temperature=0.5
        )
        json.dumps(res, default=decimal_to_int, ensure_ascii=False)
        self.response_list.append(res)
        return res["choices"][0]["message"]["content"]

```

## youtubeの要約を行うコード

```python:summary_youtube.py
import os
from summarizer import GptSummarizer
from youtube_downloader import YoutubeDownloader


class YoutubeSummarizer:
    """
    Youtubeの字幕を要約するクラス
    字幕をベクトル化して、要約する
    """

    # YoutubeDownloaderクラスのインスタンス
    youtube_downloader: YoutubeDownloader

    # YouTubeの動画タイトル
    title: str

    # YouTubeの動画ID
    id: str

    # Youtubeの字幕を纏めた文字列
    text: str

    summarizer: GptSummarizer

    def __init__(self, url: str):
        """
        Summaryクラスのコンストラクタ
        :param url: YouTubeの動画URL
        """
        self.response_list = []
        self.youtube_downloader = YoutubeDownloader(url)
        self.id = self.youtube_downloader.id
        self.transcript_list = self.youtube_downloader.transcript_list

        # 字幕を1つの文字列にまとめる
        self.text = ""
        for transcript in self.transcript_list:
            self.text += transcript["text"] + " "

        self.summarizer = GptSummarizer(self.text)

        self.save_text()
        self.summarizer.save_summary(self.youtube_downloader.id)

    def save_text(self):
        """
        textディレクトリに字幕を保存する関数
        :return: 保存したファイル名
        """

        # ディレクトリがなければ作成する
        if not os.path.exists("../text"):
            os.mkdir("../text")

        # ファイル名を指定
        file_name = f"{self.id}.txt"

        # ファイルを開く
        with open(f"text/{file_name}", "w") as f:
            # 字幕をファイルに書き込む
            for transcript in self.transcript_list:
                f.write(transcript["text"] + "\n")

        return file_name

    def remove_newlines(self, series):
        """
        文字列の改行を削除する関数
        :param serie: 文字列のSeries
        :return: 改行を削除した文字列のSeries
        """
        series = series.str.replace('\n', ' ')
        series = series.str.replace('\\n', ' ')
        series = series.str.replace('  ', ' ')
        series = series.str.replace('  ', ' ')
        return series

```

## main文

```python:main.py
from summary_youtube import *

def summary_youtube():
    url = "https://www.youtube.com/watch?v=YyhfK-aBo-Y&ab_channel=GOTOConferences"
    summary = YoutubeSummarizer(url)
    print(summary.text)
    print(summary.summarizer.summary_text_list)

if __name__ == '__main__':
    summary_youtube()
```

# 最後に
今後は, Djungoを使用して RESTfull frameworkを作りたいと考えています.
つづく?