Have an amazing solution built in RAD Studio? Let us know. Looking for discounts? Visit our Special Offers page!
DelphiRAD StudioShowcase

Delphiアプリでリアルな音声合成を行うOpenAI APIを使ってみよう!

andres-siimon-bbqvpte4vw4-unsplash

このブログ記事は、Softacom Informationの「How To Use OpenAI To Add Realistic Text-To-Speech To Your Apps」の抄訳です

speech-and-text-by-speech-generation-capabilities-with-delphi-fmx-app-via-openai-api

OpenAI を使用して、クロスプラットフォーム アプリでテキストからリアルな音声を生成したいですか? 本ブログでは、TechパートナーであるSoftacom の記事を通してその方法について詳しく紹介いたします。

アプリにリアルなテキスト読み上げ(音声合成)を追加するOpenAIの使用方法の概要

最新の人工知能ベースのサービスにより、音声の生成と音声からテキストへの変換が可能になります。しかも、さまざまな言語に対応しています。簡単にテキストサービスへ入力でき、合成された音声出力を受け取ることができます。利用可能な設定のおかげで、生成される音声のタイプを選択することもできます。

さらに、音声をテキストに変換することも可能です。例えば、お気に入りのアーティストのMP3トラックから曲をテキストに書き起こすことができます。

このブログでは、Delphi FMXアプリケーションで、テキスト記述に基づく音声生成のためのOpenAI APIの機能を分析し、さらに音声からテキストを生成する方法を簡単な手順を交えて説明いたします。

OpenAI API の機能を音声生成やテキスト変換に使用するには、秘密鍵を登録して取得する必要があります。こちらの記事では、テキスト生成に特化した内容ですが、登録と秘密鍵 (API キー) を取得するプロセスが説明されています。

ユーザーのリクエストに基づいて音声を生成するには、OpenAI API ([Create speech] タブ) を利用します。

createspeechitem

OpenAI APIは、音声生成のための広範な機能を提供しています。ここでは、音声の種類、出力メディアファイル形式(mp3、wavなど)、生成メディアファイルの音声速度、生成された音声のテキストによる説明、および使用する機械学習モデル(ts-1またはts-1-hd)を設定できます。

openaiapisettingsforspeechgeneration

音声からテキストを抽出するには、OpenAI API (「Create transcription」タブ) も使用します。

createtranscriptionitem

OpenAI APIは、音声に基づいてテキストを生成するための豊富な機能も備えています。

ここでは、入力メディアファイルのタイプ(mp3、wavなど)と、OpenAIからのレスポンスのフォーマット(json、text、srt、verbose_json、vtt)を設定できます。

openaiapisettingsfortranscriptiongeneration

ユーザーのリクエストに基づいて音声を生成し、音声からテキストを生成するOpenAI API の使いやすさを向上させるために、テキスト生成に焦点を当てたこちらの記事で説明されており、以前に開発された TChatGPT クラスの機能を拡張しています。

GetGeneratedSpeechAsStream や GetGeneratedTextFromSpeech などのメソッドをクラスに追加しましょう。 また、メディアファイル内の音声からテキストを抽出できるようにするために、オーバーロードされたコンストラクタメソッド (Create) を追加します。

tchatgptmodifiedclass

取得したバージョンの TChatGPTクラスのコンストラクタメソッドは、入力パラメータとして以下のオブジェクトを受け取ります: HttpBasicAuthenticator (THTTPBasicAuthenticator クラス)、RESTClient (TRESTClient クラス)、RESTRequest (TRESTRequest クラス)、および文字列定数 OpenAIApiKeyと 秘密鍵 です。

overloadedconstructor

ChatGPTクラスは、OpenAI APIを使用して、テキスト記述に基づいて生成された音声を含むTMemoryStreamオブジェクトを取得することができます。

入力パラメータには、文字列定数InputとVoiceがあり、テキスト記述と生成された音声のタイプ(alloy、echo、fable、onyx、nova、shimmer)を表します。

使用される機械学習モデル(ここではtts-1)の詳細と、音声生成のためのテキスト記述(input)と音声タイプ(voice)は、TJSONObjectクラスのオブジェクトであるJObjに含まれています。

