AIによるラジオを作ってみた

## AIによるラジオ作ってみた

 ![airadio-image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894554/337596ba-3e84-2de4-655a-086fe749bdce.jpeg)

AIってたまに突飛な発想しますよね。聞いてるのはそういうことじゃないんだよなあって感覚と、そのズレがおもしろく思えたりもします。

もしAI同士で会話させたら、どれほどの会話の内容になるのか気になったため、今回はAI同士で会話をさせるラジオを作成してみました！

その名も「田中と中田のAIラジオ」
 
「この番組では、AIパーソナリティ田中と中田が、リスナーの皆さんから寄せられたお便りやお題に全力でお応えします。ふつおた、大喜利、AIに言わせたいことなど、内容はすべてリスナーとAIが共同で作り上げるインタラクティブなラジオ体験です。」

この紹介文もAIに書いてもらいました。

## プログラム

ChatGPTのAPI、Python、VOICEVOX、Premire Pro、fireflyで作成しました。

まずメールの内容をスプレッドシートに書き出し微調整したのち、Pythonでスプレッドシートを読み込みました。NGワードをひたすら書き出していって、変なものを読み込まないようにしました。作っていておもしろかったです。

書き出すシステムはこちらを参考にさせていただきました。
↓
https://note.com/omame727/n/neeea81533906

![スクリーンショット 2024-11-18 15.17.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894554/87c51941-132e-8869-d667-d0371b85ff60.png)
（↑読み込むスプレッドシート）

PythonではChatGPTのAPIを会話のターンごとに呼び出して、台本を作成しました。
こうすることでAIの発言に対して、AIが考えて発言する仕組みを実現しました。
ただしオープニングや、エンディング、コーナーの頭の方は伝えないといけない情報があるため、文章をある程度作成しましたが、その後のやり取りは完全にAIによって作成されました。

台本ができたら、ずんだもんで有名なVOICEVOXで音声を作成しました。
音声ファイルを扱うのに、pydubのモジュールを使いました。

最終的に以下の二つのファイルを出力するようにしています。
・音声ファイル(output.wav)
・本文にタイトルと要約をつけたテキストファイル(content.txt)



 Pythonのコードはこちら。

