Autonomous Database：ボイスメモをアップロードするとその内容が自動的に自然言語によるAIベクトル検索の対象になる仕組みの構築

# はじめに
OCIで提供されている様々なサービスを組み合わせて、Object Storageバケットにボイスメモ（m4a形式）をアップロードすると、自動的にトランスクリプトが作成され、そのトランスクリプトがAIベクトル検索（Select AI RAG）の対象となる仕組みを構築して検証してみました。

ここでの検証で使用しているOCIのサービスは、以下の通りです。
・Autonomous Database Serverless
・OCI Generative AI Service
・OCI AI Speech
・OCI Functions
・OCI Events
・OCI Object Storage など

:::note warn
注意
こちらの記事の内容はあくまで個人の実験メモ的な内容のため、こちらの内容を利用した場合のトラブルには一切責任を負いません。
また、こちらの記事の内容を元にしたOracleサポートへの問い合わせはご遠慮ください。
:::

# 1. リソース・プリンシパルの有効化

また、今回の検証ではリソース・プリンシパルを使用しますので、あらかじめ使用するAutonomous Databaseを含む動的グループ、ポリシーを作成しておきます。

なお、OCI AI Speechでトランスクリプション・ジョブを作成するには「manage ai-service-speech-transcription-job」ポリシーが必要です。

Autonomous Databaseにadminユーザとして接続します。

```
[oracle@oracle23ai sql]$ sqlplus admin/Demo#1Demo#1@atp23ai_tokyo

SQL*Plus: Release 23.0.0.0.0 - Production on Wed Mar 12 11:49:17 2025
Version 23.4.0.24.05

Copyright (c) 1982, 2024, Oracle.  All rights reserved.

Last Successful login time: Wed Mar 12 2025 11:37:14 +09:00

Connected to:
Oracle Database 23ai Enterprise Edition Release 23.0.0.0.0 - for Oracle Cloud and Engineered Systems
Version 23.7.0.25.03

SQL>
```

DBMS_CLOUD_ADMIN.ENABLE_RESOURCE_PRINCIPALプロシージャを実行して、Autonomous Databaseのリソース・プリンシパルを有効化します。
```
SQL> EXEC DBMS_CLOUD_ADMIN.ENABLE_RESOURCE_PRINCIPAL();

PL/SQLプロシージャが正常に完了しました。

SQL>
```

# 2. Object Storage バケットの作成
ボイスメモのアップロード先になるバケットSourceと、ボイスメモのトランスクリプトが出力されるバケットTargetを作成します。

バケット名：Source
オブジェクト・イベントの出力：チェックをオン
![スクリーンショット 2025-03-10 19.35.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/c0d27354-933f-4d34-904e-877cb8b3f935.png)

バケット名：Target
![スクリーンショット 2025-03-10 19.35.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/9f791208-dd61-4c30-8cbc-82b36f9feb07.png)

# 3. PL/SQLファンクションの作成
ソースとなる音声ファイルの情報をパラメータとして受け取り、OCI AI Speechをコールして音声ファイルのトランスクリプトを生成し、Object Storageバケットに出力するファンクションtranscription_funcを作成します。