文字列変数Requestは、JObjオブジェクトからのデータを文字列として格納します。さらに、文字列形式のRequestの内容は、StringStreamオブジェクト(TStringStreamクラス)に渡されます。

次に、StringStreamからの文字列データは、MultipartFormDataオブジェクト(TMultipartFormDataクラス)に転送されます。音声生成向けの OpenAI API の URL が入力パラメータとして FNetHttpClient オブジェクトの Post メソッドに送信され、さらにモデル データ、音声生成のテキスト記述、および音声タイプを含む MultipartFormData オブジェクトも渡されます。

テキストや画像の生成プロジェクトと同様に、ヘッダ(AuthorizationとContent-Type)も含める必要があります。FNetHttpClient.Postメソッドを実行すると、生成された音声がOpenAIからTMemoryStreamの形式で取得されます。

getgeneratedspeechasstream_method

GetGeneratedTextFromSpeechメソッドを使用すると、音声をテキストに変換できます。このメソッドは、メディア ファイルへのパスを含む文字列定数 InputAudioFilePath を入力として受け取ります。 

FRESTClientオブジェクトのBaseURLプロパティには、メディアファイルから音声に基づいたテキストを生成するためのOpenAI APIのURLが格納されています。FRESTRequestオブジェクトには、OpenAIからのレスポンスの種類(text、json、srt、verbose_json、vtt)、使用した機械学習モデル(ここではwhisper-1)、音声を録音したメディアファイルへのパスに関する情報が格納されています。

認証は、FHTTPBasicAuthenticator オブジェクト(THTTPBasicAuthenticator クラス)を使用して実行されます。Password フィールドに秘密鍵を割り当てる必要があります 。(FHTTPBasicAuthenticator.Password:=FOpenAIApiKey)

FRESTRequest.Execute メソッドは POST リクエストを実行し、メディアファイルを渡して OpenAI を使用してテキストを抽出します。その結果、音声テキストが変換された文字列が返されます。(Result:= FRESTRequest.Response.Content)

getgeneratedtextfromspeech_method

TChatGPTクラスの全ソースコードは以下の通りです。

Delphi FMX アプリケーションのメディア ファイル内のテキスト記述と音声からのテキスト抽出に基づく音声生成の実装

unit ChatGPTHelper;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes,
  System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics,
  FMX.Dialogs, FMX.Memo.Types, FMX.ScrollBox, FMX.Memo, FMX.StdCtrls,
  FMX.Controls.Presentation, System.Net.URLClient, System.Net.HttpClient,
  System.Net.HttpClientComponent, JSON, System.Threading,
  System.Net.Mime, System.Generics.Collections,
  REST.Client, REST.Types, REST.Authenticator.Basic;

type
  IChatGPTHelper = interface
    function SendTextToChatGPT(const Text: string): string;
    function GetJSONWithImage(const Prompt: string; ResponseFormat: Integer): string;
    function GetImageURLFromJSON(const JsonResponse: string): string;
    function GetImageAsStream(const ImageURL: string): TMemoryStream;
    function GetImageBASE64FromJSON(const JsonResponse: string): string;
    function GetGeneratedSpeechAsStream(const Input: string; const Voice: string): TMemoryStream;
    function GetGeneratedTextFromSpeech(const InputAudioFilePath: string): string;
  end;

  TChatGPT = class(TInterfacedObject, IChatGPTHelper)
  private
    FNetHttpClient: TNetHTTPClient;
    FHttpBasicAuthenticator: THTTPBasicAuthenticator;
    FRestRequest: TRESTRequest;
    FRestClient: TRESTClient;
    FOpenAIApiKey: string;
    FText: string;
    function FormatJSON(const JSON: string): string;
    function SendTextToChatGPT(const Text: string): string;
    function GetJSONWithImage(const Prompt: string; ResponseFormat: Integer): string;
    function GetImageURLFromJSON(const JsonResponse: string): string;
    function GetImageAsStream(const ImageURL: string): TMemoryStream;
    function GetImageBASE64FromJSON(const JsonResponse: string): string;
    function GetGeneratedSpeechAsStream(const Input: string; const Voice: string): TMemoryStream;
    function GetGeneratedTextFromSpeech(const InputAudioFilePath: string): string;
  public
    constructor Create(const NetHttpClient: TNetHTTPClient;
      const OpenAIApiKey: string); overload;
    constructor Create(const HttpBasicAuthentificator: THTTPBasicAuthenticator;
      const RESTClient: TRESTClient; const RESTRequest: TRESTRequest;
      const OpenAIApiKey: string); overload;
    class function MessageContentFromChatGPT(const JsonAnswer: string): string;
  end;

