6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OCI AI Speech を使って日本語のリアルタイム文字起こし動作を確認する

Posted at

はじめに

Oracle Cloud Infrastructure (OCI) の AI Speech サービスは、リアルタイムでの音声認識(文字起こし)機能を提供しています。
本記事では、Node.js (Express) をバックエンド、React をフロントエンドとした構成で、公開されているWeb SDKのサンプルコードを利用する手順を説明します。

公式のサンプルコード(example-client)をベースに、日本語の文字起こしを行うための設定変更点を中心に解説します。

下記リリースノートに記載があるように、Whisperモデルを利用すれば日本語が利用できます。

前提条件

  • Node.js: インストール済みであること。
  • OCI アカウント: 有効な OCI アカウントがあること。
  • OCI Config: ローカル環境 .oci/config に適切な設定ファイル(プロファイル)が存在すること。
    • バックエンドサーバーがこの設定を使用して認証トークンを取得します。

本手順はローカル環境での動作検証記事です。
OCI Computeなどのサーバ上で動かすには、httpsを利用したり、インスタンスプリンシパルなどを適用するなどの修正が必要になるためご注意ください。

プロジェクトのセットアップ

サンプルコードのディレクトリ構造は以下のようになっています。

example-client/
├── index.ts          # サーバーはOCI認証情報/認証プロバイダーにアクセスし、音声サービスを呼び出す
├── package.json
└── react-client/     # フロントエンド (Reactアプリ)
    ├── src/
    │   └── App.tsx   # クライアント実装
    └── package.json

クローン

GitHubからリポジトリをクローンします。

git clone https://github.com/oracle/oci-ai-speech-realtime-web-sdk.git
cd oci-ai-speech-realtime-web-sdk/example-client

インストール

プロジェクトのルートディレクトリで以下のコマンドを実行し、依存関係をインストールします。
package.json に便利な setup スクリプトが用意されており、バックエンド・フロントエンド両方の依存パッケージを一括でインストールできます。

npm run setup

バックエンドの実装 (index.ts)

バックエンドの役割は、OCI AI Speech サービスとの WebSocket 接続に必要な「セッション認証トークン」を発行することです。
SDK (oci-tool-sdk) の認証情報をブラウザ側に直接持たせるのはセキュリティ上推奨されないため、サーバー側でトークンを生成し、クライアントに渡す構成をとります。

修正ポイント

example-client/index.ts を開き、以下の2点を自分の環境に合わせて変更します。

  1. import文を修正
  2. コンパートメントID (compartmentId)
  3. リージョン (region)
  4. (任意)認証情報
  5. 末尾をコメントアウトからアンコメント
index.ts
// ... (imports)
import http from "http";  // <--- httpsからhttpに変更


// 1. 認証と認可に使用するコンパートメントのOCIDを設定
const compartmentId = "ocid1.compartment.oc1..aaaaaaaa..."; // <--- ご自身のコンパートメントOCIDに変更

// 2. サービスを使用するリージョンを設定
const region = "ap-tokyo-1"; // <--- 東京リージョン (ap-tokyo-1) などに変更

// ...

// リアルタイムセッション・トークンを生成する関数
async function getRealtimeToken() {
  const provider: common.SessionAuthDetailProvider = new common.SessionAuthDetailProvider("~/.oci/config", "DEFAULT"); // <--- 必要に応じて認証情報のパスを編集
  provider.setRegion(region);

  const speechClient = new aispeech.AIServiceSpeechClient({ authenticationDetailsProvider: provider });

  // ... (トークン生成リクエスト)
}

// uncomment to use http if certificates not available
// コメント状態からアンコメントにする!
app.listen(port, () => {
  console.log(`Server initiated on ${port}`);
});

このサーバーは /authenticate エンドポイントを提供し、トークンを返却します。また /region エンドポイントでリージョン情報もクライアントに通知します。

