true uses Guzzle's StreamHandler, which casts the * multipart body to a string and OOMs on big recordings. */ class LocalWhisperCurlClient { /** * POST /audio/transcriptions and stream the response body. * * @param Closure(string): bool $onBodyChunk Return false to abort the transfer. * @param Closure(string): void|null $onContentType Invoked when the response Content-Type header arrives. * @return array{status: int, content_type: string} */ public function streamTranscription( string $url, string $apiKey, string $path, string $filename, string $mimeType, string $model, int $timeout, Closure $onBodyChunk, ?Closure $onContentType = null, ): array { if (! function_exists('curl_init')) { throw new RuntimeException('The curl PHP extension is required for local Whisper transcription.'); } $contentType = ''; $abort = false; $handle = curl_init($url); if ($handle === false) { throw new RuntimeException('Unable to initialize curl for local Whisper.'); } try { curl_setopt_array($handle, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer '.$apiKey, 'Accept: application/json, text/event-stream', ], CURLOPT_POSTFIELDS => [ 'file' => new CURLFile($path, $mimeType, $filename), 'model' => $model, 'response_format' => 'verbose_json', 'stream' => 'true', 'without_timestamps' => 'false', 'timestamp_granularities[]' => 'word', ], CURLOPT_RETURNTRANSFER => false, CURLOPT_HEADER => false, CURLOPT_TIMEOUT => max(1, $timeout), CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_HEADERFUNCTION => static function ($ch, string $header) use (&$contentType, $onContentType): int { if (stripos($header, 'Content-Type:') === 0) { $contentType = trim(substr($header, strlen('Content-Type:'))); if ($onContentType !== null) { $onContentType($contentType); } } return strlen($header); }, CURLOPT_WRITEFUNCTION => static function ($ch, string $chunk) use ($onBodyChunk, &$abort): int { if ($abort) { return 0; } if ($onBodyChunk($chunk) === false) { $abort = true; return 0; } return strlen($chunk); }, ]); $ok = curl_exec($handle); $errno = curl_errno($handle); $error = curl_error($handle); $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); if ($abort) { return [ 'status' => $status > 0 ? $status : 499, 'content_type' => $contentType, ]; } if ($ok === false && $errno !== 0) { throw new RuntimeException( 'Local transcription request failed: '.$error.' (curl '.$errno.')' ); } return [ 'status' => $status, 'content_type' => $contentType, ]; } finally { curl_close($handle); } } }