``` python
import requests
import os
import random
from openai import OpenAI
#import sounddevice as sd
#import soundfile as sf
import gspread
from pydub import AudioSegment
from pydub.playback import play

def get_sheet():
    """ 
    スプレッドシートを取得する関数 
    
    """
    # 認証してGoogleスプレッドシートにアクセス
    gc = gspread.service_account(filename="./.config/airadio.json")
    # スプレッドシートを開く (スプレッドシートのURLの最後にあるIDを使用)
    sh = gc.open_by_key(os.getenv("SPREADSHEET_URL"))
    #シートを取得
    ws = sh.worksheet("data")
    return ws

def make_ngwords_list():
    """ 
    NGリストを作成 
    
    """
    ng_words = ['事故','死亡','骨折','重傷','殺害','傷害','暴力','被害者','放送事故',\
            'ポルノ','アダルト','セックス','バイブレーター','マスターベーション','オナニー','スケベ','羞恥','セクロス',\
            'エッチ','SEX','風俗','童貞','ペニス','巨乳','ロリ','触手','羞恥','ノーブラ',\
            '大麻','麻薬',\
            '基地外','糞','死ね','殺す','しね','ころす',\
            'shit','piss','fuck','cunt','cocksucker','motherfucker','tits',\
            'R18','R-18','例のアレ','真夏の夜の淫夢','アヘ顔','亀頭','へんたい','ヘンタイ','変態','パンチラ',\
            '♂','アッー','アナル','アヘ顔','イマラ','淫','運営のお気に入り','エッチ','MKT','おっぱい','オッパイ','オナシャス','ガチムチ',\
            '姦','元祖羞恥心','亀頭','KMR','糞','クルルァ','グロ','ゲイ','ケツ','殺','シコ','自分を売る',\
            '18禁','春画','処女','ショタ','パイパン','フェラ','ふたなり','ペニス','へんたい','ヘンタイ','変態','ホモ',\
            'マラ','まんこ','マンコ','野獣','幼女','ょぅ','レイプ','レズ','ろり','ロリ','セックス','せっくす',\
            'レスリングシリーズ','来いよアグネス','紳士','運営仕事しろ','例のプール',\
        ]
    return ng_words

def check_ngwords(words_list, ng_words):
    """
    リストからNGリストに載ってる単語を取り除く。

    """
    filtered_array = [
    row for row in words_list if not any(
        any(ng_word in item for item in row) for ng_word in ng_words
    )
]
    return filtered_array

def pick_up_mail(ws,pick_up_num,words_list):
    """ 
    リストからランダムにpick_up_um個要素を抽出し、
    何を抽出したかスプレッドシートに記録する。
    
    """
    not_used_list=[x for x in words_list if str(x[4])=="0"]

    if len(not_used_list) >= pick_up_num:
        choices = random.sample(not_used_list,pick_up_num)
    else:
        return []

    #スプレッドシート更新
    for c in choices:
        row_num = int(c[0])+1
        ws.update_acell(f'E{row_num}',"1")

    return choices

def get_reply(client,content):
    """
    contentの中身をChatGPTに質問する。

    """
    res= client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role":"user",
            "content":(content)
        }]
    )
    reply = res.choices[0].message.content
    return reply

def input_to_male(content):
    """
    田中に投げかける質問文を生成する。

    """
    content = str(content)
    text = "あなたは田中という男の芸人だ。\
        友達の中田と、簡単な会話をしている。\
        次の中田の言葉に対して、文脈に即した上で、短文でできる限り馬鹿馬鹿しい返事をして。また冒頭に挨拶をする必要はありません。\n"
    return text + content

def input_to_female(content):
    """
    中田に投げかける質問文を生成する。
    
    """
    content = str(content)
    text = "あなたは中村という、常に冷静な女性だ。\
        友達の田村と、簡単な会話をしています。\
        次の田村の言葉に対して、文脈に即した上で、短文でできる限り現実的な返事をして。また冒頭に挨拶をする必要はありません。\n"
    return text + content

def man_talks(client,content):
    """
    田中に質問文を投げかけて、回答を生成する。
    
    """
    txt = input_to_male(content)
    reply = get_reply(client,txt)
    return reply

def female_talks(client,content):
    """
    中田に質問文を投げかけて、回答を生成する。
    
    """
    txt = input_to_female(content)
    reply = get_reply(client,txt)
    return reply


def talk_with_each_other(client,first_content,repeat_time=2):
    """
    田中と中田の会話のやりとりをrepeat_time分、生成する。
    
    """
    conv_list = []
    content = first_content
    i = 0
    while i < repeat_time:
        content = man_talks(client,content)
        conv_list.append(content)
        content = female_talks(client,content)
        conv_list.append(content)
        i += 1
    return conv_list

def is_charactor_num(num):
    """
    voicevoxに渡すキャラクター番号を、入力に応じて返す。
    """
    if num%2: #もし2で割り切れるなら、女性の声を当てる。
        return 9
    else: #もし2で割り切れないなら、男性の声を当てる。
        return 39



    
def get_audio_filepath(reply, charactor_num):
    """
    textをvoicevoxに読み上げてもらう。音声ファイルをwavで保存。
    
    """
    params = {"text":reply, "speaker":charactor_num}
    res = requests.post(
        f'http://localhost:50021/audio_query',
        params=params
    )
    res = requests.post(
        f'http://localhost:50021/synthesis',
        params=params,
        json = res.json()
    )
    voice = res.content
    file_path = f'audio/airadio-voice.wav'
    with open(file_path, "wb") as f:
        f.write(voice)
    return file_path

    #音声ファイルを再生
    #data,fs = sf.read(file_path)
    #sd.play(data,fs)
    #sd.wait()

def get_audio_file(file_path):
    """
    wavファイルを呼び出して、AudioSegmentに変換。
    
    """
    #音声ファイルをAudioSegmentで出力
    audio_file = AudioSegment.from_file(file_path)
    return audio_file

def get_audio_reply(reply, charactor_num):
    """
    voicevoxに読み上げてもらう音声をAudioSegmentに変換。
    
    """
    filepath = get_audio_filepath(reply, charactor_num)
    audio_reply = get_audio_file(filepath)
    return audio_reply

def make_them_speak(client,conv_list):
    """
    二人の会話のやりとりを交互に読み上げてもらう。
    
    """
    combined_audio_file = AudioSegment.empty()
    for i, conv in enumerate(conv_list):
        combined_audio_file += get_audio_reply(conv,is_charactor_num(i))
    return combined_audio_file

def get_mail_content(content_list):   
    """
    メールの内容を取得する。
    
    """
    radio_names = list(map(lambda x:x[5], content_list))
    for i in range(len(radio_names)):
        if radio_names[i].strip(" ") == "":
            radio_names[i] = "なし"
        else:
            radio_names[i] += "さん"
    odais = list(map(lambda x:x[6], content_list))
    return radio_names,odais
    
def oogiri(client,ws,oogiri_list):
    """
    大喜利のコーナーの文章を生成する。
    
    """
    pick_up_list = pick_up_mail(ws,2,oogiri_list)

    if len(pick_up_list) < 2:
        return []

    radio_names, odais = get_mail_content(pick_up_list)
    answers = []
    for odai in odais:
        answer = get_reply(client,f'あなたは芸人です。次のお題に対して大喜利してください。\n{odai}')
        answers.append(answer) 

    contents1 = ["大喜利ー！","えーと、このコーナーは、リスナーから募集したお題で大喜利をしていくコーナーです。",\
                "今週のお題はこちら！",\
                f'ラジオネーム{radio_names[0]}からいただきました。{odais[0]}', f'{answers[0]}']
    reply10 = female_talks(client,answer[0])
    reply11 = talk_with_each_other(client,reply10,repeat_time=2)
    contents2 = ["",f'えー続いて、ラジオネーム{radio_names[1]}からいただきました。{odais[1]}',\
                f'{answers[1]}']
    reply20 = female_talks(client,answer[1])
    reply21 = talk_with_each_other(client,reply20,repeat_time=2)

    return contents1 + [reply10] + reply11 + contents2 + [reply20] + reply21 


def iwasetaikoto(client,ws,iwasetaikoto_list):
    """
    AIに言わせたいことのコーナーの文章を生成する。
    
    """
    pick_up_list = pick_up_mail(ws,2,iwasetaikoto_list)

    if len(pick_up_list) < 2:
        return []
    
    radio_names, odais = get_mail_content(pick_up_list)

    contents1 = ["AIに言わせたいこと！","えーと、このコーナーは、我々AIパーソナリティーに読み上げてほしい文章を募集するコーナーです。",\
                "今週のお題はこちら！",\
                f'ラジオネーム{radio_names[0]}からいただきました。', f'{odais[0]}']
    reply10 = female_talks(client,odais[0])
    reply11 = talk_with_each_other(client,reply10,repeat_time=2)
    contents2 = ["",f'えーと、続いて、ラジオネーム{radio_names[1]}からいただきました。',\
                f'{odais[1]}']
    reply20 = female_talks(client,odais[1])
    reply21 = talk_with_each_other(client,reply20,repeat_time=1)

    return contents1 + [reply10] + reply11 + contents2 + [reply20] + reply21


def hutsuota(ws,hutsuota_list):
    """
    ふつおたのコーナーの文章を生成する。
    
    """
    pick_up_list = pick_up_mail(ws,1,hutsuota_list)
    if len(pick_up_list) == 0:
        return ""
    radio_name, odai = get_mail_content(pick_up_list)
    content = f'ふつおたが届いています。ラジオネーム、{radio_name[0]}からいただきました。\n{odai[0]}'
    return content

def get_title(client,content):
    """
    タイトルと要約を取得。
    
    """
    text = f'## 依頼\n次の文章を要約して、人を惹きつけるタイトルをつけてください。\
    \n## アウトプットのフォーマット\nタイトル：\n要約：\
    \n## 文章\n{content}'
    return get_reply(client,text)

def main():
    print("処理を開始します。")
    client = OpenAI()
    ws = get_sheet()
    mail_data = ws.get_all_values()
    ng_words = make_ngwords_list()

    #ふつおたがあれば最初に読む。最初のトークの内容を生成。
    hutsuota_list=[x for x in mail_data if "ふつおた" in x[2]]
    hutsuota_list = check_ngwords(hutsuota_list, ng_words)
    first_content = ""

    if len(hutsuota_list) != 0:
        first_content = hutsuota(ws,hutsuota_list)

    if first_content == "":
        topic_list = ["何か面白い話してよ。","最近どう？",
                      "最近見た夢あれば教えて。","最近ハマってるものある？","最近どこか行った？",
                      "最近何か食べた？","最近欲しいものある？"]
        first_content = random.sample(topic_list,1)
        
    print("ふつおたの処理完了")

    #大喜利コーナー
    oogiri_list=[x for x in mail_data if "大喜利" in x[2]]
    oogiri_list = check_ngwords(oogiri_list,ng_words)
    oogiri_conv = []
    if len(oogiri_list) >= 2: #大喜利のメールが二件以上あれば大喜利のコーナー実施。
        oogiri_conv = oogiri(client,ws,oogiri_list)
    print("大喜利の処理完了")

    #AIに言わせたいことのコーナー
    iwasetaikoto_list=[x for x in mail_data if ("言わせたい" in x[2])or("いわせたい" in x[2])]
    iwasetaikoto_list = check_ngwords(iwasetaikoto_list, ng_words)
    iwasetaikoto_conv = []
    if len(iwasetaikoto_list) >= 2: #AIに言わせたいことのメールが二件以上あれば大喜利のコーナー実施。
        iwasetaikoto_conv = iwasetaikoto(client,ws,iwasetaikoto_list)
    print("言わせたいことの処理完了")

    #オープニングのコーナーを生成。
    opening = ["こんにちは！AIの田中です。","こんにちはAIの中田です。",\
                "この番組はAIパーソナリティの田中と中田でお送りいたします。",\
                "内容はすべてリスナーの皆様とAIによって作られています。",\
                "よろしくお願いします。オープニング行きます、AIラジオ"]
    
    #エンディングのコーナーを生成。
    ending  =  ["AIラジオ、そろそろお別れのお時間です。","大喜利のお題やAIに言わせたいこと、ふつおたのメールお待ちしております。",\
               "件名にコーナー名を書いて送ってください。","宛先はTANAKATA、ドット、AIRADIO、アット、GMAIL、ドット、COM",
               "たなかた、ドット、AIラジオアットジーメールコムとお覚えください。","感想は、ハッシュタグ田中と中田のAIラジオをつけてポストしてください。"\
               "お願いしますー。","以上、ここまでのお相手は中田と","田中でした！今週も聞いてくれてありがとう！","来週もお楽しみに！","さよならー！"]
    
    #フリートーク（ふつおた）のコーナーを生成。
    conv_list = talk_with_each_other(client,first_content,repeat_time=6) 
    #conv_list = ['「うん、ウサギの耳を持つスパゲッティマシーンが欲しいんだ！」', '「それはユニークなアイデアね。どんな用途を考えているの？」', '「うん、カメラの代わりにスイカを使って、スイカの中から風景を撮ろうと思ってるんだ！」', '「面白いアイデアね、その発想でどんな風景が撮れるか楽しみ。」']
    print("フリートークの内容生成完了")

    #要約文・タイトルを生成。
    subtitle = opening + [first_content] + conv_list + oogiri_conv + iwasetaikoto_conv + ending 
    title = get_title(client,subtitle)
    text = title + "\n-----以下本文-----\n" + str(subtitle)
    print("----------------------")
    print(text)
    print("----------------------")
    print("全テキストの生成完了")

    #BGMの収録
    audio1 = get_audio_file("airadio-bgm.mp3").fade_out(1000)
    audio2 = get_audio_file("airadio-corner-bgm.mp3").fade_out(1000) - 10
    print("BGMの処理完了")


    #オープニングの収録
    opening_audio = make_them_speak(client,opening) + audio1
    print("オープニングの収録終了")

    #フリートークの収録
    free_talk_audio1 = get_audio_reply(first_content, 9)
    free_talk_audio2 = make_them_speak(client,conv_list)
    free_talk_audio = free_talk_audio1 + free_talk_audio2
    play(free_talk_audio)
    print("フリートークの収録終了")

    #大喜利の収録
    if len(oogiri_conv) >= 2:
        oogiri_audio = audio2 + make_them_speak(client,oogiri_conv).fade_out(1000)
    else:
        oogiri_audio = AudioSegment.empty()
    print("大喜利の収録終了")

    #我々に言わせたいことの収録
    if len(iwasetaikoto_conv) >= 2:
        iwasetaikoto_audio = audio2 + make_them_speak(client,iwasetaikoto_conv).fade_out(1000)
    else:
        iwasetaikoto_audio = AudioSegment.empty()
    print("言わせたいことの収録終了")

    #エンディングの収録
    ending_audio = audio1 + make_them_speak(client,ending)
    print("エンディングの収録終了")

    #音声ファイル処理
    file_title = title.split("\n")[0]
    file_title = file_title[5:]
    body = opening_audio + free_talk_audio + oogiri_audio + iwasetaikoto_audio + ending_audio
    play(body)
    body.export(f'output/{file_title}.wav', format="wav")

    #テキストファイルの処理
    with open(f'output/{file_title}.txt', "wb") as f:
        f.write(text.encode("utf-8"))
    print(f'全処理の完了。{file_title}.wav, {file_title}.txtを確認してください。')
   
if __name__=="__main__":
    main()
```
 
