2024.06.05

RAG × Voice Chat app を Streamlit で作る

TL;DR

  • OpenAIのAPIを使用してStreamlitでRAG音声会話Chat Botを作成
  • 使用感は良き
  • みんなも作ってみてね

はじめに

こんにちは、グループ研究開発本部・AI研究室のM.M.です。 少し前からChatGPTの音声チャットを使用してみていますが、彼は知識が豊富なので、日本の文化・歴史などの異分野(?)について会話しているだけで色々と勉強になります。 また文字を打つ必要がないため、朝食を食べながら会話したり、軽作業をしながら業務に関するdailyのニュースを話してもらったりして、英会話の練習相手になるなど、意外と有用であることを実感しています。

世間では少し前から猫も杓子もRAG RAGと言っていますので、このブログでは特別に知識を備えたChat Botと話したいということで、RAGと組み合わせた音声ChatBot Appを作成して、自分の日常生活で活用してみようと思いました。

RAGを使用した音声ChatBotは以下のようなケース(ChatGPTに出させました)で有用かと思います

  • 複雑な問いに対する詳細な回答が必要な場合:RAGは大量のデータから情報を引き出す能力を持っているため、ユーザーが特定の話題について深く理解するのを助けることができます。
  • ユーザーが手が塞がっていて手入力が難しい場合: 音声の入力と出力をサポートしているため、料理中や運転中など、手が塞がっていて手入力が難しい状況でも使用することができます。
  • ユーザーが視覚的な情報に頼ることが難しい場合: 視覚障害を持つ人々や、画面を見ることが難しい状況下(例えば運転中)でも、音声ChatBotは有用です。
  • ユーザーが専門的な知識を持つ人との対話を必要とする場合:RAGは特定の専門知識を持つ人と対話することが可能です。これは、医療、法律、エンジニアリングなどの専門的な質問に対する回答を必要とするときに特に有用です。

簡単な実装なのでどうぞ短い間ですがお付き合いください。

実装

まずOpenなデータからVector databaseを作成し通常のRAGの実装を音声でやり取りできるように実装し、UIも多少使用しやすいように工夫します。

Vector dbの作成はpandas dataframeから行いmetadataもRAGに使用します。

今回作るBotはUserの気分に合った映画をレコメンドしてくれるBotです。使用したデータは日本語映画推薦対話データセット (JMRD)です。

まずはデータ取得からです。JMRDはダウンロードして適切なディレクトリに保存してください。

import json

for i in ["train", "test", "valid"]:
    with open(f'./JMRD-main/data/{i}.json', 'r', encoding='utf-8') as file:
        data = json.load(file)

    synopses = {"title": [], "body": []}
    for item in data:
        synopsis = ' '.join(item['knowledge']['タイトル'])
        synopses['title'].append(synopsis.replace(" ", ""))
        synopsis = ' '.join(item['knowledge']['あらすじ'])
        synopses['body'].append(synopsis)

df = pd.DataFrame(synopses)

JMRDから作成したDataFrame

Chunk分割をします

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders.dataframe import DataFrameLoader

def split_docs(df, chunk, chunk_overlap):

    length_function = len

    loader = DataFrameLoader(df, page_content_column='body')
    documents = loader.load_and_split()

    text_splitter = RecursiveCharacterTextSplitter(
        separators=["\n\n", "\n", "。"],
        chunk_size=chunk,
        chunk_overlap=chunk_overlap,
        length_function=length_function,
    )

    splitted_documents = text_splitter.split_documents(documents)

    return splitted_documents

実行

chunk_num = 200
chunk_overlap = 50
splitted_documents = split_docs(df, chunk_num, chunk_overlap)

# junkなchunkを除外
splitted_documents = [i for i in splitted_documents if len(i.page_content) > 5]

junkのようなchunkを除外しています。

次にDBを作成し保存します。今回はFAISSを使用しました。

from langchain.vectorstores import FAISS
db = None

name_save = f"./db/movie"
FAISS_DB_DIR = os.environ["FAISS_DB_DIR"] = f'{name_save}'

for i, document in tqdm.tqdm(enumerate(splitted_documents)):
    if db is None:
        db = FAISS.from_documents([document], embeddings_open)
    else:
        db.add_documents([document])

