このブログ記事は、Softacom Informationの「How To Use OpenAI To Add Realistic Text-To-Speech To Your Apps」の抄訳です
OpenAI を使用して、クロスプラットフォーム アプリでテキストからリアルな音声を生成したいですか? 本ブログでは、TechパートナーであるSoftacom の記事を通してその方法について詳しく紹介いたします。
アプリにリアルなテキスト読み上げ(音声合成)を追加するOpenAIの使用方法の概要
最新の人工知能ベースのサービスにより、音声の生成と音声からテキストへの変換が可能になります。しかも、さまざまな言語に対応しています。簡単にテキストサービスへ入力でき、合成された音声出力を受け取ることができます。利用可能な設定のおかげで、生成される音声のタイプを選択することもできます。
さらに、音声をテキストに変換することも可能です。例えば、お気に入りのアーティストのMP3トラックから曲をテキストに書き起こすことができます。
このブログでは、Delphi FMXアプリケーションで、テキスト記述に基づく音声生成のためのOpenAI APIの機能を分析し、さらに音声からテキストを生成する方法を簡単な手順を交えて説明いたします。
OpenAI API の機能を音声生成やテキスト変換に使用するには、秘密鍵を登録して取得する必要があります。こちらの記事では、テキスト生成に特化した内容ですが、登録と秘密鍵 (API キー) を取得するプロセスが説明されています。
ユーザーのリクエストに基づいて音声を生成するには、OpenAI API ([Create speech] タブ) を利用します。
OpenAI APIは、音声生成のための広範な機能を提供しています。ここでは、音声の種類、出力メディアファイル形式(mp3、wavなど)、生成メディアファイルの音声速度、生成された音声のテキストによる説明、および使用する機械学習モデル(ts-1またはts-1-hd)を設定できます。
音声からテキストを抽出するには、OpenAI API (「Create transcription」タブ) も使用します。
OpenAI APIは、音声に基づいてテキストを生成するための豊富な機能も備えています。
ここでは、入力メディアファイルのタイプ(mp3、wavなど)と、OpenAIからのレスポンスのフォーマット(json、text、srt、verbose_json、vtt)を設定できます。
ユーザーのリクエストに基づいて音声を生成し、音声からテキストを生成するOpenAI API の使いやすさを向上させるために、テキスト生成に焦点を当てたこちらの記事で説明されており、以前に開発された TChatGPT クラスの機能を拡張しています。
GetGeneratedSpeechAsStream や GetGeneratedTextFromSpeech などのメソッドをクラスに追加しましょう。 また、メディアファイル内の音声からテキストを抽出できるようにするために、オーバーロードされたコンストラクタメソッド (Create) を追加します。
取得したバージョンの TChatGPTクラスのコンストラクタメソッドは、入力パラメータとして以下のオブジェクトを受け取ります: HttpBasicAuthenticator (THTTPBasicAuthenticator クラス)、RESTClient (TRESTClient クラス)、RESTRequest (TRESTRequest クラス)、および文字列定数 OpenAIApiKeyと 秘密鍵 です。
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の形式で取得されます。
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)
TChatGPTクラスの全ソースコードは以下の通りです。
Delphi FMX アプリケーションのメディア ファイル内のテキスト記述と音声からのテキスト抽出に基づく音声生成の実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
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リクエストを送信します。
OpenAIによって生成され、メディアファイル(MP3形式)に保存された音声をDelphi FMXアプリケーションで再生するには、TMediaPlayerコンポーネントを使用します。
保存されたメディアファイルをOpenAIにリクエストして、その中の音声からテキストを抽出するために、TRESTClient、TRESTRequest、THTTPBasicAuthenticator の 3 つのコンポーネントを使用します。
これらのコンポーネントに追加の設定は必要ありません。TRESTClientとTRESTRequestは、POSTリクエストを行い、メディアファイルから抽出した音声テキストをOpenAIからデータを取得するために使用されます。THTTPBasicAuthenticator は秘密鍵を用いた認証に使用されます。
そして音声生成のためのテキスト説明を入力するために、TMemoコンポーネントを使用します。
またTMemo コンポーネントを使用して、メディア ファイル内の音声から抽出されたテキストを表示します。
メイン フォームの onCreate メソッドで、OpenAI によって生成された音声が保存されるメディア ファイルへのパスを FAudioFilePath フィールドに割り当てる必要があります。 また、秘密鍵の値を FOpenAIApiKey フィールドに割り当てます。
Delphi FMXアプリケーションで、テキスト記述に基づく音声生成とメディアファイルへの保存、再生機能は、”Send Request For Speech Generation “ボタンのonClickハンドラに実装されています。このハンドラでは、音声生成のためにテキスト記述をOpenAIに渡すためのオブジェクトGPTHelper(IChatGPTHelper型)と、生成された音声をTMemoryStreamとして保存するためのImageStream(TMemoryStreamクラス)を宣言します。
次に、TChatGPTクラスのコンストラクタを呼び出し、NetHttpClient1や秘密鍵(FOpenAIApiKey)などのオブジェクトを渡します。そしてGetGeneratedSpeechAsStreamメソッドを呼び出し、生成される音声のテキスト説明 (Memo2.Text)や音声タイプ(この例では文字列「onyx」)などのパラメータを指定します。リクエストの実行中にアプリケーションインターフェースがブロックされるのを防ぐため、TTask.Runを使用します。GetGeneratedSpeechAsStreamメソッドの実行結果、つまり生成された音声はImageStreamに保存されます。
アプリケーションのメインスレッドでは、TThread.Synchronizeを使用して、ImageStream SaveToFileメソッドを使用して音声をMP3メディアファイルに保存します。このプロセスでは、FileExists関数を使用して、指定されたパスにファイルが存在するかどうかを確認します。ファイルが存在する場合は、DeleteFile関数を使用して削除する必要があります。保存後、TMediaPlayer(MediaPlayer1.Play)を使用してDelphi FMXアプリケーションでメディアファイルを再生します。そのためには、メディアファイルのパス(MediaPlayer1.FileName)を指定する必要があります。
以下は、 “Send Request For Speech Generation “ボタンのOnClickハンドラの実装コードの例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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 を宣言します。
次に4 つの入力パラメータ(HTTPBasicAuthenticator1、RESTClient1、RESTRequest1、FOpenAIApiKey)を指定して、コンストラクタの 2 番目のバリアントを呼び出します。そしてGetGeneratedTextFromSpeech メソッドを呼び出して、メディア ファイルへのパスを渡します。このメソッドは、メディアファイル内の音声から抽出されたテキストを返します。最後に受信したテキストをTMemo(Memo1.Text)で表示します。
以下は、 “Speech From Audio File To Text “ボタンのOnClickハンドラの実装コードの例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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のメディアファイルに保存されます。
このDelphi FMXアプリケーションでは、メディアファイルは “Documents “ディレクトリに保存されます。
最後に、このアプリケーションを使用して、メディアファイルに保存された音声をテキストに変換してみましょう。
About SOFTACOM
SOFTACOMは、15 年以上にわたり、お客様に対してビジネスソフトウェアやモバイル、Web、デスクトップ アプリの開発、強化、サポート、マイグレーション、最新化を支援してきたエンバカデロのテクノロジーパートナーです。
Design. Code. Compile. Deploy.
Start Free Trial Upgrade Today
Free Delphi Community Edition Free C++Builder Community Edition