Anthropic Claude で文字起こしを要約する際のプロンプトエンジニアリング

この記事は [Anthropic Advent Calendar](https://qiita.com/advent-calendar/2023/anthropic-claude) の 10 日目の投稿です。

世の中にはたくさんの動画や記事が出回っていますが 1 日の時間は限られており、なかなか全て確認するのは難しいです。
効率よく情報収集するためには要約して情報を圧縮する必要があります。

今回は大量の Youtube 動画を Anthropic Claude を使用して要約して記事にしたのでその際に利用したプロンプトエンジニアリングテクニックについてご紹介します。

## 要約してみたもの

今回は re:Invent 2023 のセッション動画で公開されているものを全て要約してみました。

re:Invent は AWS (Amazon Web Services) が毎年開催するクラウドコンピューティングの大規模カンファレンスで1週間にわたって 2000 以上のセッションが行われます。

現時点で公開されている動画だけでも 700 近くあり、事例やベストプラクティスが多数紹介されていて非常に参考にはなるのですがとてもですが全部は見切れません。

なので全ての動画から要点だけ抽出して興味のあるものだけ動画で深掘りして確認できるようにインデックス化しました。その裏側を紹介していきます。

https://zenn.dev/tmae/articles/ecd30c6e0e65da

## Why Claude?

Claude は 200K トークンまでの文章を処理することができ、さらに[非常に長い文章でも精度を落とさずに情報を抽出できる](https://www.anthropic.com/index/claude-2-1-prompting)ため、特に長い動画の要約に適しています。

例えば、Adam Selipsky のキーノートは2時間15分もあり文字起こしは 19108 word (121762 character) もあります。

https://www.youtube.com/watch?v=PMfn9_nTDbM

内容をチャンク化して情報抽出させることもできますが、このような長い動画の中から前後のコンテキストも考慮しながら知りたい情報を正しく抽出し要約するには長いコンテキストを処理できるモデルとプロンプトエンジニアリングが必要です。

## Claude で大量の Youtube 動画を要約する

まずは、Youtube のプレイリストから全動画の文字起こしを取得するために必要なライブラリをインストールします。

```bash
!pip install youtube-transcript-api
!pip install google-api-python-client
```

Claude を使用するために Amazon Bedrock の boto3 クライアント と Youtube API のクライアントを作成します。

```python
import urllib.request
import os
import pandas as pd
import json
import boto3
import re

from youtube_transcript_api import YouTubeTranscriptApi
import googleapiclient.discovery

from pptx import Presentation
from pptx.enum.text import MSO_AUTO_SIZE
from PIL import Image

bedrock_client = boto3.client("bedrock-runtime")

# Youtube API クライアントを取得してプレイリスト情報を取得する

api_service_name = "youtube"
api_version = "v3"
DEVELOPER_KEY = "<Youtube API Key>" # https://qiita.com/shinkai_/items/10a400c25de270cb02e4


def get_authenticated_service():
    return googleapiclient.discovery.build(
        api_service_name, api_version, developerKey = DEVELOPER_KEY)

youtube = get_authenticated_service()
```

プレイリスト ID から title, Description, video_id を取得する関数を定義します。

```python
def get_video_id_in_playlist(playlistId):
    video_id_list = []

    request = youtube.playlistItems().list(
        part="snippet",
        maxResults=50,
        playlistId=playlistId,
        fields="nextPageToken,items/snippet/resourceId/videoId,items(id,snippet(title,description))"
    )

    while request:
        response = request.execute()
        video_id_list.extend(list(map(lambda item: { 
            "video_id": item["snippet"]["resourceId"]["videoId"], 
            "title": item["snippet"]["title"],
            "description": item["snippet"]["description"],
        }, response["items"])))
        request = youtube.playlistItems().list_next(request, response)

    return video_id_list
```

次に、video_id から文字起こしを取得し、要点を抽出するための関数を定義します。

ここでさまざまなプロンプトエンジニアリングテクニックを実践しているため一つ一つ紹介していきます。
Claude のプロンプトエンジニアリング基本的なものについては @kiiwamit さんが書かれている [Anthropic Claude - Prompt Design大全](https://qiita.com/kiiwami/items/4a62a3dcbedeb141e605#system-prompt-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B) を参照いただき、ここでは応用と実践編として今回のユーズケースにおいて活用したプロンプトエンジニアリングテクニックについて解説します。

1. まず、要点抽出を英語での要点抽出と日本語への翻訳の2ステップに分けています。試した限りでは直接英語の文字起こしから日本語で要点抽出を行うよりも、英語で要点抽出を行い、日本語に翻訳する方が要点抽出においてより多くの重要情報をピックアップすることができ、また出力フォーマットの指定も反映されやすい傾向があります。Step by Step や Chain of Thought などのテクニックと同様に、複雑なタスクはステップを分解するとデバッグもしやすいですし精度も出やすい傾向があります。
1. 情報抽出の際は、プロンプトにどのような情報を抽出して欲しいのか、またどのような出力フォーマットにするべきか明示すると期待する出力を安定的に得ることが可能です。その際、`<rule></rule>` や `<output-format></output-format>` など XML で区切ると Claude の場合はより安定して指示を守るようになります。今回はアナウンスされた機能と紹介された事例については特にフォーカスして抽出させるようにし、また出力形式は記事にした際の手直しを減らすために、日経クロステック風を指定することで文体や技術レベルの大まかなイメージを指定するとともに固有名詞をアルファベットにするなどを指定しています。ちなみに「口語でアルファベットはカタカナの読みに変換する」などと指定すると Polly などで読み上げさせてパワポ生成と組み合わせて動画生成も可能です。
1. 文字起こしだけで情報抽出も可能ですが、その場合 Youtube の文字起こしで間違っている単語については間違ったままになり後ほど修正する必要が出てきます。（一応ある程度は補正してくれますが特に re:Invent の新機能の新単語やマイナーな会社名などに対応しずらい）これに対して、動画の概要欄のテキストも要約時に参照できるようにすることである程度自動で文字起こしの間違いを修正した出力を行うようになります。またタイムスタンプ情報なども概要欄に書いてある場合は概要欄の情報を入れることでより情報抽出の漏れも減りました。
1. 余分な出力をなくし答えだけを出力させるために出力を `<output></output>` で囲むよう指示した上で、`Assistant: <output>` から答えだけを出力させ出力結果を安定させています。

```python
def get_transcript(video_id):
    """
    Youtube から文字起こしを取得
    """
    transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['en', 'ko', 'ja', 'de', 'en-US'])
    return " ".join([partial["text"] for partial in transcript])


def generate(prompt):
    """
    Claude の呼び出しのラッパー
    """
    payload = json.dumps({
        "prompt": prompt,
        "max_tokens_to_sample": 2000,
        "temperature": 0.6,
        "top_k": 250,
        "top_p": 0.999,
        "stop_sequences": ["Human: ", "Assistant: "],
    })

    model_id = "anthropic.claude-instant-v1"
    accept = "application/json"
    content_type = "application/json"

    response = bedrock_client.invoke_model(
        body=payload, modelId=model_id, accept=accept, contentType=content_type
    )
    
    output = json.loads(response.get("body").read())
    output_txt = output["completion"]
    return output_txt


def summarize(description, transcript):
    prompt = f"""
Human: Extract key information from <description> and <transcript> and write summary of content in bullet points.

<rule>
List up as many important information so no important information is missing.
Focus on announced new service or feature as well as customer case study.
</rule>

<output-format>
- xxx
- xxx
...
</output-format>

<description>
{description}
</description>

<transcript>
{transcript}
</transcript>

Only output result wrapped in <output></output>
Do not output anything else.

List up as many important information so no important information is missing.
Focus on announced new service or feature as well as customer case study.

Assistant: 
<output>
"""
    output_txt = generate(prompt)
    return output_txt.replace("<output>", "").replace("</output>", "")


def translate(text):
    prompt = f"""
Human: input のまとめを日本語に翻訳してください。

<rule>
全ての情報を漏らさず箇条書きで日経クロステック風に IT エンジニアに伝わるような文章に変換してください。
AWS サービスなど固有名詞でアルファベットが適当なものはアルファベットのままにしてください。
</rule>

<input>
{text}
</input>

<output-example>
- xxx
- xxx
...
</output-example>

出力は要約結果だけを <output></output> の xml タグで囲って出力してください。
それ以外の文章は一切出力してはいけません。例外はありません。

Assistant: 
<output>
"""
    output_txt = generate(prompt)
    return output_txt.replace("<output>", "").replace("</output>", "")


def create_title(title, text):
    prompt = f"""
Human: title と summary から日本語の記事タイトルを考案してください

<title>
{title}
</title>

<summary>
{text}
</summary>

出力は要約結果だけを <output></output> の xml タグで囲って出力してください。
それ以外の文章は一切出力してはいけません。例外はありません。

Assistant: 
<output>
"""
    output_txt = generate(prompt)
    return output_txt.replace("<output>", "").replace("</output>", "").strip()


def summarize_video(title, description, video_id):
    print(f"Working on {title}")
    print("Summarizing video...\n")
    transcript = get_transcript(video_id)
    english_summary = summarize(description, transcript)
    print(english_summary)
    
    japanese_summary = translate(english_summary)
    article_title = create_title(title, japanese_summary)
    return article_title, japanese_summary


def playlist2article(playlist, visited):
    articles = []
    for item in playlist:
        # タイトルとセッション ID をパース（re:Invent の Innovation Session / Breakout Session 特有）
        try:
            title, session_id = item["title"].replace("AWS re:Invent 2023 - ", "").rsplit("(", 1)
        except:
            try:
                title, session_id = item["title"].replace("AWS re:Invent 2023 - ", "").rsplit("|", 1)
            except:
                try:
                    title, session_id = item["title"].replace("AWS re:Invent 2023 - ", "").rsplit("-", 1)
                except:
                    print("Title parsing Failed: ", item["title"])
                    continue
        title = title.strip()
        session_id = session_id.replace(")", "")

        # すでに要約済みの場合はスキップ
        if session_id in visited:
            continue

        print("\n\n####")
        print(title, session_id)
        try:
            article_title, japanese_summary = summarize_video(title, item["description"], item["video_id"])
            print(japanese_summary)
        except Exception as e:
            print("Error: ", e)
            continue
        articles += [{
            "session_id": session_id,
            "article_title_en": title,
            "article_title_ja": article_title,
            "article_summary": japanese_summary,
            "video_id": item["video_id"],
        }]
    return articles
```

最後にプレイリストを指定してプレイリストの動画全ての要約を作成します。プレイリストが更新された時のために、差分のみ実行するようにしています。

```python
# Breakout Session の Playlist ID
breakout = "PL2yQDdvlhXf93SMk5EpQVIq4kdWQhUcMV"
playlist = get_video_id_in_playlist(breakout)

try:
    output_file = "data/breakout.csv"
    existing_articles = pd.read_csv(output_file)
    visited = set(existing_articles["session_id"])
except:
    existing_articles = pd.DataFrame()
    visited = {}
print(visited)

articles = playlist2article(playlist, visited)

updated_articles = pd.concat([existing_articles, pd.DataFrame(articles)])
updated_articles.to_csv(output_file, index=False)
```

あとは抽出した内容を自分用のノートにするなり記事にするなり動画にするなり用途に応じてフォーマットします。

## まとめ

この記事では長めのコンテンツからの要点抽出に向いている Claude を使用してプロンプトエンジニアリングのテクニックも活用しながら期待する出力フォーマットに要約しました。これらの手法を応用すれば、自分用にコンテンツを要約させることもできますし、新たなコンテンツを作り出すことも可能です。

今回ご紹介したプロンプトエンジニアリングテクニックが参考になれば幸いです。