diff --git a/Siv3D/include/Siv3D/OpenAI/Speech.hpp b/Siv3D/include/Siv3D/OpenAI/Speech.hpp index c65a8a740..2344e589c 100644 --- a/Siv3D/include/Siv3D/OpenAI/Speech.hpp +++ b/Siv3D/include/Siv3D/OpenAI/Speech.hpp @@ -19,20 +19,68 @@ namespace s3d { namespace Speech { + namespace Model + { + /// @brief 音声合成モデル tts-1 | Speech Synthesis Model tts-1 + /// @see https://platform.openai.com/docs/models/tts + inline constexpr StringView TTS1{ U"tts-1" }; + + /// @brief 音声合成モデル tts-1-hd | Speech Synthesis Model tts-1-hd + /// @see https://platform.openai.com/docs/models/tts + /// @remark tts-1 よりも高品質な音声を生成します。 | Generates higher quality audio than tts-1. + inline constexpr StringView TTS1HD{ U"tts-1-hd" }; + } + + namespace Voice + { + inline constexpr StringView Alloy{ U"alloy" }; + + inline constexpr StringView Echo{ U"echo" }; + + inline constexpr StringView Fable{ U"fable" }; + + inline constexpr StringView Onyx{ U"onyx" }; + + inline constexpr StringView Nova{ U"nova" }; + + inline constexpr StringView Shimmer{ U"shimmer" }; + } + + namespace ResponseFormat + { + inline constexpr StringView MP3{ U"mp3" }; + + inline constexpr StringView Opus{ U"opus" }; + + inline constexpr StringView AAC{ U"aac" }; + + inline constexpr StringView FLAC{ U"flac" }; + } + + inline constexpr size_t MaxInputLength = 4096; + + inline constexpr double MinSpeed = 0.25; + + inline constexpr double DefaultSpeed = 1.0; + + inline constexpr double MaxSpeed = 4.0; + struct Request { - String model = U"tts-1"; + String model{ OpenAI::Speech::Model::TTS1 }; String input; - String voice = U"alloy"; + String voice{ OpenAI::Speech::Voice::Alloy }; - String responseFormat = U"mp3"; + String responseFormat{ OpenAI::Speech::ResponseFormat::MP3 }; - double speed = 1.0; + double speed = OpenAI::Speech::DefaultSpeed; }; - bool Create(const Request& request, FilePathView path); + bool Create(StringView apiKey, const Request& request, FilePathView path); + + AsyncTask CreateAsync(StringView apiKey, const Request& request, FilePathView path); } } } diff --git a/Siv3D/src/Siv3D/OpenAI/OpenAICommon.hpp b/Siv3D/src/Siv3D/OpenAI/OpenAICommon.hpp index 174f257a9..a53a79124 100644 --- a/Siv3D/src/Siv3D/OpenAI/OpenAICommon.hpp +++ b/Siv3D/src/Siv3D/OpenAI/OpenAICommon.hpp @@ -11,6 +11,7 @@ # pragma once # include +# include # include namespace s3d @@ -20,5 +21,8 @@ namespace s3d // HTTP ヘッダーを作成する [[nodiscard]] HashTable MakeHeaders(StringView apiKey); + + // 非同期タスクのポーリング間隔 + constexpr Milliseconds TaskPollingInterval{ 5 }; } } diff --git a/Siv3D/src/Siv3D/OpenAI/SivOpenAIChat.cpp b/Siv3D/src/Siv3D/OpenAI/SivOpenAIChat.cpp index a11a44efc..abadb021b 100644 --- a/Siv3D/src/Siv3D/OpenAI/SivOpenAIChat.cpp +++ b/Siv3D/src/Siv3D/OpenAI/SivOpenAIChat.cpp @@ -21,7 +21,7 @@ namespace s3d namespace detail { // OpenAI のチャット API の URL - constexpr URLView ChatV1URL = U"https://api.openai.com/v1/chat/completions"; + constexpr URLView ChatCompletionsEndpoint = U"https://api.openai.com/v1/chat/completions"; // チャット API に送信するリクエストを作成する [[nodiscard]] @@ -77,7 +77,7 @@ namespace s3d MemoryWriter memoryWriter; - if (const auto response = SimpleHTTP::Post(detail::ChatV1URL, headers, data.data(), data.size(), memoryWriter)) + if (const auto response = SimpleHTTP::Post(detail::ChatCompletionsEndpoint, headers, data.data(), data.size(), memoryWriter)) { if (const HTTPStatusCode statusCode = response.getStatusCode(); statusCode == HTTPStatusCode::OK) @@ -120,7 +120,7 @@ namespace s3d const auto headers = detail::MakeHeaders(apiKey); - return SimpleHTTP::PostAsync(detail::ChatV1URL, headers, data.data(), data.size()); + return SimpleHTTP::PostAsync(detail::ChatCompletionsEndpoint, headers, data.data(), data.size()); } String GetContent(const JSON& response) diff --git a/Siv3D/src/Siv3D/OpenAI/SivOpenAIEmbedding.cpp b/Siv3D/src/Siv3D/OpenAI/SivOpenAIEmbedding.cpp index 7e00fb8e2..674dc0a21 100644 --- a/Siv3D/src/Siv3D/OpenAI/SivOpenAIEmbedding.cpp +++ b/Siv3D/src/Siv3D/OpenAI/SivOpenAIEmbedding.cpp @@ -22,7 +22,7 @@ namespace s3d namespace detail { // OpenAI の Embeddings API の URL - constexpr URLView EmbeddingsV1URL = U"https://api.openai.com/v1/embeddings"; + constexpr URLView EmbeddingsEndpoint = U"https://api.openai.com/v1/embeddings"; // Embeddings API に送信するリクエストを作成する [[nodiscard]] @@ -113,7 +113,7 @@ namespace s3d MemoryWriter memoryWriter; - if (const auto response = SimpleHTTP::Post(detail::EmbeddingsV1URL, headers, data.data(), data.size(), memoryWriter)) + if (const auto response = SimpleHTTP::Post(detail::EmbeddingsEndpoint, headers, data.data(), data.size(), memoryWriter)) { if (const HTTPStatusCode statusCode = response.getStatusCode(); statusCode == HTTPStatusCode::OK) @@ -151,7 +151,7 @@ namespace s3d const auto headers = detail::MakeHeaders(apiKey); - return SimpleHTTP::PostAsync(detail::EmbeddingsV1URL, headers, data.data(), data.size()); + return SimpleHTTP::PostAsync(detail::EmbeddingsEndpoint, headers, data.data(), data.size()); } Array GetVector(const JSON& response) diff --git a/Siv3D/src/Siv3D/OpenAI/SivOpenAIImage.cpp b/Siv3D/src/Siv3D/OpenAI/SivOpenAIImage.cpp index 1bd7047ff..cebfb73e6 100644 --- a/Siv3D/src/Siv3D/OpenAI/SivOpenAIImage.cpp +++ b/Siv3D/src/Siv3D/OpenAI/SivOpenAIImage.cpp @@ -11,7 +11,6 @@ # include # include -# include # include # include # include @@ -22,10 +21,7 @@ namespace s3d namespace detail { // OpenAI の画像生成 API の URL - constexpr URLView ImageGenerationV1URL = U"https://api.openai.com/v1/images/generations"; - - // 非同期タスクのポーリング間隔 - constexpr Milliseconds TaskPollingInterval{ 5 }; + constexpr URLView ImageGenerationsEndpoint = U"https://api.openai.com/v1/images/generations"; // 画像生成 API に送信するリクエストを作成する [[nodiscard]] @@ -59,7 +55,7 @@ namespace s3d const auto headers = detail::MakeHeaders(apiKey); // 画像生成 API に非同期でリクエストを送信する - AsyncHTTPTask task1 = SimpleHTTP::PostAsync(detail::ImageGenerationV1URL, headers, data.data(), data.size()); + AsyncHTTPTask task1 = SimpleHTTP::PostAsync(detail::ImageGenerationsEndpoint, headers, data.data(), data.size()); // タスクが完了するまでポーリングする while (not task1.isReady()) diff --git a/Siv3D/src/Siv3D/OpenAI/SivOpenAISpeech.cpp b/Siv3D/src/Siv3D/OpenAI/SivOpenAISpeech.cpp index cb131f6c7..d4601494a 100644 --- a/Siv3D/src/Siv3D/OpenAI/SivOpenAISpeech.cpp +++ b/Siv3D/src/Siv3D/OpenAI/SivOpenAISpeech.cpp @@ -10,8 +10,70 @@ //----------------------------------------------- # include +# include +# include +# include "OpenAICommon.hpp" namespace s3d { + namespace detail + { + constexpr URLView TextToSpeechEndpoint = U"https://api.openai.com/v1/audio/speech"; + [[nodiscard]] + static std::string MakeSpeechRequest(const OpenAI::Speech::Request& request) + { + JSON json; + json[U"model"] = request.model; + json[U"input"] = request.input; + json[U"voice"] = request.voice; + json[U"response_format"] = request.responseFormat; + json[U"speed"] = request.speed; + return json.formatUTF8(); + } + + static bool CreateSpeechImpl(const String apiKey, const OpenAI::Speech::Request request, const FilePath path) + { + // API キーが空の文字列である場合は失敗 + if (apiKey.isEmpty()) + { + return false; + } + + const auto headers = MakeHeaders(apiKey); + + const std::string data = MakeSpeechRequest(request); + + AsyncHTTPTask task = SimpleHTTP::PostAsync(TextToSpeechEndpoint, headers, data.data(), data.size(), path); + + // タスクが完了するまでポーリングする + while (not task.isReady()) + { + System::Sleep(TaskPollingInterval); + } + + if (not task.getResponse().isOK()) + { + return false; + } + + return true; + } + } + + namespace OpenAI + { + namespace Speech + { + bool Create(const StringView apiKey, const Request& request, const FilePathView path) + { + return detail::CreateSpeechImpl(String{ apiKey }, request, FilePath{ path }); + } + + AsyncTask CreateAsync(const StringView apiKey, const Request& request, const FilePathView path) + { + return Async(detail::CreateSpeechImpl, String{ apiKey }, request, FilePath{ path }); + } + } + } }