修正全文(コンパートメントocidは要修正)
/*
**
** Copyright (c) 2024, 2025, Oracle and/or its affiliates
** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/

* @description A very simple backend server to authenticate to OCI Services.
*
* Written in TypeScript using express-js, this acts as an minimal working example of
* a server hosted by a customer.
*
* The primary purpose of this server is to accept realtime session authentication
* requests from customer's frontend application and communicate with OCI Speech Service's
* `createRealtimeSessionToken` API to generate a JWT which can be used by the frontend to
* create a direct websocket connection to OCI Realtime Speech Service
*
* It will be customer's responsibility to handle Authentication and Authorization
* between the customer's frontend application and customer's server. In case no AuthN/Z
* mechanism is put in place, any client which hits their endpoint may be able to get a
* JWT generated on the customer's expense.
*
* @tutorial
* OCI Typescript SDK getting started guide -
* https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/typescriptsdkgettingstarted.htm
*
* OCI AI Speech Realtime Session Token Generation -
* https://docs.oracle.com/en-us/iaas/api/#/en/speech/20220101/RealtimeSessionToken/CreateRealtimeSessionToken
**/

import express from "express";
import * as common from "oci-common";
import * as aispeech from "oci-aispeech";
import bodyParser from "body-parser";

import fs from "fs";
import http from "http";

const app = express();
const cors = require("cors");
const path = require("path");
const port = 8448;

app.use(cors());

// The OCID of the compartment that will be used for authentication and authorization
const compartmentId = "ocid1.compartment.oc1..aata";

// Set the region where you want to use the services
const region = "ap-tokyo-1"; // e.g. "us-phoenix-1";

/**
 * Generates a real-time session token using Oracle Cloud Infrastructure (OCI) AI Speech Service.
 *
 * This function configures the OCI client using a specified region and compartment ID, and
 * then sends a request to generate a real-time session token for the AI Speech Service.
 *
 * @async
 * @function
 *
 * @returns {Promise<string>} The real-time session token generated by the AI Speech Service.
 *
 * @throws {Error} If the request to generate the session token fails.
 */
async function getRealtimeToken() {
  // Use the AuthDetailsProvider suited for your use case.
  // Read more at - https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdk_authentication_methods.htm
  const provider: common.SessionAuthDetailProvider = new common.SessionAuthDetailProvider("~/.oci/config", "DEFAULT");

  provider.setRegion(region);

  // Initialize the OCI AI Speech API Client
  const speechClient = new aispeech.AIServiceSpeechClient({ authenticationDetailsProvider: provider });

  // Create a request and dependent object(s).
  const createRealtimeSessionTokenDetails = {
    compartmentId: compartmentId,
  };

  const createRealtimeSessionTokenRequest: aispeech.requests.CreateRealtimeSessionTokenRequest = {
    createRealtimeSessionTokenDetails: createRealtimeSessionTokenDetails,
  };

  // Send request to the Client.
  const createRealtimeSessionTokenResponse = await speechClient.createRealtimeSessionToken(createRealtimeSessionTokenRequest);

  console.log("Token generated: ", createRealtimeSessionTokenResponse);
  return createRealtimeSessionTokenResponse.realtimeSessionToken;
}

app.use(bodyParser.json());

/**
 * Handles the `/authenticate` GET request. This route initiates the process of
 * obtaining a real-time token by calling the `getRealtimeToken` function.
 *
 * @param {express.Request} req - The Express request object.
 * @param {express.Response} res - The Express response object.
 *
 * @returns {void}
 * @throws {Error} If the token retrieval fails.
 */
app.get("/authenticate", (req, res) => {
  getRealtimeToken()
    .then((response) => {
      console.log("Response: ", response);
      res.send(response);
    })
    .catch((error) => {
      console.log("createRealtimeSessionToken Failed with error ", error);
      res.status(401);
      res.send(error);
    });
});

/**
 * Handles the `/region` GET request. This route responds with the current region.
 * Use this call to ensure the frontend uses the same region as the server.
 *
 * @param {express.Request} req - The Express request object.
 * @param {express.Response} res - The Express response object.
 *
 * @returns {void}
 */
app.get("/region", (req, res) => {
  res.send({ region: region });
});

/**
 * Add your own certificates to setup HTTPS server.
 */
// const httpsOptions = {
//   key: fs.readFileSync("./security/cert.key"),
//   cert: fs.readFileSync("./security/cert.pem"),
// };

/**
 * Creates an HTTPS Express server and listens on the specified port.
 *
 * @param {{key: Buffer, cert: Buffer}} httpsOptions - Options for HTTPS server like certificate and key.
 * @param {Express} app - Express object that is bind to the HTTPS server.
 * @param {number} port - The port number on which the server will listen.
 * @param {() => void} [callback] - Optional callback function that is invoked after the server starts listening.
 *
 * @returns {https.Server}
 */