```
SQL> CREATE OR REPLACE FUNCTION transcription_func ( namespace IN VARCHAR2, compartment_id IN VARCHAR2, input_bucket_name IN VARCHAR2, input_object_name IN VARCHAR2)
  2    RETURN VARCHAR2
  3  IS
  4  	 prefix VARCHAR2(100)             := 'trans_result';   -- ジョブの実行結果ファイルを保存する際の接頭辞
  5  	 output_bucket_name VARCHAR2(100) := 'Target';         -- トランスクリプションのファイルを出力するバケット
  6  	 lang_code VARCHAR2(2)            := 'ja';             -- メディアファイルの言語の言語コード
  7      model_type VARCHAR2(100)         := 'WHISPER_MEDIUM'; -- ジョブで使用するモデル
  8  
  9 　　　region              VARCHAR2(255);
 10  
 11  	 req_body            CLOB;
 12      resp                DBMS_CLOUD_TYPES.RESP;
 13      resp_txt            JSON;
 14  	 job_id 	         VARCHAR2(255);
 15  	 result_prefix       VARCHAR2(255);
 16  
 17  	 base_uri            VARCHAR2(255);
 18  	 result_object_name  VARCHAR2(255);
 19  	 transcript          CLOB;
 20  
 21  BEGIN
 22  -- ADBのあるリージョンを取得
 23  	 SELECT JSON_VALUE(cloud_identity, '$.REGION') INTO region FROM v$pdbs;
 24  
 25  -- AI Speechに送信するリクエストのbodyを構成
 26  	 SELECT
 27  	     JSON
 28  		 { 'compartmentId' VALUE compartment_id,
 29  		   'inputLocation' VALUE
 30  		     { 'locationType'	 VALUE 'OBJECT_LIST_INLINE_INPUT_LOCATION',
 31  		       'objectLocations' VALUE
 32  			  [
 33  			    { 'namespaceName' VALUE namespace,
 34  			      'bucketName'    VALUE input_bucket_name,
 35  			      'objectNames'   VALUE [input_object_name]
 36  			    }
 37  			  ]
 38  		     },
 39  		   'outputLocation' VALUE
 40  		     { 'namespaceName' VALUE namespace,
 41  		       'bucketName'    VALUE input_bucket_name,
 42  		       'prefix'        VALUE prefix
 43  		     },
 44  		   'modelDetails' VALUE
 45  		     { 'languageCode' VALUE lang_code,
 46  		       'modelType'    VALUE model_type
 47  		     }
 48  		 }
 49  	 INTO req_body;
 50  
 51  -- AI Speechにリクエストを送信
 52  	 resp := DBMS_CLOUD.SEND_REQUEST(
 53                  credential_name  => 'OCI$RESOURCE_PRINCIPAL',
 54                  uri              => 'https://speech.aiservice.'||region||'.oci.oraclecloud.com/20220101/transcriptionJobs',
 55                  method           => DBMS_CLOUD.METHOD_POST,
 56                  body             => APEX_UTIL.CLOB_TO_BLOB(p_clob => req_body)
 57              );
 58  
 59  -- AI SpeechからのレスポンスからジョブIDと結果ファイルの接頭辞を取得
 60      resp_txt      := JSON(DBMS_CLOUD.get_response_text(resp));
 61      job_id        := JSON_VALUE(resp_txt, '$.id');
 62  	 result_prefix := JSON_VALUE(resp_txt, '$.outputLocation.prefix');
 63  
 64  -- ジョブの終了を待つ
 65  	 LOOP
 66  		 DBMS_SESSION.SLEEP(5);
 67  	     resp := DBMS_CLOUD.SEND_REQUEST(
                         credential_name  => 'OCI$RESOURCE_PRINCIPAL',
 69                      uri              => 'https://speech.aiservice.'||region||'.oci.oraclecloud.com/20220101/transcriptionJobs/'||job_id,
 70                      method          => DBMS_CLOUD.METHOD_GET
 71                  );
 72          resp_txt := JSON(DBMS_CLOUD.get_response_text(resp));
 73  
 74          IF (JSON_VALUE(resp_txt, '$.lifecycleState') = 'SUCCEEDED') THEN
 75              EXIT;
 76  	     END IF;
 77      END LOOP;
 78  
 79  -- オブジェクト・ストレージにアクセスする際に使用するベースURIを構成
 80  	 base_uri := 'https://'||namespace||'.objectstorage.'||region||'.oci.customer-oci.com/n/'||namespace||'/b/';
 81  
 82  -- 結果ファイルのファイル名（オブジェクト名）を取得
 83  	 SELECT object_name INTO result_object_name FROM
 84  	     DBMS_CLOUD.LIST_OBJECTS(
 85  		     'OCI$RESOURCE_PRINCIPAL',
 86  		     base_uri||input_bucket_name||'/o/'||result_prefix||'*'
 87  	     );
 88  
 89  -- 結果ファイルからトランスクリプションを取得
 90  	 SELECT JSON_VALUE(
 91                 JSON(
 92  			        DBMS_CLOUD.GET_OBJECT(
 93                         credential_name => 'OCI$RESOURCE_PRINCIPAL',
 94                         object_uri => base_uri||input_bucket_name||'/o/'||result_prefix||result_object_name
 95  			        )
 96                 ), '$.transcriptions.transcription'
 97             ) INTO transcript;
 98  
 99  -- トランスクリプションをObject Storageにテキストファイルとして出力
100  	 DBMS_CLOUD.PUT_OBJECT(
101  	     credential_name => 'OCI$RESOURCE_PRINCIPAL',
102  	     object_uri => base_uri||output_bucket_name||'/o/'||SUBSTR(input_object_name, 1, LENGTH(input_object_name)-4)||'.txt',
103  	     contents => APEX_UTIL.CLOB_TO_BLOB(p_clob => transcript)
104  	 );
105  
106  	 RETURN 'Input file:'||'Input file: '||base_uri||input_bucket_name||'/o/'||input_object_name||'  '||'Output file:'||base_uri||output_bucket_name||'/o/'||SUBSTR(input_object_name, 1, LENGTH(input_object_name)-4)||'.txt';
107  END;
108  /

ファンクションが作成されました。

SQL> 
```

