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 -
+ + @csrf
- +

Supported formats: mp3, mp4, mpeg, mpga, m4a, wav and webm.
@@ -55,12 +86,44 @@ class="font-display text-3xl tracking-tight text-slate-900 sm:text-5xl"> rows="3" class="block w-full rounded-md border-0 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:py-1.5 sm:text-sm sm:leading-6" placeholder="The transcript is about Laravel which makes development with PHP a breeze." - > + >{{ old('prompt') }}

-

The prompt can be used to provide additional information about the audio. This can be very helpful for correcting specific words or acronyms that the AI model often misrecognizes in the audio..

+

+ The prompt can be used to provide additional information about the audio. This can be very helpful + for correcting specific words or acronyms that the AI model often misrecognizes in the audio. +

- @if (auth()->user()->email && !$isSubscribed) + @if (! auth()->check() && $guestUploadsEnabled) + +
+

+ Guest uploads are enabled in this environment. Your transcript will be public so you can reopen + it without logging in. +

+
+ @else +
+
+
+ +
+
+ +

Anyone with the transcript link will be able to view it.

+
+
+
+ @endif + + @if (auth()->check() && auth()->user()->email && !$isSubscribed)
@@ -68,7 +131,7 @@ class="block w-full rounded-md border-0 text-gray-900 shadow-sm ring-1 ring-inse
-

Stay informed about our developer tools, such was writeout.ai

+

Stay informed about our developer tools, such as writeout.ai.

diff --git a/resources/views/transcript.blade.php b/resources/views/transcript.blade.php index eea6bdf..1f73f09 100644 --- a/resources/views/transcript.blade.php +++ b/resources/views/transcript.blade.php @@ -23,6 +23,17 @@ class="font-display text-3xl tracking-tight text-slate-900 sm:text-5xl">

We were unable to transcribe your audio. Please try again.

+ + @if (app()->isLocal() && filled($transcript->error)) +
+

+ Failure details +

+

+ {{ $transcript->error }} +

+
+ @endif @include('partials.banner') @elseif($transcript->isTranscribed() || $transcript->isTranslated() || $transcript->isTranslating()) @@ -54,8 +65,7 @@ class="font-display text-3xl tracking-tight text-slate-900 sm:text-5xl"> @endforeach -
@@ -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