はじめに
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 など
注意
こちらの記事の内容はあくまで個人の実験メモ的な内容のため、こちらの内容を利用した場合のトラブルには一切責任を負いません。
また、こちらの記事の内容を元にした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
オブジェクト・イベントの出力:チェックをオン
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を作成します。
Cloud Shellを起動します。
PL/SQLファンクションtranscription_funcを呼び出すファンクションinvoke_funcを作成します。
fn init --runtime python invoke_func
cd invoke_func
ファイルfunc.ymlに
timeout: 300
を追加します。
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
を追加します。
fdk>=0.1.90
requests
oci
ファイルfunc.pyを以下の内容に書き換えます。
requests.postのあとのURLは、Webアクセス(ORDS)のパブリック・アクセスURLの後に「/ユーザ名/ファンクション名/」を追加したものに置き換えます。
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
6. AIプロファイルの作成
DBMS_CLOUD_AI.CREATE_PROFILEプロシージャで、SELECT AI RAGのためのAIプロファイルの作成を作成します。
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分おきにバケットの内容に応じてベクトルストアの内容およびベクトル索引が更新されるように設定しました。
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にはオブジェクトがありません。
ボイスメモの音声ファイルtestvoice1.m4aをバケットSourceにアップロードします。
ファンクションのメトリックを確認すると、invoke_funcが実行されたことがわかります。
AI Speechのトランスクリプション・ジョブを確認すると、ジョブが問題なく完了したことがわかります。
バケットTargetを確認してみます。
testvoice1.txtが出力されていることが確認できました。
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にアップロードします。
testvoice2.txtが出力されます。
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ベクトル検索の対象になる仕組みが構築できました。