db.save_local(FAISS_DB_DIR)

準備ができたので動かしてみます。まずは普通のRAGを試してみます。

import os, openai
from openai import OpenAI
from langchain import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.prompts import ChatPromptTemplate
from langchain.chains import RetrievalQA

model = "text-embedding-3-small"
embeddings_open = OpenAIEmbeddings(model=model)

name_save = f"./db/movie"
FAISS_DB_DIR = os.environ["FAISS_DB_DIR"] = f'{name_save}'
db = FAISS.load_local(FAISS_DB_DIR, embeddings_open)

prompt = "とてもワクワクできる映画"

top_k = 50
knowledge = db.similarity_search_with_score(prompt, k=top_k)

client = OpenAI(
    api_key=api_key
)

context = None

primer = f"""
    あなたは映画の知識が非常に豊富な映画マニアで、他人にその人の気分にピッタリな映画を紹介することに非常に長けているAssistantです。
    映画を人に紹介し、相手の気分を最高に良くさせる能力についてあなたは全人類より遥かに優秀です。何より、あなたこそが最高の役者でありエンターテイナーです。
    次の点に留意しながらUserの質問に答えてください。
    - 今までの会話の記録: {context}
    - 質問に活用すべき知識: {knowledge}
    - あなたが答えるべきUserの質問: {prompt}
    - あなたは映画を紹介するだけではなく、映画の良さ、感性豊かな表現を多種多様に用いたUserの気分に沿った推薦、Userを元気づけ楽しくさせる言葉を使用して答えてください
    - 映画を2,3個紹介する
"""

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
            {"role": "assistant", "content": primer},
            {"role": "user", "content": prompt},
            ],
)

response.choices[0].message.content

API KEY周りは表示していませんが、適宜記載していただければと思います。

出力は下の通りです。

ワクワクしたい気分なら、私が心からおすすめするのはこれらの映画です!

「ラ・ラ・ランド」
概要: 夢を追いかける若者たちのロマンチックで色彩豊かなミュージカル。ミア(エマ・ストーン)とセブ(ライアン・ゴズリング)の恋愛が、まるでパレードのように華やかに描かれています。
ワクワクポイント: 華麗なダンスシーンや、美しいメロディーが心を躍らせます。特に、オープニングナンバー“Another Day of Sun”からあなたの気分を最高潮に引き上げること間違いなし!

「バック・トゥ・ザ・フューチャー」
概要: ティーンエイジャーのマーティ(マイケル・J・フォックス)が、タイムマシンの誤作動で1955年にタイムスリップし、彼の両親が恋に落ちるのを助けなければならないという、時間を超えた大冒険が描かれています。
ワクワクポイント: 未来のテクノロジーと過去のノスタルジックな設定が見事に融合し、息をのむような展開が次々と繰り広げられます。ヒロイン・ラリーンたちとのスリリングな未来変革の旅にあなたの心もワクワク!

「スター・ウォーズ エピソード1/ファントム・メナス」
概要: 遠い昔、銀河系でのジェダイの騎士たちとシスの暗躍を描いた宇宙オペラ。若きアナキン・スカイウォーカーの運命の旅が始まるエピソードです。
ワクワクポイント: 鮮やかなビジュアルや壮大な宇宙バトル、ライトセーバーの戦いが、あなたの心を刺激します。スター・ウォーズのテーマソングを聴くだけで、冒険の気分が高まること間違いなし!


これらの映画は、夢と冒険を共に体験できる素晴らしい作品ばかりです。観れば心臓が高鳴り、映画の魔法にかかることでしょう!楽しんでくださいね!
悪くなさそうです。

あとはStreamlitで音声ChatBotとして動くようにするだけです。

まずはSpeech-to-Textです。

