1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【OpenAI x Amazon Polly】漫才ネタと掛け合い音声を自動生成してみた

Last updated at Posted at 2025-06-12

はじめに

諸般の事情により、AWSを使ってデモ発表することになりました。その中で「Amazon Pollyを使って何か面白いことができないか?」と考え、「AIが考えた漫才ネタを機械音声で実演させてみよう!」というアイデアにたどり着きました。

本記事では、ChatGPTで漫才台本を自動生成し、それをAWS LambdaからAmazon Pollyで音声化し、S3に保存するまでの一連のフローについて紹介します。

この記事でわかること

  • (1)AWS→OpenAI API(ChatGPT)との連携
  • (2)Amazon Pollyで2人のキャラクターによる漫才の掛け合い音声を生成

工夫したポイント

Amazon Pollyで音声を生成する際に、2種類の音声を交互に喋らせ、コンビ漫才をしているかのようにしたことです。

というのも、Amazon Pollyは、本来1人分の音声を生成するサービスです。
日本語だと標準2種類(ニューラルだと4種類)の話者が選べますが、1つのテキストで複数話者を喋らせることはできません。

image.png

そこで、ChatGPTに「芸人A」「芸人B」と話者を明記した台本を作らせ、セリフごとにPollyで声種を切り替えて音声を生成・合成することで、漫才の掛け合いを再現しています。

処理の流れはざっくり以下のとおり:

(1)AWS→OpenAI API(ChatGPT)との連携
 1.ChatGPTで漫才ネタの台本を自動生成する際に、セリフの先頭に「芸人A」/「芸人B」を付与するよう指示

(2)Amazon Pollyで2人のキャラクターによる漫才の掛け合い音声を生成
 2.台本を行ごとに分割し、それぞれのセリフが「芸人A」か「芸人B」かを判定
 3.話者が「芸人A」ならMizukiの音声、「芸人B」ならTakumiの音声を使用
 4.セリフから話者名(例:「芸人A:」)を除き、音声を生成し順番に配列へ追加
 5.最後に、すべての音声データを結合し、1つの音声ファイルとして出力

このようにして、「芸人A → 芸人B → 芸人A → ...」と交互に再生される掛け合い音声が完成します。

では、具体的な手順を見ていきましょう。

(1) AWS→OpenAI API(ChatGPT)との連携

事前準備1 APIキー準備

今回はAWSからOpenAI APIと連携するため、ChatGPT(gpt-3.5-turbo)を利用します。
あらかじめOpenAIの公式サイトでAPIアカウントを作成し、APIキーを取得しておきましょう。
ChatGPT OpenAI公式サイト

※ChatGPTのOpenAI APIは重量課金となりますので、使い過ぎに注意してください。

なお、今回は手早く構築するため、取得したAPIキーはLambdaの環境変数に設定しています。

事前準備2 ネタ台本保管用S3

ChatGPTで生成した漫才ネタはテキストファイルとしてS3に保存します。
そのため、事前にS3バケットを作成し、バケット名もLambdaの環境変数に設定しておきましょう。

image.png

事前準備が済んだら、いよいよコーディングです。

lambda関数でChat GPTを呼び出し、漫才ネタを作ってみる

まずはChatGPTに渡すプロンプト(指示文)を決めておきます。今回、以下のように指定しました。

  • ネタの長さは1分程度で披露できる内容にする
  • 二人組の漫才師(芸人A、芸人B)の台本形式で、各セリフの冒頭に発言者名を明記する

プログラム作成

実際のコードはこうなります。
(Javaで作成しました)

MANZAI.js
//初期設定
const AWS = require('aws-sdk');  // AWSサービス操作用
const axios = require('axios');  // HTTP通信(OpenAI API呼び出し用)

const s3 = new AWS.S3();        // S3クライントインスタンス


// 環境変数から取得
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;   // ChatGPTのAPIキー
const NETA_BUCKET_NAME = process.env.NETA_BUCKET_NAME; // ネタ台本保管用バケット