ORDS.ENABLE_OBJECTプロシージャを使用して、ファンクションtranscription_funcへのRESTアクセスを有効化します。
なお、ここでは検証の簡素化のために認証なしでアクセスできる設定になっていますが、本番環境でRESTアクセスを有効化する際は、認証を有効化することをお勧めします。
```
SQL> BEGIN
  2  	 ORDS.ENABLE_OBJECT(
  3  	     P_ENABLED	       => TRUE,
  4  	     P_SCHEMA	       => 'ADMIN',
  5  	     P_OBJECT	       => 'TRANSCRIPTION_FUNC',
  6  	     P_OBJECT_TYPE     => 'FUNCTION',
  7  	     P_OBJECT_ALIAS    => 'transcription_func',
  8  	     P_AUTO_REST_AUTH  => FALSE
  9  	 );
 10  	 COMMIT;
 11  END;
 12  /

PL/SQLプロシージャが正常に完了しました。

SQL>
```

# 4. OCI Functionsのファンクションの作成

OCI FunctionsのアプリケーションADBFuncCallAppを作成します。

![スクリーンショット 2025-03-12 14.57.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/2caf45b7-40fd-429c-ae0a-d941c37f2f3e.png)


Cloud Shellを起動します。

PL/SQLファンクションtranscription_funcを呼び出すファンクションinvoke_funcを作成します。
```
fn init --runtime python invoke_func
```

```
cd invoke_func
```

ファイル**func.yml**に
timeout: 300
を追加します。
```text:func.yml
schema_version: 20180708
name: invoke_proc
version: 0.0.36
runtime: python
build_image: fnproject/python:3.11-dev
run_image: fnproject/python:3.11
entrypoint: /python/bin/fdk /function/func.py handler
memory: 256
timeout: 300
```

ファイル**requirements.txt**に
requests
oci
を追加します。
```text:requirements.txt
fdk>=0.1.90
requests
oci
```

ファイル**func.py**を以下の内容に書き換えます。

requests.postのあとのURLは、Webアクセス(ORDS)のパブリック・アクセスURLの後に「/ユーザ名/ファンクション名/」を追加したものに置き換えます。
![スクリーンショット 2025-03-11 10.08.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/cefb118b-fd4a-40b8-806b-13fb687e8e95.png)


```python:func.py
import io
import json
import oci
import logging
import requests

from fdk import response

def handler(ctx, data: io.BytesIO = None):
	try:

		# イベント・ルールからアップロードされたオブジェクトの情報を取得
		body             = json.loads(data.getvalue())
		compartment_ocid = body["data"]["compartmentId"]                   # Compartment OCID
		object_name      = body["data"]["resourceName"]                    # Object Name
		namespace        = body["data"]["additionalDetails"]["namespace"]  # namespace
		bucket_name      = body["data"]["additionalDetails"]["bucketName"] # Bucket Name

		logging.getLogger().info("Namespace:"+namespace＋" Bucket Name:"+bucket_name+" Object Name:"+object_name+" Compartment OCID:"+compartment_ocid)

        # POSTパラメータを構成
		data = {'namespace': namespace, 'compartment_id': compartment_ocid, 'input_bucket_name': bucket_name, 'input_object_name': object_name}
		post_data = json.dumps(data)

        # PL/SQLファンクションのRESTをコール
		resp = requests.post('https://xxxxxxxxxx-atp23aitest.adb.ap-osaka-1.oraclecloudapps.com/ords/admin/transcription_proc/', data=post_data, headers={"Content-Type": "application/json"})
		logging.getLogger().info(resp.text)

	except (Exception, ValueError) as ex:
		logging.getLogger().error("Error: " + str(ex))
		return response.Response(
			ctx,
			response_data=json.dumps({"message": "Error: " + str(ex)}),
			headers={"Content-Type": "application/json"},
		)

	return response.Response(
		ctx,
		response_data=json.dumps({"message": "Function executed successfully"}),
		headers={"Content-Type": "application/json"},
	)
```
ファンクションをデプロイします。
```
fn -v deploy --app ADBFuncCallApp
```