implementation

{ TFirebaseAuth }

constructor TChatGPT.Create(const NetHttpClient: TNetHTTPClient;
  const OpenAIApiKey: string);
begin
  FNetHttpClient := NetHttpClient;
  if OpenAIApiKey  '' then
    FOpenAIApiKey := OpenAIApiKey
  else
  begin
    ShowMessage('OpenAI API key is empty!');
    Exit;
  end;
end;

constructor TChatGPT.Create(const HttpBasicAuthentificator: THTTPBasicAuthenticator;
  const RESTClient: TRESTClient; const RESTRequest: TRESTRequest;
  const OpenAIApiKey: string);
begin
  FHttpBasicAuthenticator := HttpBasicAuthentificator;
  FRestRequest :=  RESTRequest;
  FRestClient := RESTClient;
  if OpenAIApiKey  '' then
    FOpenAIApiKey := OpenAIApiKey
  else
  begin
    ShowMessage('OpenAI API key is empty!');
    Exit;
  end;
end;

function TChatGPT.FormatJSON(const JSON: string): string;
var
  JsonObject: TJsonObject;
begin
  JsonObject := TJsonObject.ParseJSONValue(JSON) as TJsonObject;
  try
    if Assigned(JsonObject) then
      Result := JsonObject.Format()
    else
      Result := JSON;
  finally
    JsonObject.Free;
  end;
end;

function TChatGPT.GetGeneratedSpeechAsStream(const Input, Voice: string): TMemoryStream;
var
  JObj: TJsonObject;
  Request: string;
  MultipartFormData: TMultipartFormData;
  MyHeaders: TArray;
  StringStream: TStringStream;
begin
  JObj := nil;
  MultipartFormData := nil;
  StringStream := nil;
  try
    Result := TMemoryStream.Create;
    SetLength(MyHeaders, 2);
    MyHeaders[0] := TNameValuePair.Create('Authorization', FOpenAIApiKey);
    MyHeaders[1] := TNameValuePair.Create('Content-Type', 'application/json');
    JObj := TJSONObject.Create;
    JObj.AddPair('model', 'tts-1');
    JObj.AddPair('input', Input);
    JObj.AddPair('voice', Voice);
    Request := JObj.ToString;
    MultipartFormData := TMultipartFormData.Create;
    StringStream := TStringStream.Create(Request, TEncoding.UTF8);
    MultipartFormData.Stream.LoadFromStream(StringStream);
    FNetHttpClient.Post('https://api.openai.com/v1/audio/speech',
    MultiPartFormData, Result, MyHeaders);
  finally
    JObj.Free;
    MultipartFormData.Free;
    StringStream.Free;
  end;
end;

function TChatGPT.GetGeneratedTextFromSpeech(const InputAudioFilePath: string): string;
begin
  FRESTClient.Authenticator := FHTTPBasicAuthenticator;
  FRESTRequest.Method := TRESTRequestMethod.rmPOST;
  FHTTPBasicAuthenticator.Password := FOpenAIApiKey;
  FRESTClient.BaseURL := 'https://api.openai.com/v1/audio/transcriptions';
  FRESTRequest.AddParameter('response_format', 'text',
    TRESTRequestParameterKind.pkREQUESTBODY);
  FRESTRequest.AddParameter('model', 'whisper-1',
    TRESTRequestParameterKind.pkREQUESTBODY);
  FRESTRequest.AddFile('file', InputAudioFilePath,
    TRESTContentType.ctAPPLICATION_OCTET_STREAM);
  FRESTRequest.Client := FRESTClient;
  FRESTRequest.Execute;
  Result := FRESTRequest.Response.Content;
end;

function TChatGPT.GetImageAsStream(const ImageURL: string): TMemoryStream;
begin
  Result := TMemoryStream.Create;
  FNetHTTPClient.Get(ImageURL, Result);
end;

