OCI Speechがwhisper対応したので簡易アプリを作ってみた

## はじめに
つい先日、Oracle Cloud Infrastructure (OCI) のSpeechサービスがwhisperモデルを用いて日本語を含む多言語対応を強化しました。

https://blogs.oracle.com/ai-and-datascience/post/oci-speech-supports-the-whisper-model


whisperは精度良く音声を文字列化できるので、様々なビジネスシーンで利用できそうです。
今回は、whisperモデルのOCI Speechだけでなく、生成AIも用いてテキスト整形や要約をする、簡単なGUI付きwebアプリ(streamlit)を作ってみます。

## whisperとは
whisperは、高度な音声認識技術を使用して、様々な言語の音声をテキストに変換するオープンソースのモデルです。
whisperモデルは、背景雑音が存在する環境や、さまざまなアクセントを持つ話者に対しても、高い精度で音声を認識できるように設計されています。whisperの特徴は、多言語対応と高い汎用性にあり、アプリケーション開発者にとって非常に魅力的な選択肢です。

詳細はこちらの記事が非常に参考になります。

https://qiita.com/diesekiefer/items/00d8c1507829b58a62ab


## OCI Speechの設定

まず以下のポリシーを設定しておきます。
```shell:speechに必要なポリシー
allow group SpeechUsers to manage ai-service-speech-family in tenancy
allow group SpeechUsers to manage object-family in tenancy
allow group SpeechUsers to read tag-namespaces in tenancy
allow group SpeechUsers to inspect tag-namespaces in tenancy
```
詳細はこちら：

https://docs.oracle.com/en-us/iaas/Content/speech/using/policies.htm

OCI Speechでwhisperモデルを選択し、Speech-to-Textを実行します。
2024/3時点では、oracleブログに書いてある通り、リアルタイムSpeech-to-Textは未対応ですが、OCI Speechでは下記流れとなり、OCIコンソールから実行できます。
 * オブジェクトストレージに変換対象音声/動画データをアップ
 * Speechジョブを作成
 * 指定したオブジェクトストレージにSpeech-to-Text結果を含むjsonを格納




## デモアプリ

### 概要
動作確認用のデモアプリはstreamlitで作成します。
前章の流れをoci python sdkを用いて一気通貫で実行してみます。

さらにSpeech-to-Textは生成AIとの親和性も高いため、出力された結果をLLM(今回はgpt3.5-turbo)を用い、整形処理（句読点と改行）と要約を簡単なプロンプトで実行してみます。

音声データやzoomの録画ファイルなどを枠内にD＆Dします↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/872a4bf2-bf04-dd7e-62d8-9b82aeeb9a6e.png)


### サンプルコード

まずは必要なモジュールを入れます。
```shell:必要なモジュールたち
pip install streamlit oci openai
```

