getConfig()->getOutputMimeType(); if ($mimeType !== null && $mimeType !== '' && $mimeType !== self::MIME_TYPE) { throw new InvalidArgumentException( sprintf('deAPI text to speech only produces audio/mpeg, not "%s".', $mimeType) ); } $request = new Request( HttpMethodEnum::POST(), DeApiProvider::url('audio/speech'), ['Content-Type' => 'application/json'], $this->prepareConvertParams($prompt), DeApiProvider::requestOptions($this->getRequestOptions()) ); $request = $this->getRequestAuthentication()->authenticateRequest($request); $response = $this->getHttpTransporter()->send($request); ResponseUtil::throwIfNotSuccessful($response); return $this->parseResponseToGenerativeAiResult($response); } /** * Prepares the request parameters. * * @since 0.1.0 * * @param list $prompt The prompt with the text to speak. * @return array The request parameters. * @throws InvalidArgumentException If a custom option conflicts with an existing parameter. */ protected function prepareConvertParams(array $prompt): array { $voice = $this->getConfig()->getOutputSpeechVoice(); $params = [ 'model' => $this->metadata()->getId(), 'input' => $this->preparePromptText($prompt), 'voice' => ($voice === null || $voice === '') ? self::DEFAULT_VOICE : $voice, 'response_format' => 'mp3', ]; // Custom options, for example `speed` (Kokoro: 0.5-2.0) or `language`. foreach ($this->getConfig()->getCustomOptions() as $key => $value) { if (isset($params[$key])) { throw new InvalidArgumentException( sprintf('The custom option "%s" conflicts with an existing parameter.', $key) ); } $params[$key] = $value; } return $params; } /** * Joins all text parts of the prompt. * * @since 0.1.0 * * @param list $prompt The prompt messages. * @return string The text to convert. * @throws InvalidArgumentException If the prompt has no text. */ protected function preparePromptText(array $prompt): string { $text = ''; foreach ($prompt as $message) { foreach ($message->getParts() as $part) { $partText = $part->getText(); if ($partText !== null) { $text .= ($text === '' ? '' : "\n") . $partText; } } } if ($text === '') { throw new InvalidArgumentException('The prompt must contain text to convert to speech.'); } return $text; } /** * Wraps the audio bytes of the response in a result. * * @since 0.1.0 * * @param Response $response The response with the MP3 body. * @return GenerativeAiResult The result. */ protected function parseResponseToGenerativeAiResult(Response $response): GenerativeAiResult { $body = $response->getBody(); if ($body === null || $body === '') { throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'body'); } $audioFile = new File(base64_encode($body), self::MIME_TYPE); $message = new Message(MessageRoleEnum::model(), [new MessagePart($audioFile)]); $candidate = new Candidate($message, FinishReasonEnum::stop()); return new GenerativeAiResult( uniqid('deapi-tts-', true), [$candidate], new TokenUsage(0, 0, 0), $this->providerMetadata(), $this->metadata() ); } }