function TChatGPT.GetImageURLFromJSON(const JsonResponse: string): string;
var
  Json: TJsonObject;
  DataArr: TJsonArray;
begin
  Json := TJsonObject.ParseJSONValue(JsonResponse) as TJsonObject;
  try
    if Assigned(Json) then
    begin
      DataArr := TJsonArray(Json.Get('data').JsonValue);
      Result := TJSONPair(TJSONObject(DataArr.Items[0]).Get('url')).JsonValue.Value;
    end
    else
      Result := '';
  finally
    Json.Free;
  end;
end;

function TChatGPT.GetImageBASE64FromJSON(const JsonResponse: string): string;
var
  Json: TJsonObject;
  DataArr: TJsonArray;
begin
  Json := TJsonObject.ParseJSONValue(JsonResponse) as TJsonObject;
  try
    if Assigned(Json) then
    begin
      DataArr := TJsonArray(Json.Get('data').JsonValue);
      Result := TJSONPair(TJSONObject(DataArr.Items[0]).Get('b64_json')).JsonValue.Value;
    end
    else
      Result := '';
  finally
    Json.Free;
  end;
end;


function TChatGPT.GetJSONWithImage(const Prompt: string; ResponseFormat: Integer): string;
var
  JObj: TJsonObject;
  Request: string;
  ResponseContent, StringStream: TStringStream;
  MultipartFormData: TMultipartFormData;
  MyHeaders: TArray;
begin
  JObj := nil;
  MultipartFormData := nil;
  ResponseContent := nil;
  StringStream := nil;
  try
    SetLength(MyHeaders, 2);
    MyHeaders[0] := TNameValuePair.Create('Authorization', FOpenAIApiKey);
    MyHeaders[1] := TNameValuePair.Create('Content-Type', 'application/json');
    JObj := TJSONObject.Create;
    with JObj do
    begin
      Owned := False;
      AddPair('model', 'dall-e-2');
      if ResponseFormat = 1 then
        AddPair('response_format','b64_json')
      else
        AddPair('response_format','url');
      AddPair('prompt', Prompt);
      AddPair('n', TJSONNumber.Create(1));
      AddPair('size', '1024x1024');
    end;
    Request := Jobj.ToString;
    MultipartFormData := TMultipartFormData.Create;
    StringStream := TStringStream.Create(Request, TEncoding.UTF8);
    MultipartFormData.Stream.LoadFromStream(StringStream);
    ResponseContent := TStringStream.Create;
    FNetHttpClient.Post('https://api.openai.com/v1/images/generations',
      MultiPartFormData, ResponseContent, MyHeaders);
    Result := ResponseContent.DataString;
  finally
    JObj.Free;
    MultipartFormData.Free;
    ResponseContent.Free;
    StringStream.Free;
  end;
end;

class function TChatGPT.MessageContentFromChatGPT(const JsonAnswer: string): string;
var
  Mes: TJsonArray;
  JsonResp: TJsonObject;
begin
  JsonResp := nil;
  try
    JsonResp := TJsonObject.ParseJSONValue(JsonAnswer) as TJsonObject;
    if Assigned(JsonResp) then
    begin
      Mes := TJsonArray(JsonResp.Get('choices').JsonValue);
      Result := TJsonObject(TJsonObject(Mes.Get(0)).Get('message').JsonValue).GetValue('content').Value;
    end
    else
      Result := '';
  finally
    JsonResp.Free;
  end;
end;

function TChatGPT.SendTextToChatGPT(const Text: string): string;
var
  JArr: TJsonArray;
  JObj, JObjOut: TJsonObject;
  Request: string;
  ResponseContent, StringStream: TStringStream;
  MultipartFormData: TMultipartFormData;
  Headers: TArray;
  I: Integer;