streamlitデモアプリのソースは以下です：
```python:oci whisperのデモアプリ
import streamlit as st
import time
import json
from oci.config import from_file
from oci.object_storage import ObjectStorageClient
from oci.ai_speech import AIServiceSpeechClient
from oci.ai_speech.models import CreateTranscriptionJobDetails, ObjectListInlineInputLocation, ObjectLocation, OutputLocation, TranscriptionModelDetails
from datetime import datetime
from openai import OpenAI

config = from_file()
object_storage = ObjectStorageClient(config)
namespace = object_storage.get_namespace().data
st.title('Whisper demo🎤')
bucket_name = "bucket-specch_demo" #バケット名を入れます
uploaded_file = st.file_uploader(
    "音声/動画ファイルをドラッグ＆ドロップまたは選択してください", type=['mp3', 'wav', 'mp4', 'm4a', 'aac'])
if uploaded_file is not None:
    file_content = uploaded_file.getvalue()
    object_name = uploaded_file.name
    object_storage.put_object(namespace_name=namespace, bucket_name=bucket_name,
                              object_name=object_name, put_object_body=file_content)
    st.success(f"ファイル '{object_name}' がバケット '{bucket_name}' にアップロードされました！")
    st.session_state.uploaded_file_name = object_name

    st.write("OCI Speechの処理を開始します。")
    speech_client = AIServiceSpeechClient(config)
    audio_file_uri = f'oci://{bucket_name}@{namespace}/{
        st.session_state.uploaded_file_name}'

    create_transcription_job_details = CreateTranscriptionJobDetails(
        compartment_id='ocid1.compartment.oc1..aaaa...',    # コンパートメントIDを入れます
        display_name="PythonSDKSampleTranscriptionJob",
        description="Transcription job created by Python SDK",
        input_location=ObjectListInlineInputLocation(
            location_type="OBJECT_LIST_INLINE_INPUT_LOCATION",
            object_locations=[ObjectLocation(
                namespace_name=namespace,
                bucket_name=bucket_name,
                object_names=[st.session_state.uploaded_file_name])]),
        output_location=OutputLocation(
            namespace_name=namespace, bucket_name=bucket_name),
        model_details=TranscriptionModelDetails(
            model_type="WHISPER_MEDIUM", language_code="ja")
    )

    response = speech_client.create_transcription_job(
        create_transcription_job_details)
    job_id = response.data.id

    status_placeholder = st.empty()
    status_message = ""
    start_time = datetime.now()
    with st.spinner('処理中です。しばらくお待ちください...'):
        job_finished = False
        while not job_finished:
            current_time = datetime.now()
            processing_time = current_time - start_time
            job_details = speech_client.get_transcription_job(job_id).data
            job_status = job_details.lifecycle_state
            processing_time_str = str(processing_time).split('.')[0]
            new_status_message = f"Job Status: {
                job_status} - Processing Time: {processing_time_str}"
            if new_status_message != status_message:
                status_placeholder.write("更新中...")
                time.sleep(1)
                status_placeholder.empty()
            status_placeholder.write(new_status_message)
            status_message = new_status_message
            st.session_state.job_id = job_id
            if job_status in ["SUCCEEDED"]:
                job_finished = True
                output_location = job_details.output_location
                bucket_name = output_location.bucket_name
                namespace_name = output_location.namespace_name
                prefix = output_location.prefix
                object_storage_client = ObjectStorageClient(config)
                list_objects_response = object_storage_client.list_objects(
                    namespace_name=namespace_name,
                    bucket_name=bucket_name,
                    prefix=prefix,
                    fields="name"
                )
                file_name = list_objects_response.data.objects[0].name
                get_object_response = object_storage_client.get_object(
                    namespace_name=namespace_name,
                    bucket_name=bucket_name,
                    object_name=file_name
                )
                file_content = get_object_response.data.text
                transcriptions_json = json.loads(file_content)
                for transcription in transcriptions_json.get('transcriptions', []):
                    # LLMで整形処理を実施します。
                    client = OpenAI(
                        api_key="sk-***", # apiキーを取り込み
                    )
                    chat_completion = client.chat.completions.create(
                        messages=[
                            {
                                "role": "user",
                                "content": "以下に内容について句読点、改行(<br>)を入れてください。また最後に要約した内容を別途追記してください。：\n"
                                + transcription.get('transcription'),
                            }
                        ],
                        model="gpt-3.5-turbo",
                    )
                    st.markdown(
                        chat_completion.choices[0].message.content, unsafe_allow_html=True)
            else:
                time.sleep(5)
else:
    st.write("音声/動画ファイルをアップロードして、処理を開始してください。")
```


### 実行結果



以下の通りです。
whisper mediumモデルでは上手く句読点、改行が含まれないところを生成AIで補完し、かつ最後に要約した内容を出力しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/8963a84a-b515-bfae-48cd-6278d4bede6c.png)

参考までに、OCIコンソールにてwhisperでのテキスト化結果を確認できます
※生成AI整形前のため句読点が整っていない。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/d02e93a6-e7de-6f8a-5b87-21be0be77080.png)

### 追記
デモアプリの作りに関して、同期処理のような作りになっていますが、
実際は再生時間の長いファイルの場合、数十分かかることもあるため、アプリ作成時にはsdkでもサポートしている非同期処理の機能をアプリ側で作ることになるかと思います。
アプリに実装しているように、speechのjobを実行すると、job_idが払い出されます。
それをもとにstatusを確認して、SUCCEEDEDになったら出力格納先のobject storageを見に行く感じです。






## おわりに
OCI Speechの日本語対応は、開発者にとって大きなチャンスを提供しています。whisperを利用したアプリケーション開発は、音声データを活用した新しいソリューションの提供につながります。
今回作成したデモツールは、その第一歩に過ぎません。streamlitとwhisperの組み合わせにより、音声認識を用いたアプリケーションの可能性は無限に広がります。これからも、このようなテクノロジーの進化を活かし、さらに多くのイノベーションを生み出していくことを楽しみにしています。