また、Githubにも載せてあります。
https://github.com/KANEI/AIradio

音声ファイルとテキストファイルを元に、fireflyでイメージ画像を再生して、Premire Proを使って動画を作成しました。

## 配信はこちら
#### Podcast
https://open.spotify.com/show/7qb7XGl5niKZQ4xXp5SXlo?si=e423c2982dae4695
#### Youtube
https://www.youtube.com/@AIradio-tanakata/featured

## 感想
2回ほど作成してみましたが、内容がぶっとんでいてやはりおもしろかったです。
chatGPTのAPIを使用するいい練習になった気がします。
気になった方、おもしろいのでぜひ聴いてみてください！！

## 参照
・AITuberを作ってみよう！サプーが引退の危機？！〜 Pythonを使って自動で喋るVTuber 〜
　https://youtu.be/WVpvExDPgOE?si=FhazmJR_3fAp7crc
 
・【GAS】Gmailの内容をスプレッドシートに自動転記する方法
　https://note.com/omame727/n/neeea81533906

・NGワードを考えるのが面倒な人向けにサンプル集を作りました
　https://qiita.com/hose/items/774ac58f0155ef0a9973

・Python pydubで音声ファイルを操作しよう: 基本的な読み込みと保存
　https://qiita.com/Tadataka_Takahashi/items/1dd564802932d4a81b04