begin
  JArr := nil;
  JObj := nil;
  JObjOut := nil;
  MultipartFormData := nil;
  ResponseContent := nil;
  StringStream := nil;
  try
    SetLength(Headers, 2);
    Headers[0] := TNameValuePair.Create('Authorization', FOpenAIApiKey);
    Headers[1] := TNameValuePair.Create('Content-Type', 'application/json');
    JObj := TJsonObject.Create;
    JObj.Owned := False;
    JObj.AddPair('role', 'user');
    JArr := TJsonArray.Create;
    JArr.AddElement(JObj);
    Self.FText := Text;
    JObj.AddPair('content', FText);
    JObjOut := TJsonObject.Create;
    JObjOut.AddPair('model', 'gpt-3.5-turbo');
    JObjOut.AddPair('messages', Trim(JArr.ToString));
    JObjOut.AddPair('temperature', TJSONNumber.Create(0.7));
    Request := JObjOut.ToString.Replace('', '');
    for I := 0 to Length(Request) - 1 do
    begin
      if ((Request[I] = '"') and (Request[I + 1] = '[')) or
        ((Request[I] = '"') and (Request[I - 1] = ']')) then
      begin
        Request[I] := ' ';
      end;
    end;
    ResponseContent := TStringStream.Create;
    MultipartFormData := TMultipartFormData.Create;
    StringStream := TStringStream.Create(Request, TEncoding.UTF8);
    MultipartFormData.Stream.LoadFromStream(StringStream);
    FNetHttpClient.Post('https://api.openai.com/v1/chat/completions',
      MultipartFormData, ResponseContent, Headers);
    Result := FormatJSON(ResponseContent.DataString);
  finally
    StringStream.Free;
    ResponseContent.Free;
    MultipartFormData.Free;
    JObjOut.Free;
    JArr.Free;
    JObj.Free;
  end;
end;

end.

上記のDelphi FMXアプリケーションの例では、TNetHttpClientコンポーネントを使用してOpenAI APIと連携し、特にOpenAIにPOSTリクエストを送信します。

tnethttpclientcomponent

OpenAIによって生成され、メディアファイル(MP3形式)に保存された音声をDelphi FMXアプリケーションで再生するには、TMediaPlayerコンポーネントを使用します。

tmediaplayercomponent

保存されたメディアファイルをOpenAIにリクエストして、その中の音声からテキストを抽出するために、TRESTClient、TRESTRequest、THTTPBasicAuthenticator の 3 つのコンポーネントを使用します。

trestclient_trestrequest_thttpbasicauthenticator_components

これらのコンポーネントに追加の設定は必要ありません。TRESTClientとTRESTRequestは、POSTリクエストを行い、メディアファイルから抽出した音声テキストをOpenAIからデータを取得するために使用されます。THTTPBasicAuthenticator は秘密鍵を用いた認証に使用されます。

そして音声生成のためのテキスト説明を入力するために、TMemoコンポーネントを使用します。

tmemocomponentforusersrequest

またTMemo コンポーネントを使用して、メディア ファイル内の音声から抽出されたテキストを表示します。

メイン フォームの onCreate メソッドで、OpenAI によって生成された音声が保存されるメディア ファイルへのパスを FAudioFilePath フィールドに割り当てる必要があります。 また、秘密鍵の値を FOpenAIApiKey フィールドに割り当てます。

mainformoncreatemethod

Delphi FMXアプリケーションで、テキスト記述に基づく音声生成とメディアファイルへの保存、再生機能は、”Send Request For Speech Generation “ボタンのonClickハンドラに実装されています。このハンドラでは、音声生成のためにテキスト記述をOpenAIに渡すためのオブジェクトGPTHelper(IChatGPTHelper型)と、生成された音声をTMemoryStreamとして保存するためのImageStream(TMemoryStreamクラス)を宣言します。

gpthelperandimagestream

次に、TChatGPTクラスのコンストラクタを呼び出し、NetHttpClient1や秘密鍵(FOpenAIApiKey)などのオブジェクトを渡します。そしてGetGeneratedSpeechAsStreamメソッドを呼び出し、生成される音声のテキスト説明 (Memo2.Text)や音声タイプ(この例では文字列「onyx」)などのパラメータを指定します。リクエストの実行中にアプリケーションインターフェースがブロックされるのを防ぐため、TTask.Runを使用します。GetGeneratedSpeechAsStreamメソッドの実行結果、つまり生成された音声はImageStreamに保存されます。

usersrequestforspeechgeneration-2