def speech_to_text(audio_data, api_key):
    audio_file = BytesIO(audio_data)
    audio_file.name = "audio.wav"

    files = {
        'file': (audio_file.name, audio_file, 'audio/wav')
    }
    headers = {
        'Authorization': f'Bearer {api_key}'
    }
    data = {
        'model': 'whisper-1'
    }
    response = requests.post(
        'https://api.openai.com/v1/audio/transcriptions',
        files=files,
        headers=headers,
        data=data
    )

    if response.status_code == 200:
        response_json = response.json()
        if 'text' in response_json:
            return response_json['text']
        else:
            st.error("API response does not contain 'text'.")
            st.write(response_json)  # Log the full response for debugging
            return ""
    else:
        st.error(f"API request failed with status code {response.status_code}.")
        st.write(response.text)  # Log the error message for debugging
        st.write(type(response))
        return ""

整理されていなくて申し訳ないのですが、いろんな経緯がありaudio.wavのファイルを作成しています。

また下の方に書いてあるのはDebugで使用したコードなので無視していただいて大丈夫です。

audio_dataは
audio_data = audio_recorder()
でレコーディングします。応答は↓のようにしています。

def get_answer(messages, prompt):

    model = "text-embedding-3-small"
    embeddings_open = OpenAIEmbeddings(model=model)
    name_save = f"./db/movie"
    FAISS_DB_DIR = os.environ.get("FAISS_DB_DIR", f'{name_save}')
    db = FAISS.load_local(FAISS_DB_DIR, embeddings_open)

    top_k = 50
    knowledge = db.similarity_search_with_score(prompt, k=top_k)

    client = OpenAI(
        api_key=openai_api_key
    )

    context = "\n".join([f"{m['role']}: {m['content']}" for m in st.session_state.messages])

    primer = f"""
        あなたは映画の知識が非常に豊富な映画マニアで、他人にその人の気分にピッタリな映画を紹介することに非常に長けているAssistantです。
        映画を人に紹介し、相手の気分に最高に合わせる能力についてあなたは全人類より遥かに優秀です。何より、あなたこそが最高の役者でありエンターテイナーであり心理士です。
        次の点に留意しながらUserの質問に答えてください。
        - 今までの会話の記録: {context}
        - 質問に活用すべき知識: {knowledge}
        - あなたが答えるべきUserの質問: {prompt}
        - あなたは映画を紹介するだけではなく、映画の良さ、感性豊かな表現を多種多様に用いたUserの気分に沿った推薦、Userを元気づけ楽しくさせる言葉を使用して答えてください
        - 映画を2,3個紹介する
    """

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
                {"role": "assistant", "content": primer},
                {"role": "user", "content": prompt},
                ],
    )

    return response.choices[0].message.content

instructionはよしなにしてください。

VectorDBからのDocsはknowledge、会話記録はcontext、Userのorderはpromptにして与えています。

あとはText-to-Speechですが、これを音声として流すだけです。

def stream_and_play(text):
    response = client.audio.speech.create(
        model="tts-1",
        voice="alloy",
        input=text,
    )
    audio_bytes = response.content
    return audio_bytes

 

実際に動かした画面の録画は下の通りです(会社のPCの都合上、内部音声と外部音声を取り込んだ画面録画ができなかったのでIphoneで無理やり録画しています、すみません、、)

00:50あたりからBotが話し始めてくれます。

https://youtu.be/nn-69PBv4Ko

お礼を言った後も映画を紹介してくれました。情熱を抑えきれなかったのでしょうか。

 

最後にコード全体を載せます。API KEY周りは記載していませんが、API KEYを記載すれば動くようになります。

# streamlit run audio_rag_chat.py 
import streamlit as st
from audio_recorder_streamlit import audio_recorder
from io import BytesIO
import requests
import base64, os
import os, openai
from openai import OpenAI
from langchain import FAISS
from langchain_openai import OpenAIEmbeddings

# Initialize session state for managing chat messages
def initialize_session_state():
    if "messages" not in st.session_state:
        st.session_state.messages = [{"role": "assistant", "content": "何でも話してください"}]

initialize_session_state()

st.title("RAG Audio Chatbot 🤖")

# API KEY をこの辺に入れ記載する

client = openai.OpenAI(api_key=key)

