このブログ記事は、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 アプリケーションのメディア ファイル内のテキスト記述と音声からのテキスト抽出に基づく音声生成の実装
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ハンドラの実装コードの例です。
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ハンドラの実装コードの例です。
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、デスクトップ アプリの開発、強化、サポート、マイグレーション、最新化を支援してきたエンバカデロのテクノロジーパートナーです。
Reduce development time and get to market faster with RAD Studio, Delphi, or C++Builder.
Design. Code. Compile. Deploy.
Free Delphi Community Edition Free C++Builder Community Edition