exports.handler = async (event) => {
    try {
        // リクエストのボディからお題を取得
        const body = JSON.parse(event.body);
        const { topic } = body;

        // OpenAIにリクエストして漫才ネタを生成
        // - Systemロール、Userロールで指示
        const openAiPayload = {
            model: 'gpt-3.5-turbo',
            messages: [
                { role: 'system', content: 'あなたはお笑いのプロです。' },
                { role: 'user', content: 次のお題で1分の漫才ネタを考えてください: ${topic} },
            ],
            max_tokens: 500,
        };
        
        // OpenAI APIへHTTP POSTでリクエストを送信
        const response = await axios.post(
            'https://api.openai.com/v1/chat/completions',
            openAiPayload,
            {
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': Bearer ${OPENAI_API_KEY},   // 認証トークン(Bearer接頭辞必須)
                },
            }
        );

        // ChatGPTが生成した台本テキストを取得
        const generatedText = response.data.choices[0].message.content;
        console.log('生成されたネタ:', generatedText);

        // 生成したネタ台本をテキストファイルとしてS3に保存
        const textFileName = comedy-${Date.now()}.txt;
        await s3.putObject({
            Bucket: NETA_BUCKET_NAME,
            Key: text/${textFileName},
            Body: generatedText,
            ContentType: 'text/plain',
        }).promise();
        console.log(ネタをS3に保存しました: text/${textFileName});

        // 台本の各行をセリフ単位で分割(改行区切り・空白除外)
        const lines = generatedText.split('\n').filter(line => line.trim() !== '');

        // それぞれの発話者ごとにセリフを整理(後続のPolly音声合成等に利用)
        const dialogue = { comedianA: [], comedianB: [] };
        for (const line of lines) {
            if (line.startsWith('芸人A:')) {
                dialogue.comedianA.push(line.replace('芸人A: ', '').trim());
            } else if (line.startsWith('芸人B:')) {
                dialogue.comedianB.push(line.replace('芸人B: ', '').trim());
            }
        }

lambda実行

作成したLambda関数をデプロイしたら、テスト実行で動作を確認します。
漫才ネタのお題は、LambdaコンソールのイベントJSONに設定します。

test
{
  "body": "{\"topic\": \"ここにお題を設定\"}"
}

 
image.png

これで実行すると、設定したお題をもとに漫才ネタが自動生成され、S3バケットに台本テキストファイルが保存されるはずです。

image.png

AIが考えた漫才ネタが面白いかどうかは、ぜひ実際に試してみてください。

(2)Amazon Pollyで2人のキャラクターによる漫才の掛け合い音声を生成

次に、ChatGPTで作成した漫才ネタをAmazon Pollyを使って実際に喋らせてみます。

事前準備1 音声ファイル保管用S3

Amazon Pollyで作成した音声は、mp3ファイルとしてS3に保存します。
あらかじめS3バケットを作成し、バケット名をLambdaの環境変数に設定しておきましょう。

image.png

機械音声で漫才を実演させてみる

せっかくなので、2人の異なる声で掛け合いを再現し、より本物の漫才に近づけてみます。
Amazon Pollyの日本語音声から「Takumi(男性音声)」と「Mizuki(女性音声)」を使用します。
それぞれの音声でセリフを生成し、最後に1つの音声ファイルとして合成します。
(★ここがポイント!:2つの声を交互に使うことで雰囲気がかなりリアルになります)

先ほど作成した漫才台本生成用Lambdaに、音声生成・合成機能をアドオンして実装します。

まず、初期設定や環境変数まわりに下記内容を追加します。

MANZAI_ver2.js
//初期設定
const AWS = require('aws-sdk');
const axios = require('axios');

const s3 = new AWS.S3();
// ★追加★
const polly = new AWS.Polly();  // Amazon Polly クライアント設定

// 環境変数から取得
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;   // ChatGPTのAPIキー
const NETA_BUCKET_NAME = process.env.NETA_BUCKET_NAME; // ネタ台本保管用バケット
// ★追加★
const POLLY_BUCKET_NAME = process.env.POLLY_BUCKET_NAME; // 音声保管用バケット

次に、漫才ネタ作成の処理の続きにPollyでの音声合成処理を追記します。
(エラーハンドリングも追加しています)

MANZAI_ver2.js

        // Pollyで音声を生成
        // 2人のキャラクターごとに VoiceId(Mizuki/Takumi)を割り当て
        const voices = { comedianA: 'Mizuki', comedianB: 'Takumi' }; // 日本語の女性と男性の声
        const audioStreams = [];

        for (const line of lines) {
        // セリフの先頭で話者を判定し、VoiceIdを動的に切り替え
        // こうすることで「AとBが交互に喋る漫才」を再現
            const speaker = line.startsWith('芸人A:') ? 'comedianA' : 'comedianB';
            const params = {
                // '芸人A:'/'芸人B:' という話者表記をセリフから除いて音声生成
                Text: line.replace(/芸人[AB]: /, ''),
                VoiceId: voices[speaker],
                OutputFormat: 'mp3',
            };
            // Pollyで音声データ(mp3)を生成し、ストリームとして格納
            const response = await polly.synthesizeSpeech(params).promise();
            audioStreams.push(response.AudioStream);
        }

        // すべてのセリフの音声ストリームを順番に結合
        const combinedAudio = Buffer.concat(audioStreams);

        // 合成した音声データをmp3ファイルとしてS3に保存
        const audioFileName = textFileName.replace('.txt', '.mp3');
        await s3.putObject({
            Bucket: POLLY_BUCKET_NAME,
            Key: audio/${audioFileName},
            Body: combinedAudio,
            ContentType: 'audio/mpeg',
        }).promise();
        console.log(音声ファイルをS3に保存しました: audio/${audioFileName});

        // 正常終了レスポンスを返却
        return {
            statusCode: 200,
            body: JSON.stringify({
                message: 'ネタ生成と音声作成成功',
                topic: topic,
                textFile: text/${textFileName},
                audioFile: audio/${audioFileName},
            }),
        };
    } catch (error) {
        // エラーレスポンス&エラーログ
        console.error('エラーが発生しました:', error);

        return {
            statusCode: 500,
            body: JSON.stringify({
                message: '処理中にエラーが発生しました。',
                error: error.message,
            }),
        };
    }
};

これで、指定したS3バケットに漫才の掛け合い音声(mp3)が作成されるはずです。

Qiita1.jpg

おわりに

発表の場でお題をもらいデモ実演することで、ライブ感を演出でき、本番はとても盛り上がりました。
OpenAIでコンテンツを生み出し、Pollyで“声”にするという流れは、まさに生成AIの実用例として面白い体験でした。

今回は、生成AIと音声合成、そしてAWSのサーバーレス技術を組み合わせることで、「ネタを作って喋らせる」という一連の流れを自動化できました。
真面目な用途にも転用できそうですが、こうした遊び心のあるユースケースこそ、技術に対する理解や応用力を育てるチャンスだと感じています。
もしこの記事が面白いと思ったら、ぜひ試してみてください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?