# 5. イベント・ルールの作成

ボイスメモをアップロードするバケットSourceにファイルがアップロードされたら、ファンクションを実行するイベント・ルールObject_Create_Sourceを作成します。

表示名：Object_Create_Source
ルール条件：
＜条件1＞
・条件：イベントタイプ
・サービス名：Object Storage
・イベント・タイプ：Object - Create
＜条件2＞
・条件：属性
・属性名：compartmentId
・属性値：バケットSourceがあるコンパートメントのOCID
＜条件3＞
・条件：属性
・属性名：resourceName
・属性値：*.m4a
＜条件4＞
・条件：属性
・属性名：bucketName
・属性値：Source

アクション；
・アクション・タイプ：ファンクション
・ファンクション・コンパートメント：作成したファンクションのあるコンパートメント
・ファンクション・アプリケーション：ADBFuncCallApp
・ファンクション：invoke_func

![スクリーンショット 2025-03-10 19.42.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/af313082-6ebb-4be1-a1c1-c0ddf5b1db71.png)

# 6. AIプロファイルの作成

DBMS_CLOUD_AI.CREATE_PROFILEプロシージャで、SELECT AI RAGのためのAIプロファイルの作成を作成します。
```sql
BEGIN
    DBMS_CLOUD_AI.CREATE_PROFILE(
	     profile_name =>'GENAI_RAG',
 	     attributes   => '{ "provider" : "oci",
    			            "region"   : "ap-osaka-1",
    			            "model"    : "meta.llama-3.1-70b-instruct",
    	                    "embedding_model"	: "cohere.embed-multilingual-v3.0",
                            "credential_name"	: "OCI$RESOURCE_PRINCIPAL",
        		            "vector_index_name" : "MY_VECTOR_INDEX" }'
    );
END;
/
```


# 7. ベクトル索引とベクトル索引作成のパイプラインの作成

DBMS_CLOUD_AI.CREATE_VECTOR_INDEXプロシージャで、ベクトルストア、ベクトル索引、およびベクトル索引更新のためのパイプラインを作成します。

locationには、トランスクリプトが出力されるバケット（ここではTarget）を指定します。
refresh_rateを5として、5分おきにバケットの内容に応じてベクトルストアの内容およびベクトル索引が更新されるように設定しました。

```sql
BEGIN
    DBMS_CLOUD_AI.CREATE_VECTOR_INDEX(
  	 index_name  => 'MY_VECTOR_INDEX',
  	 attributes  => '{ "vector_db_provider": "oracle",
  			           "location": "https://ネームスペース.objectstorage.ap-osaka-1.oci.customer-oci.com/n/ネームスペース/b/Target/o/",
  			           "object_storage_credential_name": "OCI$RESOURCE_PRINCIPAL",
  			           "profile_name": "GENAI_RAG",
  			           "vector_distance_metric": "cosine",
  			           "vector_dimension": 1024,
  			           "chunk_overlap":128,
  			           "chunk_size":400,
  			           "refresh_rate":5 }'
    );
END;
/
```

# 8. 動作確認

この時点では、バケットSourceにはオブジェクトがありません。

![スクリーンショット 2025-03-14 20.34.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/6a88814b-c714-46b7-9ed4-32e502f24eca.png)

ボイスメモの音声ファイルtestvoice1.m4aをバケットSourceにアップロードします。

