121 lines
3.9 KiB
PHP
121 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Closure;
|
|
use CURLFile;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Streams large audio uploads to Whisper with curl (disk → network),
|
|
* and delivers response body chunks without buffering the request in PHP.
|
|
*
|
|
* Laravel Http's stream => 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);
|
|
}
|
|
}
|
|
}
|