Stream Whisper uploads with curl so large audio does not OOM.

This commit is contained in:
ben
2026-08-13 20:29:59 +02:00
parent 1898be4def
commit e77a18c49f
7 changed files with 488 additions and 42 deletions
@@ -0,0 +1,119 @@
<?php
namespace Tests\Feature;
use App\Models\Recording;
use App\Models\User;
use App\Services\TranscriptionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class LocalWhisperCurlTransportTest extends TestCase
{
use RefreshDatabase;
public function test_curl_transport_streams_sse_from_real_http_endpoint(): void
{
if (! function_exists('curl_init')) {
$this->markTestSkipped('curl extension required');
}
Storage::fake('local');
Storage::disk('local')->put('recordings/large.mp3', str_repeat('A', 2_000_000));
$stub = base_path('tests/fixtures/whisper-sse-stub.php');
$host = '127.0.0.1';
$port = $this->reservePort($host);
$docRoot = base_path('tests/fixtures');
$server = Process::timeout(30)
->path($docRoot)
->start([
PHP_BINARY,
'-S',
"{$host}:{$port}",
'whisper-sse-stub.php',
]);
try {
$this->waitForServer($host, $port);
config([
'ai.local_whisper_transport' => 'curl',
'ai.providers.local-whisper.url' => "http://{$host}:{$port}/v1",
'ai.providers.local-whisper.key' => 'test-key',
]);
$user = User::factory()->create();
$recording = Recording::query()->create([
'user_id' => $user->id,
'title' => 'Curl transport',
'original_filename' => 'large.mp3',
'file_path' => 'recordings/large.mp3',
'file_size_bytes' => 2_000_000,
'duration_seconds' => 40,
'transcription_status' => 'processing',
'transcription_driver' => 'local',
]);
$partials = [];
$text = app(TranscriptionService::class)->transcribe(
$recording,
function (string $message, int $percent, ?string $partial = null) use (&$partials): void {
if ($partial !== null) {
$partials[] = $partial;
}
},
);
$this->assertSame('Hello from whisper.', $text);
$this->assertContains('Hello ', $partials);
$this->assertContains('Hello from whisper.', $partials);
} finally {
$server->signal(SIGTERM);
$server->wait();
}
}
private function reservePort(string $host): int
{
$socket = stream_socket_server("tcp://{$host}:0");
if ($socket === false) {
$this->fail('Unable to reserve a local TCP port for the Whisper stub.');
}
$name = stream_socket_get_name($socket, false);
fclose($socket);
if (! is_string($name) || ! str_contains($name, ':')) {
$this->fail('Unable to determine reserved port.');
}
return (int) substr($name, strrpos($name, ':') + 1);
}
private function waitForServer(string $host, int $port): void
{
$deadline = microtime(true) + 5;
while (microtime(true) < $deadline) {
$errno = 0;
$errstr = '';
$fp = @fsockopen($host, $port, $errno, $errstr, 0.2);
if (is_resource($fp)) {
fclose($fp);
return;
}
usleep(50_000);
}
$this->fail("Whisper stub server did not start on {$host}:{$port}");
}
}