![スクリーンショット 2025-03-14 20.39.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/d0f68e99-209f-43ab-a5a7-2b283487ad94.png)

ファンクションのメトリックを確認すると、invoke_funcが実行されたことがわかります。
![スクリーンショット 2025-03-14 20.37.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/947aa676-f9a8-44de-b1ee-890c240e4fe4.png)

AI Speechのトランスクリプション・ジョブを確認すると、ジョブが問題なく完了したことがわかります。
![スクリーンショット 2025-03-14 20.38.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/9e5bb9e0-28fd-434b-86a1-9d4dca517981.png)

バケットTargetを確認してみます。

![スクリーンショット 2025-03-14 20.38.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/32fbc96b-fc15-48cc-af27-884b24a26a52.png)

testvoice1.txtが出力されていることが確認できました。

testvoice1.txtをダウンロードして、内容を確認してみます。

```text:testvoice1.txt
山 田 太 郎 です。 私 の 20 25 年 2 月 の 給 料 は 2 万 円 です。
```

ベクトル索引は5分おきに更新されているので、5分ほど待ってからSelect AI RAGでベクトル検索を実行します。

DBMS_CLOUD_AI.SET_PROFILEプロファイルで、セッションで使用するプロファイルを設定します。
```
SQL> EXEC DBMS_CLOUD_AI.SET_PROFILE('GENAI_RAG');
```

SELECT AI NARRATEで、自然言語によるベクトル検索を実行してみます。
```
SQL> SELECT AI NARRATE 山田太郎の2025年2月の給料は;

RESPONSE
--------------------------------------------------------------------------------
山田太郎の2025年2月の給料は2万円です。

Sources:
  - testvoice1.txt (https://objectstorage.ap-osaka-1.oraclecloud.com/n/orasejapa
n/b/Target/o/testvoice1.txt)


SQL>
```
結果が正しく返ってきました。

バケットTargetにあるファイルにはない情報を質問してみます。
```
SQL> SELECT AI NARRATE 山田太郎の2025年3月の給料はいくら;

RESPONSE
--------------------------------------------------------------------------------
山田太郎の2025年3月の給料は、提供された情報では確認できません。提供された情報で
は、山田太郎の2025年2月の給料は2万円であることが分かりますが、3月の給料について
は記載されていません。

Sources:
  - testvoice1.txt (https://objectstorage.ap-osaka-1.oraclecloud.com/n/orasejapa
n/b/Target/o/testvoice1.txt)


SQL>
```
情報がない旨が結果として返されました。

ボイスメモの音声ファイルtestvoice2.m4aをバケットSourceにアップロードします。
![スクリーンショット 2025-03-14 20.47.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/9fe1c040-42d1-4045-b3d2-628fe9d0ee98.png)

バケットTargetを確認します。
![スクリーンショット 2025-03-14 20.48.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/784824/844d5c23-13a9-45dc-ba01-2e8d988c3c7d.png)

testvoice2.txtが出力されます。

testvoice2.txtをダウンロードして、内容を確認してみます。

```text:testvoice2.txt
山 田 太 郎 です。 私 の 20 25 年 3 月 の 給 料 は、 先 月 よ り 1 万 円 増 え ました。
```

ベクトル索引の更新間隔を5分と設定したので、5分ほど待ってから、先ほどはありませんでしたが、バケットに追加したファイルに含まれている情報に関して質問してみます。
```
SQL> SELECT AI NARRATE 山田太郎の2025年3月の給料は;

RESPONSE
--------------------------------------------------------------------------------
山田太郎の2025年3月の給料は、先月より1万円増えました。先月の給料は2万円でした。
したがって、2025年3月の給料は3万円です。

Sources:
  - testvoice1.txt (https://objectstorage.ap-osaka-1.oraclecloud.com/n/orasejapa
n/b/Target/o/testvoice1.txt)
  - testvoice2.txt (https://objectstorage.ap-osaka-1.oraclecloud.com/n/orasejapa
n/b/Target/o/testvoice2.txt)


SQL>
```
RAGを利用したベクトル検索によって、結果が正しく返ってきました。

以上で、ボイスメモをアップロードするとその内容が自然言語によるAIベクトル検索の対象になる仕組みが構築できました。