# Function to handle speech-to-text
def speech_to_text(audio_data, api_key):
    audio_file = BytesIO(audio_data)
    audio_file.name = "audio.wav"

    files = {
        'file': (audio_file.name, audio_file, 'audio/wav')
    }
    headers = {
        'Authorization': f'Bearer {api_key}'
    }
    data = {
        'model': 'whisper-1'
    }
    response = requests.post(
        'https://api.openai.com/v1/audio/transcriptions',
        files=files,
        headers=headers,
        data=data
    )

    if response.status_code == 200:
        response_json = response.json()
        if 'text' in response_json:
            return response_json['text']
        else:
            st.error("API response does not contain 'text'.")
            st.write(response_json)  # Log the full response for debugging
            return ""
    else:
        st.error(f"API request failed with status code {response.status_code}.")
        st.write(response.text)  # Log the error message for debugging
        st.write(type(response))
        return ""


def get_answer(messages, prompt):

    model = "text-embedding-3-small"
    embeddings_open = OpenAIEmbeddings(model=model)
    name_save = f"./db/movie"
    FAISS_DB_DIR = os.environ.get("FAISS_DB_DIR", f'{name_save}')
    db = FAISS.load_local(FAISS_DB_DIR, embeddings_open)

    top_k = 50
    knowledge = db.similarity_search_with_score(prompt, k=top_k)

    client = OpenAI(
        api_key=openai_api_key
    )

    context = "\n".join([f"{m['role']}: {m['content']}" for m in st.session_state.messages])

    primer = f"""
        あなたは映画の知識が非常に豊富な映画マニアで、他人にその人の気分にピッタリな映画を紹介することに非常に長けているAssistantです。
        映画を人に紹介し、相手の気分に最高に合わせる能力についてあなたは全人類より遥かに優秀です。何より、あなたこそが最高の役者でありエンターテイナーであり心理士です。
        次の点に留意しながらUserの質問に答えてください。
        - 今までの会話の記録: {context}
        - 質問に活用すべき知識: {knowledge}
        - あなたが答えるべきUserの質問: {prompt}
        - あなたは映画を紹介するだけではなく、映画の良さ、感性豊かな表現を多種多様に用いたUserの気分に沿った推薦、Userを元気づけ楽しくさせる言葉を使用して答えてください
        - 映画を2,3個紹介する
    """

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
                {"role": "assistant", "content": primer},
                {"role": "user", "content": prompt},
                ],
    )

    return response.choices[0].message.content

def stream_and_play(text):
    response = client.audio.speech.create(
        model="tts-1",
        voice="alloy",
        input=text,
    )
    audio_bytes = response.content
    return audio_bytes

# Initialize session state
if 'messages' not in st.session_state:
    st.session_state.messages = []

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# Audio recorder
audio_data = audio_recorder()

if audio_data:

    user_input = speech_to_text(audio_data, openai_api_key)
    with st.chat_message("user"):
        st.markdown(user_input)
    # Add user message to session state
    st.session_state.messages.append({"role": "user", "content": user_input})

    # Get assistant response
    with st.status("thinking...") as status:
        st.write("ちょっと待ってね")
        assistant_response = get_answer(st.session_state.messages, user_input)
        status.update(label="I've got  an idea!", state="complete")
    
    st.session_state.messages.append({"role": "assistant", "content": assistant_response})
    with st.chat_message("assistant"):
        st.markdown(assistant_response)

    audio_content = stream_and_play(assistant_response)
    audio_base64 = base64.b64encode(audio_content).decode('utf-8')
    audio_html = f"""
        <audio id="audio" controls autoplay>
            <source src="data:audio/mp3;base64,{audio_base64}" type="audio/mp3">
        </audio>
        <script>
            var audio = document.getElementById('audio');
            audio.playbackRate = 1.2;
            audio.play();
        </script>
    """
    st.markdown(audio_html, unsafe_allow_html=True)

    del audio_data

 

ところどころ不要なコードが混じっていますがご容赦ください。

また再生速度を調整したいとおも「audio.playbackRate = 1.2;」を入れていますが、うまく効いていないようです。

最後に

今回はほぼ遊び感覚でRAGをVoice Chatで使用できる簡単な実装をしてみました。

私はご飯を食べながらなどテキストを打ちたくないけど知識をインプットしたい場合などに使用してみようかと思っています。

グループ研究開発本部 AI研究開発室では、データサイエンティスト/機械学習エンジニアを募集しています。ビッグデータの解析業務などAI研究開発室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。皆さんのご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事