アプリケーションのメインスレッドでは、TThread.Synchronizeを使用して、ImageStream SaveToFileメソッドを使用して音声をMP3メディアファイルに保存します。このプロセスでは、FileExists関数を使用して、指定されたパスにファイルが存在するかどうかを確認します。ファイルが存在する場合は、DeleteFile関数を使用して削除する必要があります。保存後、TMediaPlayer(MediaPlayer1.Play)を使用してDelphi FMXアプリケーションでメディアファイルを再生します。そのためには、メディアファイルのパス(MediaPlayer1.FileName)を指定する必要があります。

playingourmediafilewithspeechbytmediaplayer

以下は、 “Send Request For Speech Generation “ボタンのOnClickハンドラの実装コードの例です。

procedure TForm1.Button1Click(Sender: TObject);
var
  GPTHelper: IChatGPTHelper;
  ImageStream: TMemoryStream;
begin
  TTask.Run(
    procedure
    begin
      GPTHelper := TChatGPT.Create(NetHTTPClient1, 'Bearer ' + FOpenAIApiKey);
      ImageStream := GPTHelper.GetGeneratedSpeechAsStream(Memo2.Text, 'onyx');
      try
        TThread.Synchronize(nil,
          procedure
          begin
            if FileExists(FAudioFilePath) then
            begin
              DeleteFile(FAudioFilePath);
              ImageStream.SaveToFile(FAudioFilePath);
            end
            else
              ImageStream.SaveToFile(FAudioFilePath);
            MediaPlayer1.FileName := FAudioFilePath;
            MediaPlayer1.Play;
            ShowMessage('All is done!!!');
          end);
      finally
        ImageStream.Free;
      end;
    end);
end;

それでは、保存したメディアファイルの音声からテキストを抽出してみましょう。この機能は、”Speech From Audio File To Text “ボタンのonClickハンドラで実装します。ハンドラの中で、テキスト抽出のためにメディアファイルをOpenAIに渡すためのオブジェクトGPTHelper(IChatGPTHelper)型を宣言します。またメディアファイルから抽出したテキストを格納する文字列変数 Text を宣言します。

gpthelperandtext

次に4 つの入力パラメータ(HTTPBasicAuthenticator1、RESTClient1、RESTRequest1、FOpenAIApiKey)を指定して、コンストラクタの 2 番目のバリアントを呼び出します。そしてGetGeneratedTextFromSpeech メソッドを呼び出して、メディア ファイルへのパスを渡します。このメソッドは、メディアファイル内の音声から抽出されたテキストを返します。最後に受信したテキストをTMemo(Memo1.Text)で表示します。

generatedtextfrommediafile

以下は、 “Speech From Audio File To Text “ボタンのOnClickハンドラの実装コードの例です。

procedure TForm1.Button4Click(Sender: TObject);
var
  GPTHelper: IChatGPTHelper;
  Text: string;
begin
  TTask.Run(
    procedure
    begin
      GPTHelper := TChatGPT.Create(HTTPBasicAuthenticator1,
        RESTClient1, RESTRequest1, FOpenAIApiKey);
      Text := GPTHelper.GetGeneratedTextFromSpeech(FAudioFilePath);
      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Text := Text;
          ShowMessage('All is done!!!');
        end);
    end);
end;

それではDelphi FMXアプリケーションを実行してみましょう。まず、テキストの説明に基づいて、音声を生成します。音声はTMediaPlayerを使用して再生され、拡張子がmp3のメディアファイルに保存されます。

speechgenerationdemonstration

このDelphi FMXアプリケーションでは、メディアファイルは “Documents “ディレクトリに保存されます。

generatedspeechsavedtomediafile

最後に、このアプリケーションを使用して、メディアファイルに保存された音声をテキストに変換してみましょう。

textformspeechgenerationdemonstration

About SOFTACOM

SOFTACOMは、15 年以上にわたり、お客様に対してビジネスソフトウェアやモバイル、Web、デスクトップ アプリの開発、強化、サポート、マイグレーション、最新化を支援してきたエンバカデロのテクノロジーパートナーです。

https://www.softacom.com

Kai for RAD Studio is Now Available! Special Live Webinar: Introducing Kai - A New Chapter for RAD Studio RAD Studio 13.1 Florence Now Available

Reduce development time and get to market faster with RAD Studio, Delphi, or C++Builder.
Design. Code. Compile. Deploy.

Start Free Trial   Upgrade Today

   Free Delphi Community Edition   Free C++Builder Community Edition

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

IN THE ARTICLES