さんだーさんだ!(ブログ版)

2015年度より中高英語教員になりました。2020年度開校の幼小中混在校で働いています。

【英語上級者向け】英語ポッドキャストを文字起こし&要約&単語抽出【ChatGPTさまさま】

 今年度から始めた英語コーチング、ぼちぼち続いています。(楽しい!
www.notion.so

 その中で、相当な英語上級者の方のご相談に乗ることも。目標や、それに紐づく学習内容やスケジュールを深掘りしていく中で、「英語ポッドキャストを聴きたい。でも、生の(ネイティヴが聞くような)ポッドキャストをそのまま聴くのは正直レベルが高くて難しい」という話を聞くことも。そうですよね〜。まさに自分のレベル感もこんな感じ。

 OpenAI社のWhisperの文字起こし精度が高いことは知っていたし、以下のブログ記事を参考に文字起こしに挑戦したりもしていた。
OpenAI Whisperに音声データを全文書き起こしさせる【Google Colaboratory】

 ChatGPTのプログラミング力もすごいと聞いていたので、それじゃあ、これらを組み合わせてやってみよう!と思った次第です。

注意書き

 以下は、僕の環境下では満足に動いていますが、コピペ(&ご自身なりの改変)したコードによって何らかの被害を被った場合も、僕の方では責任を取りかねますので、あしからずご了承くださいm(_ _)m
 それでは、以下、なんらか参考になりましたら幸いです。

①前準備〜Google Colab〜

【Google Colab】GPUが無料で使える!? 基本的な使い方 | Science By Python
 こちらを参考にしてください。(丸投げ。笑
 Googleドライブ上で、python環境がつくれます。さらに、一般人には手の届かない、高性能のGPUを使用することも可能。これにより、長時間の音声の文字起こしが、現実的な範囲の時間で可能になりました。上記記事を参考に「ランタイムのタイプ」をGPUにします。
 その他、以下の作業が事前に必要です。

  1. 自身のGoogleドライブ「マイドライブ」直下に「Colab Notebooks」というフォルダを作成
    1. これは↑の記事を参考にGoogle Colaboratoryを始めると、自動的に作成される…のか??
  2. そのフォルダ内に「content」「download」「downloaded」の各フォルダを作成
  3. 同フォルダ内に適当な名前のGoogle Colaboratoryファイルを作成
  4. ②と③のコードを、そのColabファイル内にそれぞれコピペ
    1. OpenAIのAPIについては、以下の記事などを参考に。OpenAIのAPI入門(GASコード付き)|サクッと始めるチャットボット【ChatGPT】
  5. 「content」フォルダ内に、文字起こししたいポッドキャスト(など)の音声ファイル(mp3)を置く
  6. ②を実行
  7. ③を実行

②事前設定

#@title Setting up
# https://zenn.dev/kazuki_tam/articles/d59250ecf25628 こちらを参考に。
# Install packages
!pip install git+https://github.com/openai/whisper.git
!pip install -qq openai
!pip install -qq tqdm

OPEN_API_KEY = 'ここにあなたのOpenAIのAPIキーを入力しておく'

from google.colab import drive
drive.mount('/content/drive')

#マイドライブ内の「Colab Notebooks」を基準フォルダとする。
import os
%cd '/content/drive/My Drive/Colab Notebooks/'

# マイドライブ -> Colab Notebooks内に「content」「download」「downloaded」の各フォルダがあるか確認
checkContentFolder = os.path.exists("content")
checkDownLoadFolder = os.path.exists("download")
checkDownLoadFolder = os.path.exists("downloaded")
# もしなければ、それぞれのフォルダを作成
if not checkContentFolder:
  os.mkdir("content")
if not checkDownLoadFolder:
  os.mkdir("download")
if not checkDownLoadFolder:
  os.mkdir("downloaded")

# ディレクトリ内の全ファイル名を取得
file_list = os.listdir('content')

# ファイル名一覧を表示
for file in file_list:
    print(file)
# これにより、文字起こししたいmp3ファイルのファイル名のコピペが容易になる

①注:APIの利用にお金はかかります。以下のページから使用料金を確認可能。OpenAI Platform
※数十ファイル文字起こししても数ドル、という感じでした。ざっくりですが。

③いよいよ文字起こし!

やること

  1. 文字起こし(Whisper)
  2. それをテキストファイルに書き込む(python
  3. 文字起こしテキストを分割する(python:次の手順でChatGPTに読ませられる長さに収めるため)
  4. 分割したテキストの、明らかな誤字脱字を修正する(ChatGPT API: プログラム内がプロンプト)
  5. 誤字脱字を修正したテキストを、要約する(ChatGPT API: がプロンプト)
  6. 誤字脱字を修正したテキストから、特徴的な英単語や英語表現を日本語訳付きで抽出(ChatGPT API: がプロンプト)
  7. それらのテキストをファイルに書き込む
  8. それらのテキストファイルをダウンロード
  9. ダウンロード済みファイルをダウンロード済みフォルダ(downloaded)に移動

それでは、以下のコードをどうぞ〜!

#@title Transcription -> Dividing the text -> Fixing, Summarizing, Extracting from the text -> Downloading the text -> Remove from the downloads


## Transcription
import whisper

# 各パラメータの指定
fileName = "hogehoge.mp3"#@param {type:"string"}
lang = "en"#@param ["en", "ja"]
model_size = "large"#@param ["tiny", "base", "small", "medium", "large"]
model = whisper.load_model(model_size)
# GPU
# 英語対談50分の文字起こし、smallだと4分半、largeだと12分半ほど。

# Load audio
audio = whisper.load_audio(f"content/{fileName}")

## Output the recognized text
#options = whisper.DecodingOptions(language=lang, without_timestamps=True)
#result = whisper.decode(model, mel, options)
#print(result.text)

result = model.transcribe(audio, verbose=True, temperature=0.8, language=lang)

# Write into a text file
with open(f"download/{fileName}[{model_size}].txt", "w") as f:
  f.write(f"▼ Transcription of {fileName}\n")

  # Initialize a variable to keep track of the last timestamp we printed
  last_timestamp_printed = 0

  for segment in result['segments']:
    start_time = segment['start']
    text = segment['text']

    # Check if start_time is at least 60 seconds greater than the last timestamp we printed
    if start_time - last_timestamp_printed >= 60:
        minutes = int(start_time // 60)
        seconds = int(start_time % 60)
        f.write(f"\n[{minutes}:{seconds:02}]\n")  # format seconds as two digits
        # Update the last timestamp we printed
        last_timestamp_printed = start_time

    # Write the text of each segment regardless of the timestamp
    f.write(f"{text}\n")


## Dividing the text
def split_text(text, max_length=9000):
    """
    Split text into chunks, each of max_length characters approximately.
    Try not to split in the middle of a sentence by looking for the last period within max_length characters.
    If none is found, split at max_length characters.
    """
    chunks = []
    while len(text) > max_length:
        split_index = text[:max_length].rfind(".")
        if split_index == -1:  # no period found
            split_index = max_length
        chunk = text[:split_index + 1]
        chunks.append(chunk)
        text = text[split_index + 1:]
    chunks.append(text)
    return chunks

#text = result["text"]
with open(f"download/{fileName}[{model_size}].txt", "r") as f:
  text = f.read()

chunks = split_text(text)

#以下はチャンクごとに区切れたかを確認するもの。プログラム上は不要。
for i, chunk in enumerate(chunks):
    print(f"Chunk {i+1}:")
    print(chunk)
    print()


## Fixing, Summarizing, Extracting from the text
import openai
from tqdm import tqdm

# OpenAIのAPIキーをセット
openai.api_key = OPEN_API_KEY

# 使用するモデル名をセット
model_name = 'gpt-3.5-turbo'#@param ['gpt-3.5-turbo', 'gpt-4']

# naiyo変数には音源の大まかな内容を記述します。
naiyo = "a dialogue program"#@param {type:"string"}

# 以下のorder1, order2, order3はChatGPTへの指示となるメッセージです。
# order1: 音声起こしテキストの校正を指示
# order2: テキストの要約を指示
# order3: 特徴的な単語や表現の抽出を指示
order1 = "As someone who possesses excellent proofreading skills, we would like you to proofread an AI transcription of " + naiyo +". It is important that you adhere to the following seven conditions in order to ensure the highest quality of work:\n1. It is crucial that you keep the original English text as intact as possible. You must not rewrite it without permission.\n2. The output is in English, so please do not translate it into Japanese.\n3. It is important that you do not erase [nn:nn] and return it to its original place after proofreading.\n4. After making corrections, please insert a line break before [nn:nn] to make it easier to read.\n5. You should correct any obvious typographical errors.\n6. It is important that you consider the context and add punctuation and line breaks to make it easier to read.\n7. Please review the first six conditions after completing your proofreading to ensure that they have been correctly executed. If any of the conditions have not been executed correctly, please redo them to match the conditions again."
order2 = "As an exceptional editor and translator, your task is to summarize an English text from " + naiyo + ". The summary should consist of an English sentence with a maximum of 100 words. Immediately following the English sentence, you must insert a line break and provide a Japanese translation of the summary. Finally, double-check that the first two conditions have been met and redo them if necessary."
order3 = "あなたは優秀な英語指導者です。以下は、" + naiyo + "の一部です。英語学習者向けに、この英文に特徴的な英語を抽出してください。以下の4つの条件を必ず守ってください。\n1. この英文の理解に役立つ【固有名詞】や【英単語】、【英語表現】を、分けて抽出する。\n2. 抽出した英語の後には、必ず日本語訳をつける。以下の形式を必ず守って出力すること。\n【英単語】\n-apple(りんご)\n-banana(バナナ)\n\n3. 抽出した個々の英語の出力は、「英語(日本語訳)」のように、日本語訳を括弧でくくって英語の直後に置く。他の余計な語は追加しないで出力する。\n4. 条件1~3を振り返り、正しく実行されていることを確かめる。もしも未実行のものがあったら、再度条件に合うようにやり直す。"
#order3 = "As an excellent English language teacher, your task is to extract the English words that are characteristic of " + naiyo + " and useful for English language learners. You should extract separate proper nouns, English words, and English expressions, and provide a Japanese translation immediately after each extracted English word in brackets (e.g. English word (Japanese translation)). Ensure that you do not add any extra words to the output and that you check that all four conditions have been correctly executed. If any of the conditions have not been executed, redo them to match the conditions again."

# ファイル名を設定
fixedfileName = fileName + '[' + model_size + ']' + '_fixed'
summaryfileName = fileName + '[' + model_size + ']' + '_summary'
vocabfileName = fileName + '[' + model_size + ']' + '_vocab'
allfileName = fileName + '[' + model_size + ']' + '_all'

# 校正、要約、抽出結果を保持するための変数を初期化
fix_text = ''
summary_text = ''
vocab_text = ''
all_text = ''

# テキストをチャンクに分割してそれぞれのチャンクに対して処理を行う
for chunk in tqdm(chunks):

    #プロンプト1の作成
    prompt1 = order1 + chunk

    # GPTによるチャンクの校正
    response1 = openai.ChatCompletion.create(
        model=model_name,
        messages=[
            {"role": "user", "content": prompt1},
        ],
    )

    # 校正結果を保持する変数に追加
    zantei1 = response1.choices[0]["message"]["content"].strip() + "\n"
    print(zantei1)
    fix_text += zantei1

    # プロンプト2の作成
    prompt2 = order2 + zantei1

   # GPTによるチャンクの要約
    response2 = openai.ChatCompletion.create(
        model=model_name,
        messages=[
            {"role": "user", "content": prompt2},
        ],
    )

    # 要約結果を保持する変数に追加
    zantei2=response2.choices[0]["message"]["content"].strip() + "\n"
    print(zantei2)
    summary_text += zantei2

    # プロンプト3の作成
    prompt3 = order3 + zantei1

    # order3: チャンクから特徴的な単語や表現の抽出
    response3 = openai.ChatCompletion.create(
        model=model_name,
        messages=[
            {"role": "user", "content": prompt3},
        ],
    )

    # 抽出結果を保持する変数に追加
    zantei3=response3.choices[0]["message"]["content"].strip() + "\n"
    print(zantei3)
    vocab_text += zantei3

    all_text += zantei1 + "\n★ここまでの要約・日本語訳★\n" + zantei2 + "\n★ここまでの特徴的な固有名詞・英単語・英語表現★\n" + zantei3

# 各結果をテキストファイルとして保存
with open(f"download/{fixedfileName}.txt", "w", encoding='utf_8') as f:
    f.write(fix_text)

with open(f"download/{summaryfileName}.txt", "w", encoding='utf_8') as f:
    f.write(summary_text)

with open(f"download/{vocabfileName}.txt", "w", encoding='utf_8') as f:
    f.write(vocab_text)

with open(f"download/{allfileName}.txt", "w", encoding='utf_8') as f:
    f.write(all_text)


## Download the files
from google.colab import files
!zip -r download.zip download
files.download("download.zip")


## Remove from the downloads
import shutil
import os

# ファイルがあるディレクトリと移動先のディレクトリを指定します
source_dir = './download'
target_dir = './downloaded'

# ディレクトリ内のすべてのファイルを列挙します
files = os.listdir(source_dir)

# 各ファイルに対して移動操作を行います
for file in files:
    shutil.move(os.path.join(source_dir, file), target_dir)

要改善ポイント

 正直、プロンプトの質がまだまだ…と思っています。出力されたファイルを見ると、こちらの意図通りに動いていないな…と思うこともしばしば。タイムスタンプが消えたり、要約が英語のみだったり、単語集に英語の定義がついていたり。
 ただしこれは、gpt-3.5-turboのAPIを使っているからかも。gpt-4のAPIも使えるよう申請している最中(参考:GPT-4 API waitlist)なので、そちらが使えるようになったら、だいぶ改善するかな…??

思わぬ効果

 こうやって、自分で学習環境を整えたことで、毎朝曜日替わりでポッドキャストをダウンロード→文字起こし→かる〜く予習してから行き帰りの車で聞く、とできていて、なんだかんだ英語力伸びてるんじゃない?と感じている。やはり自分で自分の学びをつくるの、大事!

最後にー

  1. 今回、それなりにコストをかけて上記のコードを作成したので、「面白かった!」「ためになった!」という方は、投げ銭的に本記事の以下の部分をご購入いただけたら幸いです。m(_ _)m
    1. 購入特典として、英語ポッドキャスト(の多く)の音源がダウンロードできるwebサイトの情報を載せておきます。参考になれば幸いですが、おまけ程度にお考えくださいませ!
  2. プログラミングの環境設定、なにげに初めての人は面倒だと思います。そういったサポートも有償で行おうと思うので、ご興味ある方はお声かけください。
この続きはcodocで購入