/* uncomment to use https if certificates are available
const server = https.createServer(httpsOptions, app).listen(port, () => {
  console.log(`Server initiated on ${port}`);
});
*/

// uncomment to use http if certificates not available
app.listen(port, () => {
  console.log(`Server initiated on ${port}`);
});


フロントエンドの実装 (App.tsx)

フロントエンドは、取得したトークンを使って OCI AI Speech サービスと WebSocket 接続を確立し、マイク音声を送信して結果を受け取ります。

修正ポイント

日本語の文字起こしを行うため、react-client/src/App.tsx 内のパラメータ設定を変更します。

realtimeClientParametersの中が変更点です。

  1. 言語コード (languageCode): "ja" (日本語) に設定
  2. モデルタイプ (modelType): "WHISPER" (Whisperモデル) に設定
  3. 一部Whsper非対応のパラメータをコメントアウト

パラメータ詳細は下記をご確認ください

src/App.tsx
    const realtimeClientParameters: RealtimeParameters = {
      customizations: [],
      languageCode: "ja", //jaに
      modelDomain: RealtimeParametersModelDomainEnum.GENERIC,
      modelType: "WHISPER", //Whisperに
      // stabilizePartialResults: RealtimeParametersStabilizePartialResultsEnum.NONE,
      // partialSilenceThresholdInMs: 0,
      // finalSilenceThresholdInMs: 1000,
      // shouldIgnoreInvalidCustomizations: false,
      // punctuation: RealtimeParametersPunctuationEnum.NONE,
      encoding: "audio/raw;rate=16000",
    };
一応全文
src/App.tsx
/*
 ** Copyright (c) 2024, 2025, Oracle and/or its affiliates
 ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
 */

import React from "react";
import "./App.css";
import {
  AIServiceSpeechRealtimeApi,
  RealtimeParameters,
  RealtimeClientListener,
  RealtimeMessageAckAudio,
  RealtimeMessageResult,
  RealtimeParametersModelDomainEnum,
  RealtimeParametersStabilizePartialResultsEnum,
  RealtimeParametersPunctuationEnum,
} from "@oracle/oci-ai-speech-realtime-web";

