From 18cce03f148b7239b77b80624db973851824a677 Mon Sep 17 00:00:00 2001
From: fiona-neena
Date: Tue, 14 Apr 2026 12:01:59 +0000
Subject: [PATCH] Add local Whisper transcription and transcript UX fixes
---
.env.example | 12 +++
.gitignore | 2 +
README.md | 40 ++++++++-
.../DownloadTranscriptController.php | 43 +++++++++-
.../NewTranscriptionController.php | 9 ++-
.../Controllers/TranscribeAudioController.php | 34 ++++++--
.../TranslateTranscriptController.php | 12 +--
app/Jobs/TranscribeFileJob.php | 48 ++++++++---
app/Jobs/TranslateTranscriptJob.php | 23 ++++++
app/Policies/TranscriptPolicy.php | 6 +-
app/Support/LocalWhisperTranscriber.php | 69 ++++++++++++++++
app/Support/MockTranscriptFactory.php | 63 +++++++++++++++
config/writeout.php | 45 +++++++++++
resources/views/errors/403.blade.php | 12 ++-
resources/views/layouts/app.blade.php | 4 +-
resources/views/transcribe.blade.php | 75 +++++++++++++++--
resources/views/transcript.blade.php | 22 +++--
resources/views/welcome.blade.php | 36 ++++++---
routes/web.php | 2 +-
scripts/local_whisper_transcribe.py | 81 +++++++++++++++++++
20 files changed, 580 insertions(+), 58 deletions(-)
create mode 100644 app/Support/LocalWhisperTranscriber.php
create mode 100644 app/Support/MockTranscriptFactory.php
create mode 100644 scripts/local_whisper_transcribe.py
diff --git a/.env.example b/.env.example
index f1af0d5..856dc4b 100644
--- a/.env.example
+++ b/.env.example
@@ -21,6 +21,18 @@ FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
+TRANSCRIPT_STORAGE_DISK=public
+WRITEOUT_ALLOW_GUEST_UPLOADS=true
+WRITEOUT_MOCK_AI=true
+WRITEOUT_TRANSCRIPTION_DRIVER=local_whisper
+WRITEOUT_TRANSLATION_DRIVER=mock
+WRITEOUT_LOCAL_WHISPER_COMMAND=py
+WRITEOUT_LOCAL_WHISPER_MODEL=tiny
+WRITEOUT_LOCAL_WHISPER_DEVICE=cpu
+WRITEOUT_LOCAL_WHISPER_COMPUTE_TYPE=int8
+HF_HUB_DISABLE_SYMLINKS_WARNING=1
+HF_HUB_DISABLE_PROGRESS_BARS=1
+# HF_TOKEN=
MEMCACHED_HOST=127.0.0.1
diff --git a/.gitignore b/.gitignore
index f0d10af..951a84d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,5 @@ yarn-error.log
/.fleet
/.idea
/.vscode
+__pycache__/
+*.pyc
diff --git a/README.md b/README.md
index 39427f0..cb762c6 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,45 @@ the prompt context limit.
git clone https://github.com/beyondcode/writeout.ai
```
-### Create an OpenAI account and link your API key.
+### Configure your environment
+
+1. Copy `.env.example` to `.env`
+2. Add your `OPENAI_API_KEY`
+3. Keep `QUEUE_CONNECTION=sync` for the simplest local setup
+4. Keep `TRANSCRIPT_STORAGE_DISK=public` unless you already have a DigitalOcean Spaces bucket configured
+5. Keep `WRITEOUT_ALLOW_GUEST_UPLOADS=true` if you want to test uploads locally without GitHub OAuth
+6. Set `WRITEOUT_TRANSCRIPTION_DRIVER=local_whisper` if you want real local transcription without OpenAI billing
+7. Keep `WRITEOUT_TRANSLATION_DRIVER=mock` if you only need real transcription locally and do not want local translation work yet
+
+### Prepare the app
+
+```bash
+composer install
+php artisan key:generate
+php artisan migrate
+php artisan storage:link
+```
+
+### Start the app
+
+```bash
+php artisan serve
+```
+
+When guest uploads are enabled, local uploads are stored on the `public` disk and made public automatically so you can reopen the transcript without signing in.
+When `WRITEOUT_TRANSCRIPTION_DRIVER=local_whisper`, uploads are transcribed on your own machine instead of through OpenAI.
+When `WRITEOUT_TRANSLATION_DRIVER=mock`, translations are still mocked for local development.
+
+### Install the local Whisper dependency
+
+```bash
+python -m pip install faster-whisper
+```
+
+The first local transcription run will download the selected Whisper model automatically. `tiny` is the easiest CPU-friendly starting point; if you want more accuracy later, switch the model to `base` or `small`.
+The app keeps the Hugging Face cache under Laravel's `storage/app/huggingface` folder by default, and `HF_HUB_DISABLE_SYMLINKS_WARNING=1` keeps the Windows symlink warning out of normal local runs. If you later need higher download limits for diarization or larger models, add your own `HF_TOKEN`.
+
+### Create an OpenAI account and link your API key
1. Sign up at [OpenAI](https://openai.com/) to create a free account (you'll get $8 credits)
2. Click on the "User" / "API Keys" menu item and create a new API key
diff --git a/app/Http/Controllers/DownloadTranscriptController.php b/app/Http/Controllers/DownloadTranscriptController.php
index 66725bc..935558c 100644
--- a/app/Http/Controllers/DownloadTranscriptController.php
+++ b/app/Http/Controllers/DownloadTranscriptController.php
@@ -12,11 +12,46 @@ public function __invoke(Transcript $transcript, Request $request)
$this->authorize('view', $transcript);
$transcriptVtt = $transcript->translations[$request->get('language')] ?? $transcript->transcript;
+ $plainTextTranscript = $this->plainTextFromVtt($transcriptVtt);
- return response($transcriptVtt, 200, [
- 'Content-Type' => 'text/vtt',
- 'Content-Disposition' => 'attachment; filename="transcript.vtt"',
- 'Content-Length' => strlen($transcriptVtt),
+ return response($plainTextTranscript, 200, [
+ 'Content-Type' => 'text/plain; charset=UTF-8',
+ 'Content-Disposition' => 'attachment; filename="transcript.txt"',
+ 'Content-Length' => strlen($plainTextTranscript),
]);
}
+
+ protected function plainTextFromVtt(string $transcriptVtt): string
+ {
+ $normalizedVtt = str_replace(["\r\n", "\r"], "\n", trim($transcriptVtt));
+ $blocks = preg_split("/\n{2,}/", $normalizedVtt) ?: [];
+ $plainTextBlocks = [];
+
+ foreach ($blocks as $block) {
+ $lines = array_values(array_filter(
+ array_map('trim', explode("\n", trim($block))),
+ fn (string $line) => $line !== '',
+ ));
+
+ if ($lines === [] || $lines[0] === 'WEBVTT') {
+ continue;
+ }
+
+ if (isset($lines[1]) && str_contains($lines[1], '-->')) {
+ array_shift($lines);
+ }
+
+ if (isset($lines[0]) && str_contains($lines[0], '-->')) {
+ array_shift($lines);
+ }
+
+ if ($lines === []) {
+ continue;
+ }
+
+ $plainTextBlocks[] = implode(' ', $lines);
+ }
+
+ return implode(PHP_EOL.PHP_EOL, $plainTextBlocks);
+ }
}
diff --git a/app/Http/Controllers/NewTranscriptionController.php b/app/Http/Controllers/NewTranscriptionController.php
index 78af576..5c32ce1 100644
--- a/app/Http/Controllers/NewTranscriptionController.php
+++ b/app/Http/Controllers/NewTranscriptionController.php
@@ -3,14 +3,21 @@
namespace App\Http\Controllers;
use App\SendStack;
+use Illuminate\Contracts\Auth\Authenticatable;
class NewTranscriptionController extends Controller
{
public function __invoke(SendStack $sendStack)
{
+ /** @var Authenticatable|null $user */
+ $user = auth()->user();
return view('transcribe', [
- 'isSubscribed' => $sendStack->isActiveSubscriber(auth()->user()->email),
+ 'isSubscribed' => $user?->email ? $sendStack->isActiveSubscriber($user->email) : false,
+ 'guestUploadsEnabled' => config('writeout.allow_guest_uploads'),
+ 'mockAiEnabled' => config('writeout.mock_ai'),
+ 'transcriptionDriver' => config('writeout.transcription_driver'),
+ 'translationDriver' => config('writeout.translation_driver'),
]);
}
}
diff --git a/app/Http/Controllers/TranscribeAudioController.php b/app/Http/Controllers/TranscribeAudioController.php
index 6df45cd..dee9148 100644
--- a/app/Http/Controllers/TranscribeAudioController.php
+++ b/app/Http/Controllers/TranscribeAudioController.php
@@ -4,6 +4,7 @@
use App\Jobs\TranscribeFileJob;
use App\Models\Transcript;
+use App\Models\User;
use App\SendStack;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
@@ -14,36 +15,59 @@ public function __invoke(Request $request, SendStack $sendStack)
{
$this->authorize('create', Transcript::class);
+ $uploader = $this->resolveUploader($request);
+ $isGuestUpload = $request->user() === null;
+ $transcriptDisk = config('writeout.transcript_disk');
+
$request->validate([
'file' => [
'required',
'file',
'max:'.(25 * 1024), // max 25MB
- ]
+ ],
]);
- if ($request->get('newsletter')) {
- $sendStack->updateOrSubscribe($request->user()->email, [config('app.name')]);
+ if ($request->boolean('newsletter') && $uploader->email) {
+ $sendStack->updateOrSubscribe($uploader->email, [config('app.name')]);
}
$filename = Str::random(40).'.'.$request->file('file')->getClientOriginalExtension();
// Store the file in the public disk
$path = $request->file('file')
- ->storePubliclyAs('transcribe', $filename, 'do');
+ ->storePubliclyAs('transcribe', $filename, $transcriptDisk);
// Store the file locally temporarily for OpenAI
$request->file('file')
->storeAs('transcribe', $filename, 'local');
$transcript = Transcript::create([
- 'user_id' => $request->user()->id,
+ 'user_id' => $uploader->id,
'hash' => $path,
'prompt' => $request->input('prompt', ''),
+ 'public' => $isGuestUpload ? true : $request->boolean('public', true),
]);
$this->dispatch(new TranscribeFileJob($transcript));
return redirect()->action(ShowTranscriptController::class, $transcript);
}
+
+ protected function resolveUploader(Request $request): User
+ {
+ if ($request->user() instanceof User) {
+ return $request->user();
+ }
+
+ abort_unless(config('writeout.allow_guest_uploads'), 403);
+
+ return User::firstOrCreate(
+ ['github_id' => 'local-uploader'],
+ [
+ 'github_username' => 'local-uploader',
+ 'name' => 'Local Uploader',
+ 'email' => null,
+ ],
+ );
+ }
}
diff --git a/app/Http/Controllers/TranslateTranscriptController.php b/app/Http/Controllers/TranslateTranscriptController.php
index 5890492..f1cb8b5 100644
--- a/app/Http/Controllers/TranslateTranscriptController.php
+++ b/app/Http/Controllers/TranslateTranscriptController.php
@@ -13,14 +13,16 @@ public function __invoke(Transcript $transcript, Request $request)
{
$this->authorize('view', $transcript);
- if ($request->get('language') === null) {
+ $language = trim((string) $request->get('language', ''));
+
+ if ($language === '') {
return redirect()->action(ShowTranscriptController::class, $transcript);
}
- if ($transcript->translations[$request->get('language')] ?? null) {
+ if ($transcript->translations[$language] ?? null) {
return redirect()->action(ShowTranscriptController::class, [
'transcript' => $transcript,
- 'language' => $request->get('language'),
+ 'language' => $language,
]);
}
@@ -28,11 +30,11 @@ public function __invoke(Transcript $transcript, Request $request)
'status' => TranscriptStatus::TRANSLATING->value,
]);
- $this->dispatch(new TranslateTranscriptJob($transcript, $request->get('language')));
+ $this->dispatch(new TranslateTranscriptJob($transcript, $language));
return redirect()->action(ShowTranscriptController::class, [
'transcript' => $transcript,
- 'language' => $request->get('language'),
+ 'language' => $language,
]);
}
}
diff --git a/app/Jobs/TranscribeFileJob.php b/app/Jobs/TranscribeFileJob.php
index d657885..60f4ded 100644
--- a/app/Jobs/TranscribeFileJob.php
+++ b/app/Jobs/TranscribeFileJob.php
@@ -4,6 +4,8 @@
use App\Enum\TranscriptStatus;
use App\Models\Transcript;
+use App\Support\LocalWhisperTranscriber;
+use App\Support\MockTranscriptFactory;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -20,29 +22,51 @@ public function __construct(protected Transcript $transcript)
{
}
- public function handle()
+ public function handle(LocalWhisperTranscriber $localWhisperTranscriber)
{
+ $transcriptDisk = config('writeout.transcript_disk');
+ $driver = config('writeout.transcription_driver');
+
$this->transcript->update([
'status' => TranscriptStatus::TRANSCRIBING,
]);
try {
- $transcriptionResults = OpenAI::audio()->transcribe([
- 'model' => 'whisper-1',
- 'file' => fopen(storage_path('app/'.$this->transcript->hash), 'r'),
- 'prompt' => $this->transcript->prompt,
- 'temperature' => 0.2,
- 'response_format' => 'vtt',
- ]);
+ if ($driver === 'mock') {
+ $transcriptionResults = [
+ 'language' => 'English',
+ 'duration' => 14,
+ 'transcript' => MockTranscriptFactory::makeVtt($this->transcript),
+ ];
+ } elseif ($driver === 'local_whisper') {
+ $transcriptionResults = $localWhisperTranscriber->transcribe($this->transcript);
+ } else {
+ $openAiResults = OpenAI::audio()->transcribe([
+ 'model' => 'whisper-1',
+ 'file' => fopen(storage_path('app/'.$this->transcript->hash), 'r'),
+ 'prompt' => $this->transcript->prompt,
+ 'temperature' => 0.2,
+ 'response_format' => 'vtt',
+ ]);
+
+ $transcriptionResults = [
+ 'language' => $openAiResults->language,
+ 'duration' => $openAiResults->duration,
+ 'transcript' => $openAiResults->text,
+ ];
+ }
$this->transcript->update([
- 'source_language' => $transcriptionResults->language,
+ 'source_language' => $transcriptionResults['language'],
'status' => TranscriptStatus::COMPLETED,
- 'duration' => $transcriptionResults->duration,
- 'transcript' => $transcriptionResults->text,
+ 'duration' => $transcriptionResults['duration'],
+ 'transcript' => $transcriptionResults['transcript'],
+ 'error' => null,
]);
} catch (\Throwable $e) {
- Storage::disk('public')->delete($this->transcript->hash);
+ report($e);
+
+ Storage::disk($transcriptDisk)->delete($this->transcript->hash);
$this->transcript->update([
'status' => TranscriptStatus::FAILED,
diff --git a/app/Jobs/TranslateTranscriptJob.php b/app/Jobs/TranslateTranscriptJob.php
index b79d868..f2681f0 100644
--- a/app/Jobs/TranslateTranscriptJob.php
+++ b/app/Jobs/TranslateTranscriptJob.php
@@ -4,6 +4,7 @@
use App\Enum\TranscriptStatus;
use App\Models\Transcript;
+use App\Support\MockTranscriptFactory;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -27,8 +28,28 @@ public function __construct(private Transcript $transcript, private string $lang
public function handle()
{
$chunks = $this->getTranscriptChunks();
+ $driver = config('writeout.translation_driver');
try {
+ if ($driver === 'mock') {
+ $translations = $this->transcript->translations ?? [];
+ $translations[$this->language] = MockTranscriptFactory::translateVtt(
+ $this->transcript->transcript,
+ $this->language,
+ );
+
+ $this->transcript->update([
+ 'status' => TranscriptStatus::TRANSLATED->value,
+ 'translations' => $translations,
+ ]);
+
+ return;
+ }
+
+ if ($driver !== 'openai') {
+ throw new \RuntimeException('Unsupported translation driver: '.$driver);
+ }
+
$translation = '';
foreach ($chunks as $chunk) {
@@ -45,6 +66,8 @@ public function handle()
'translations' => $translations,
]);
} catch (\Exception $e) {
+ report($e);
+
$this->transcript->update([
'status' => TranscriptStatus::COMPLETED->value,
]);
diff --git a/app/Policies/TranscriptPolicy.php b/app/Policies/TranscriptPolicy.php
index 17c45db..ca6f514 100644
--- a/app/Policies/TranscriptPolicy.php
+++ b/app/Policies/TranscriptPolicy.php
@@ -10,7 +10,7 @@ class TranscriptPolicy
{
use HandlesAuthorization;
- public function viewAny(User $user): bool
+ public function viewAny(?User $user): bool
{
return true;
}
@@ -20,8 +20,8 @@ public function view(?User $user, Transcript $transcript): bool
return $user?->id === $transcript->user_id || $transcript->public;
}
- public function create(User $user): bool
+ public function create(?User $user): bool
{
- return true;
+ return $user !== null || config('writeout.allow_guest_uploads');
}
}
diff --git a/app/Support/LocalWhisperTranscriber.php b/app/Support/LocalWhisperTranscriber.php
new file mode 100644
index 0000000..542b1ba
--- /dev/null
+++ b/app/Support/LocalWhisperTranscriber.php
@@ -0,0 +1,69 @@
+hash),
+ '--model',
+ config('writeout.local_whisper.model'),
+ '--device',
+ config('writeout.local_whisper.device'),
+ '--compute-type',
+ config('writeout.local_whisper.compute_type'),
+ ];
+
+ if (filled($transcript->prompt)) {
+ $command[] = '--prompt';
+ $command[] = $transcript->prompt;
+ }
+
+ $process = new Process($command, base_path(), [
+ 'PYTHONIOENCODING' => 'utf-8',
+ 'HF_HOME' => config('writeout.huggingface.home'),
+ 'HF_HUB_CACHE' => config('writeout.huggingface.hub_cache'),
+ 'HF_HUB_DISABLE_SYMLINKS_WARNING' => config('writeout.huggingface.disable_symlinks_warning') ? '1' : '0',
+ 'HF_HUB_DISABLE_PROGRESS_BARS' => config('writeout.huggingface.disable_progress_bars') ? '1' : '0',
+ 'HF_TOKEN' => (string) config('writeout.huggingface.token'),
+ ]);
+
+ $process->setTimeout(3600);
+ $process->run();
+
+ if (! $process->isSuccessful()) {
+ $error = trim($process->getErrorOutput());
+
+ throw new RuntimeException(
+ $error !== ''
+ ? $error
+ : 'Local Whisper transcription failed to start.',
+ );
+ }
+
+ $payload = json_decode($process->getOutput(), true);
+
+ if (! is_array($payload)) {
+ throw new RuntimeException('Local Whisper returned an invalid response.');
+ }
+
+ if (! isset($payload['language'], $payload['duration'], $payload['transcript'])) {
+ throw new RuntimeException('Local Whisper returned an incomplete response.');
+ }
+
+ return $payload;
+ }
+}
diff --git a/app/Support/MockTranscriptFactory.php b/app/Support/MockTranscriptFactory.php
new file mode 100644
index 0000000..fdfd712
--- /dev/null
+++ b/app/Support/MockTranscriptFactory.php
@@ -0,0 +1,63 @@
+prompt)->squish()->trim();
+
+ $promptLine = $prompt->isNotEmpty()
+ ? $prompt->limit(110)->value()
+ : 'This is a locally generated demo transcript so you can keep building without OpenAI billing.';
+
+ return implode(PHP_EOL, [
+ 'WEBVTT',
+ '',
+ '1',
+ '00:00:00.000 --> 00:00:04.000',
+ 'Mock transcript generated in local mode.',
+ '',
+ '2',
+ '00:00:04.000 --> 00:00:09.000',
+ $promptLine,
+ '',
+ '3',
+ '00:00:09.000 --> 00:00:14.000',
+ 'Set WRITEOUT_MOCK_AI=false when you are ready to use the real OpenAI API.',
+ ]);
+ }
+
+ public static function translateVtt(string $vtt, string $language): string
+ {
+ $lines = preg_split('/\r\n|\r|\n/', $vtt) ?: [];
+
+ $translated = array_map(
+ fn (string $line) => self::shouldTranslateLine($line)
+ ? '['.$language.' demo] '.$line
+ : $line,
+ $lines,
+ );
+
+ return implode(PHP_EOL, $translated);
+ }
+
+ protected static function shouldTranslateLine(string $line): bool
+ {
+ $trimmed = trim($line);
+
+ if ($trimmed === '' || $trimmed === 'WEBVTT') {
+ return false;
+ }
+
+ if (is_numeric($trimmed)) {
+ return false;
+ }
+
+ return ! str_contains($trimmed, '-->');
+ }
+}
diff --git a/config/writeout.php b/config/writeout.php
index efd962f..ae10ce6 100644
--- a/config/writeout.php
+++ b/config/writeout.php
@@ -1,6 +1,51 @@
env('WRITEOUT_ALLOW_GUEST_UPLOADS', $isLocal),
+
+ 'transcript_disk' => env('TRANSCRIPT_STORAGE_DISK', env('DO_BUCKET') ? 'do' : 'public'),
+
+ 'transcription_driver' => $transcriptionDriver,
+
+ 'translation_driver' => $translationDriver,
+
+ 'mock_ai' => $transcriptionDriver === 'mock' || $translationDriver === 'mock',
+
+ 'local_whisper' => [
+ 'command' => env('WRITEOUT_LOCAL_WHISPER_COMMAND', PHP_OS_FAMILY === 'Windows' ? 'py' : 'python3'),
+ 'model' => env('WRITEOUT_LOCAL_WHISPER_MODEL', 'tiny'),
+ 'device' => env('WRITEOUT_LOCAL_WHISPER_DEVICE', 'cpu'),
+ 'compute_type' => env('WRITEOUT_LOCAL_WHISPER_COMPUTE_TYPE', 'int8'),
+ ],
+
+ 'huggingface' => [
+ 'home' => env('HF_HOME', storage_path('app/huggingface')),
+ 'hub_cache' => env('HF_HUB_CACHE', storage_path('app/huggingface/hub')),
+ 'token' => env('HF_TOKEN'),
+ 'disable_symlinks_warning' => env('HF_HUB_DISABLE_SYMLINKS_WARNING', true),
+ 'disable_progress_bars' => env('HF_HUB_DISABLE_PROGRESS_BARS', true),
+ ],
+
'translatable_languages' => [
'English',
'German',
diff --git a/resources/views/errors/403.blade.php b/resources/views/errors/403.blade.php
index d819338..9db3357 100644
--- a/resources/views/errors/403.blade.php
+++ b/resources/views/errors/403.blade.php
@@ -1,5 +1,13 @@
@extends('layouts.app')
@section('content')
+ @php
+ $canTranscribeWithoutLogin = config('writeout.allow_guest_uploads');
+ $ctaUrl = auth()->check() || $canTranscribeWithoutLogin ? url('/transcribe') : route('login');
+ $ctaLabel = auth()->check() || $canTranscribeWithoutLogin
+ ? 'Transcribe your own audio'
+ : 'Log in to transcribe your own audio';
+ @endphp
+
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
index 154cdba..f70b639 100644
--- a/resources/views/layouts/app.blade.php
+++ b/resources/views/layouts/app.blade.php
@@ -5,7 +5,7 @@ class="h-full scroll-smooth bg-white antialiased [font-feature-settings:'ss01']
-
writeout.ai – Transcribe and translate any audio file
+
writeout.ai - Transcribe and translate any audio file
@@ -82,7 +82,7 @@ class="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row
- Copyright © {{ date('Y') }} Beyond Code. All rights reserved.
+ Copyright © {{ date('Y') }} Beyond Code. All rights reserved.
diff --git a/resources/views/transcribe.blade.php b/resources/views/transcribe.blade.php
index 8389922..3fbbe2a 100644
--- a/resources/views/transcribe.blade.php
+++ b/resources/views/transcribe.blade.php
@@ -11,6 +11,31 @@ class="font-display text-3xl tracking-tight text-slate-900 sm:text-5xl">
Upload your audio file and we'll transcribe it for you. It's that easy.
+ @if ($transcriptionDriver === 'local_whisper')
+
+
+ Local Whisper mode is enabled
+
+
+ New uploads will be transcribed on this machine without using OpenAI billing.
+
+ @if ($translationDriver === 'mock')
+
+ Transcript translation is still running in demo mode for local development.
+
+ @endif
+
+ @elseif ($mockAiEnabled)
+
+
+ Mock AI mode is enabled
+
+
+ Uploads will complete with a demo transcript and demo translations, so you can keep building locally without OpenAI billing.
+
+
+ @endif
+
@if ($errors->isNotEmpty())
@@ -32,13 +57,19 @@ class="font-display text-3xl tracking-tight text-slate-900 sm:text-5xl">
@endif
-
@@ -67,7 +77,7 @@ class="font-display text-3xl tracking-tight text-slate-900 sm:text-5xl">
'transcript' => $transcript,
'language' => request()->get('language') ?? '',
]) }}">
- Download transcript
+ Download text transcript
@include('partials.banner')
+ ]) }}">
+
@endif
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php
index a6a2db3..e00e7ab 100644
--- a/resources/views/welcome.blade.php
+++ b/resources/views/welcome.blade.php
@@ -1,5 +1,9 @@
@extends('layouts.app')
@section('content')
+ @php
+ $demoTranscript = \App\Models\Transcript::query()->where('public', true)->first();
+ @endphp
+
Transcribe
From audio...
-
+ @if ($demoTranscript)
+
+ @else
+
+ Upload a public transcript to preview the audio demo here.
+
+ @endif
...to transcript
@@ -39,11 +49,17 @@ class="absolute top-1/2 left-1/2 max-w-none -translate-x-1/2 -translate-y-1/2"/>
In more than 10 languages, including Klingon 🤓
-
- Take a look at the
- demo.
-
-
+ @if ($demoTranscript)
+
+ Take a look at the
+ demo.
+
+
+ @else
+
+ The demo transcript link will appear automatically once you have a public transcript.
+
+ @endif