Add live transcription progress and Docker Whisper.
Surface stage, percent, and elapsed time while jobs run, and ship Compose so local confidential transcription can use faster-whisper on port 8090.
This commit is contained in:
@@ -7,7 +7,6 @@ use App\Models\Recording;
|
||||
use App\Services\TranscriptionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Ai\Transcription;
|
||||
use Tests\TestCase;
|
||||
@@ -51,10 +50,41 @@ class RecordingUploadTest extends TestCase
|
||||
Storage::disk('local')->assertExists($recording->file_path);
|
||||
}
|
||||
|
||||
public function test_user_can_upload_wav_and_ogg(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
|
||||
foreach ([
|
||||
['memo.wav', 'audio/wav'],
|
||||
['clip.ogg', 'audio/ogg'],
|
||||
['talk.m4a', 'audio/mp4'],
|
||||
] as [$name, $mime]) {
|
||||
$response = $this->post(route('recordings.store'), [
|
||||
'audio' => UploadedFile::fake()->create($name, 400, $mime),
|
||||
'title' => $name,
|
||||
]);
|
||||
|
||||
$recording = Recording::query()->where('title', $name)->first();
|
||||
|
||||
$this->assertNotNull($recording, "Failed uploading {$name}");
|
||||
$response->assertRedirect(route('recordings.show', $recording));
|
||||
Storage::disk('local')->assertExists($recording->file_path);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_unsupported_audio_type_is_rejected(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
|
||||
$this->from(route('recordings.create'))
|
||||
->post(route('recordings.store'), [
|
||||
'audio' => UploadedFile::fake()->create('notes.txt', 10, 'text/plain'),
|
||||
])
|
||||
->assertSessionHasErrors('audio');
|
||||
}
|
||||
|
||||
public function test_user_can_queue_cloud_transcription(): void
|
||||
{
|
||||
Queue::fake();
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'title' => 'Dictation',
|
||||
'original_filename' => 'dictation.mp3',
|
||||
@@ -63,14 +93,18 @@ class RecordingUploadTest extends TestCase
|
||||
'transcription_status' => 'pending',
|
||||
]);
|
||||
|
||||
// Fake AI so the afterResponse job (sync) does not call a real provider.
|
||||
Transcription::fake(['Queued transcription text.']);
|
||||
|
||||
$this->post(route('recordings.transcribe', $recording), [
|
||||
'driver' => 'cloud',
|
||||
])->assertRedirect();
|
||||
|
||||
Queue::assertPushed(TranscribeRecording::class);
|
||||
|
||||
$recording->refresh();
|
||||
$this->assertSame('cloud', $recording->transcription_driver);
|
||||
$this->assertContains($recording->transcription_status, ['pending', 'processing', 'done']);
|
||||
$this->assertNotNull($recording->transcription_started_at);
|
||||
$this->assertNotNull($recording->transcription_progress);
|
||||
}
|
||||
|
||||
public function test_ollama_driver_requires_url(): void
|
||||
@@ -90,6 +124,29 @@ class RecordingUploadTest extends TestCase
|
||||
->assertSessionHasErrors('ollama_url');
|
||||
}
|
||||
|
||||
public function test_transcription_status_endpoint_returns_progress(): void
|
||||
{
|
||||
$recording = Recording::query()->create([
|
||||
'title' => 'Live status',
|
||||
'original_filename' => 'live.mp3',
|
||||
'file_path' => 'recordings/live.mp3',
|
||||
'file_size_bytes' => 100,
|
||||
'transcription_status' => 'processing',
|
||||
'transcription_progress' => 'Waiting for cloud transcript…',
|
||||
'transcription_percent' => 55,
|
||||
'transcription_driver' => 'cloud',
|
||||
'transcription_started_at' => now()->subSeconds(12),
|
||||
]);
|
||||
|
||||
$this->getJson(route('recordings.transcription-status', $recording))
|
||||
->assertOk()
|
||||
->assertJsonPath('status', 'processing')
|
||||
->assertJsonPath('progress', 'Waiting for cloud transcript…')
|
||||
->assertJsonPath('percent', 55)
|
||||
->assertJsonPath('is_active', true)
|
||||
->assertJsonPath('driver_label', 'Cloud (OpenAI Whisper)');
|
||||
}
|
||||
|
||||
public function test_transcription_job_stores_transcript(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
@@ -111,6 +168,38 @@ class RecordingUploadTest extends TestCase
|
||||
$recording->refresh();
|
||||
$this->assertSame('done', $recording->transcription_status);
|
||||
$this->assertSame('Hello from the recorder.', $recording->transcript);
|
||||
$this->assertSame(100, $recording->transcription_percent);
|
||||
$this->assertSame('Transcription complete', $recording->transcription_progress);
|
||||
$this->assertNotNull($recording->transcribed_at);
|
||||
}
|
||||
|
||||
public function test_transcription_job_stores_error_on_failure(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Storage::disk('local')->put('recordings/bad.mp3', 'fake-audio-bytes');
|
||||
|
||||
Transcription::fake(function () {
|
||||
throw new \RuntimeException('Provider unavailable');
|
||||
});
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'title' => 'Bad',
|
||||
'original_filename' => 'bad.mp3',
|
||||
'file_path' => 'recordings/bad.mp3',
|
||||
'file_size_bytes' => 12,
|
||||
'transcription_status' => 'pending',
|
||||
'transcription_driver' => 'cloud',
|
||||
]);
|
||||
|
||||
try {
|
||||
(new TranscribeRecording($recording))->handle(app(TranscriptionService::class));
|
||||
$this->fail('Expected transcription to throw');
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->assertSame('Provider unavailable', $e->getMessage());
|
||||
}
|
||||
|
||||
$recording->refresh();
|
||||
$this->assertSame('failed', $recording->transcription_status);
|
||||
$this->assertSame('Provider unavailable', $recording->transcription_error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user