function App() {
  // Initialize the OCI Realtime Speech Web SDK
  const realtimeWebSocket = React.useRef<AIServiceSpeechRealtimeApi>();

  const [tokenDetails, setTokenDetails] = React.useState("");
  const [errors, setErrors] = React.useState([]);
  const [buttonState, setButtonState] = React.useState(false);

  // States to store results from the OCI Realtime Speech
  const [message, setMessage] = React.useState<RealtimeMessageResult>();
  const [resultStream, setResultStream] = React.useState([]);
  const [partialResult, setPartialResult] = React.useState("");
  const [finalResult, setFinalResult] = React.useState("");

  // States to store timings of various events
  const [startTime, setStartTime] = React.useState(0);
  const [tokenTime, setTokenTime] = React.useState(0);
  const [connectionTime, setConnectionTime] = React.useState(0);
  const [authTime, setAuthTime] = React.useState(0);

  /* CUSTOMER DEFINED API CALL
   *
   * Call the customer's server to get the region to which the websocket will connect to
   *
   */
  const getRegion = async () => {
    try {
      const response = await fetch("http://localhost:8448/region", {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        },
      });

      const result = await response.text();
      console.log("Server Response:", JSON.parse(result));
      return JSON.parse(result);
    } catch (error) {
      console.error("Error in fetching region:", error);
      setErrors((prevErrors) => [...prevErrors, "Error in fetching region:" + JSON.stringify(error)]);
      return {};
    }
  };

  /* CUSTOMER DEFINED API CALL
   *
   * Call the customer's server to request a session token from the OCI Speech Service
   *
   */
  const authenticate = async () => {
    try {
      const response = await fetch("http://localhost:8448/authenticate", {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        },
      });

      if (response.ok) {
        const result = await response.text();
        console.log("Server Response:", JSON.parse(result));
        return JSON.parse(result);
      } else {
        console.error("Error in fetching session token:", response);
        setErrors((prevErrors) => [...prevErrors, "Error in fetching session token: " + response.status + " " + response.statusText]);
        return {};
      }
    } catch (error) {
      console.error("Error in fetching session token:", error);
      setErrors((prevErrors) => [...prevErrors, "Error in fetching session token: " + JSON.stringify(error)]);
      return {};
    }
  };

  // Update the User Interface on each message received from the WebSocket
  React.useEffect(() => {
    if (message) {
      setResultStream((resultStream) => [message, ...resultStream]);
      let data = message;
      console.log(data);
      if (data && data.event === "RESULT") {
        let transcriptions = data.transcriptions;
        if (transcriptions[0].isFinal) {
          setFinalResult((finalResult) => finalResult + (finalResult.length > 0 ? " " : "") + transcriptions[0].transcription);
          setPartialResult("");
        } else {
          setPartialResult(transcriptions[0].transcription);
        }
      }
    }
  }, [message]);

  // Start the transcription session
  const startSession = async () => {
    reset();
    setStartTime(new Date().getTime());

    //use the same region as being used in the server for token generation
    const serviceRegion = (await getRegion()).region;

    // set the various parameters for the Realtime Speech session
    // Read more - https://docs.oracle.com/en-us/iaas/api/#/en/speech/20220101/datatypes/RealtimeParameters
    const realtimeClientParameters: RealtimeParameters = {
      customizations: [],
      languageCode: "ja",
      modelDomain: RealtimeParametersModelDomainEnum.GENERIC,
      modelType: "WHISPER",
      // stabilizePartialResults: RealtimeParametersStabilizePartialResultsEnum.NONE,
      // partialSilenceThresholdInMs: 0,
      // finalSilenceThresholdInMs: 1000,
      // shouldIgnoreInvalidCustomizations: false,
      // punctuation: RealtimeParametersPunctuationEnum.NONE,
      encoding: "audio/raw;rate=16000",
    };

    // configure the EventListeners for various events emitted by the OCI Realtime Speech
    const realtimeEventListener: RealtimeClientListener = {
      // triggered when `isAckEnabled` parameter is set to true.
      onAckAudio(ackMessage: RealtimeMessageAckAudio) { },

      // triggered when connection with the service is established
      onConnect(openEvent) {
        setButtonState(true);
        setConnectionTime(new Date().getTime());
        console.log("WebSocket Client Connected");
      },

      // triggered when connection with the service is authenticated
      onConnectMessage(connectMessage) {
        console.log("WebSocket Client Authenticated: ", connectMessage);
        setResultStream([connectMessage, ...resultStream]);
        setAuthTime(new Date().getTime());
      },

      // triggered whenever there is an error
      onError(error) {
        setButtonState(false);
        setErrors((prevErrors) => [...prevErrors, error.name + " " + error.message]);
        console.error(error);
      },

      // triggered when a transcription result is received
      onResult(resultMessage) {
        console.log(resultMessage);
        setMessage(resultMessage);
      },

      // triggered when a session is closed for some reason
      onClose(closeEvent) {
        setButtonState(false);
        console.log("WebSocket server closed: ", closeEvent.code, closeEvent.reason);
      },
    };

    // fetch the session token from the customer's server and
    // only then proceed to start the session with OCI Realtime Speech
    authenticate().then((tokenObject) => {
      console.log("Token received", tokenObject);
      setTokenTime(new Date().getTime());
      if (tokenObject.sessionId === undefined) {
        setTokenDetails(JSON.stringify(tokenObject, undefined, 4));
        stopSession();
        return;
      }
      setTokenDetails(JSON.stringify(tokenObject, undefined, 4));

      // configure the Realtime Speech client with Parameters, Token and EventListener
      realtimeWebSocket.current = new AIServiceSpeechRealtimeApi(
        realtimeEventListener,
        tokenObject.token,
        tokenObject.compartmentId,
        `wss://realtime.aiservice.${serviceRegion}.oci.oraclecloud.com/ws/transcribe/stream`,
        realtimeClientParameters
      );

      // connect to the Realtime Speech service
      realtimeWebSocket.current.connect();
    });
  };

  // Stop the transcription session
  const stopSession = () => {
    try {
      realtimeWebSocket.current.close();
    } catch (e) {
      console.error(e);
    }
  };

  // Request the service to send the final result
  const requestFinalResult = () => {
    realtimeWebSocket.current && realtimeWebSocket.current.requestFinalResult();
  };

  // Reset the elements on the screen
  const reset = () => {
    setAuthTime(0);
    setConnectionTime(0);
    setTokenTime(0);
    setStartTime(0);
    setFinalResult("");
    setPartialResult("");
    setMessage(undefined);
    setResultStream([]);
    setTokenDetails("");
    setErrors([]);
  };

  // Render a very simple UI for showcasing OCI Realtime Speech Service
  return (
    <div className="App">
      <div>
        <h1>OCI Realtime Speech</h1>
        <h4>Web SDK client example</h4>
        <span>
          {/* Start and stop the session */}
          <button
            onClick={() => {
              return buttonState ? stopSession() : startSession();
            }}>
            {buttonState ? "Stop Session" : "Start session"}
          </button>

          {/* Request the service to send the final result */}
          <button onClick={requestFinalResult}>{"Request final result"}</button>

          {/* Clear the screen and stop the session */}
          <button
            onClick={() => {
              buttonState && stopSession();
              reset();
            }}>
            {"Clear screen"}
          </button>
        </span>
      </div>
      <hr />

      {/* Displays the timings of various events involved
       * in a successful Realtime Speech connection */}
      {startTime > 0 && (
        <>
          <h6>
            <pre>Start time - {new Date(startTime).toLocaleString()}</pre>
            {tokenTime > 0 && (
              <pre>
                Token received - {new Date(tokenTime).toLocaleString()} (+ {tokenTime - startTime} ms)
              </pre>
            )}
            {connectionTime > 0 && (
              <pre>
                Websocket connected - {new Date(connectionTime).toLocaleString()} (+ {connectionTime - tokenTime} ms)
              </pre>
            )}
            {authTime > 0 && (
              <pre>
                Realtime authenticated - {new Date(authTime).toLocaleString()} (+ {authTime - connectionTime} ms)
              </pre>
            )}
          </h6>
        </>
      )}

      {/* Display the errors */}
      {errors?.length > 0 && (
        <>
          <h4>Errors</h4>
          <pre>{errors}</pre>
        </>
      )}

      {/* Display the token details as returned from
       * the customer's server via the /authenticate GET call
       * which in turn are returned by OCI Speech Service's
       * `createRealtimeSessionToken` API */}
      {tokenDetails && (
        <>
          <h4>Token Details</h4>
          <pre>{tokenDetails}</pre>
        </>
      )}

      {/* Display the transcriptions returned by the service.
       * Partial transcriptions are italicized where as
       * finalized transcriptions are in regular font.
       */}
      {resultStream.length > 0 && (
        <>
          <h4>Transcriptions</h4>
          <pre style={{ textWrap: "pretty" }}>
            {finalResult + (finalResult.length > 0 ? " " : "")}
            <em>{partialResult}</em>
          </pre>
        </>
      )}

      {/* Display raw messages coming from the websocket connection */}
      {resultStream.length > 0 && (
        <>
          <h4>Websocket Messages</h4>
          <pre>{JSON.stringify(resultStream, undefined, 4)}</pre>
        </>
      )}
    </div>
  );
}

export default App;

startSession 関数内で AIServiceSpeechRealtimeApi のインスタンスを作成し、connect() メソッドを呼び出すことでリアルタイム文字起こしが開始されます。

アプリケーションの実行

設定変更が完了したら、以下のコマンドでアプリケーションを起動します。
このコマンドは concurrently を使用して、バックエンドサーバーと React クライアントを同時に起動します。

npm start
  • バックエンド: http://localhost:8448 で起動
  • フロントエンド: ブラウザが自動的に開き http://localhost:3000 にアクセス

動作確認

  1. ブラウザに表示された画面の「Start session」ボタンをクリックします。
  2. マイクの使用許可を求められた場合は許可します。
  3. マイクに向かって話すと、画面上の "Transcriptions" 欄にリアルタイムで文字起こし結果が表示されます。

image.png

まとめ

OCI AI Speech Realtime Web SDK を使うことで、リアルタイム音声認識アプリを構築できます。
今回は日本語モデル (Whisper) を利用する設定を行いました。
これを活用することで、AI活用などに活かすことが期待できます。

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?