Publish Whisper Transcriber as a micro-architecture utility#130
Conversation
Adds the client-side Whisper speech-to-text tool from the ws-speech-text repository as a full site utility, following the same micro-architecture as the other embedded apps: the app is built in its own repository and published as a static artifact under static/utility-apps/, and the Docusaurus shell owns navigation, SEO, reactions, comments and access. Artifact and sync - scripts/sync-whisper-transcriber.mjs builds the source app, audits the bundle (no symlinks, no traversal, no source maps, no dev/localhost references, only packaged relative assets), stages it, and swaps it in atomically. - manifest.json records the version, build id and a SHA-256 per file; the .gitattributes rule keeps those files byte-exact through checkout. - index.html is published next to app.html so the directory URL also serves the app under trailing-slash hosting. Site wiring - catalog descriptor, shell config, route, sidebar and overview entries; - name and description added to all six locale dictionaries; - English user documentation plus ru/ua/de/es/et translations; - catalog thumbnail and social card. Security headers - Device capabilities are now delegated per utility via UtilityPageConfig .iframeAllow. The transcriber is the only utility granted microphone access; every other utility keeps the previous camera-only default. - vercel.json relaxes Permissions-Policy to microphone=(self) only on the transcriber routes — the site default stays microphone=(). - connect-src allows the Hugging Face Hub origins the pinned model is fetched from. No audio, filenames or transcript text ever leaves the page. Verified: typecheck, lint and a six-locale production build pass, and the embedded app boots and reports its build id in the local preview. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
There was a problem hiding this comment.
Code Review
This pull request integrates the new 'Whisper Transcriber' utility, enabling private, on-device speech-to-text transcription. It adds localized documentation, updates the site shell to support microphone permissions via iframe delegation, configures security policies in vercel.json, and introduces a synchronization script to build and publish the static transcriber artifact. The review feedback highlights several key improvements for the synchronization script, including robust error handling for subprocesses, supporting single quotes in the HTML asset audit, and implementing a safer atomic directory swap. Additionally, the feedback correctly notes that the canonical links and schema URLs in the localized MDX documentation files should be updated to point to their respective localized paths rather than the English version.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function run(command, args, cwd) { | ||
| const result = spawnSync(command, args, { | ||
| cwd, | ||
| stdio: 'inherit', | ||
| shell: process.platform === 'win32', | ||
| }); | ||
| if (result.status !== 0) { | ||
| fail(`Command failed: ${command} ${args.join(' ')}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
The spawnSync error handling is incomplete. If the command fails to start (e.g., pnpm is not installed or not in PATH), result.status will be null and result.error will be populated. Checking only result.status !== 0 throws a generic 'Command failed' error without the underlying error message (like ENOENT). Checking result.error explicitly improves debuggability.
function run(command, args, cwd) {
const result = spawnSync(command, args, {
cwd,
stdio: 'inherit',
shell: process.platform === 'win32',
});
if (result.error) {
fail(`Failed to start command: ${command} ${args.join(' ')}. Error: ${result.error.message}`);
}
if (result.status !== 0) {
fail(`Command failed with exit code ${result.status}: ${command} ${args.join(' ')}`);
}
}| } | ||
| } | ||
|
|
||
| const referenced = [...html.matchAll(/(?:src|href)="([^"]+)"/g)].map((match) => match[1]); |
There was a problem hiding this comment.
The regex /(?:src|href)="([^"]+)"/g only matches double-quoted attributes. If the build tool or a manual edit uses single quotes (e.g., src='./assets/...'), the audit will miss them or fail. Supporting both single and double quotes makes the audit more robust.
| const referenced = [...html.matchAll(/(?:src|href)="([^"]+)"/g)].map((match) => match[1]); | |
| const referenced = [...html.matchAll(/(?:src|href)=["']([^"']+)["']/g)].map((match) => match[1]); |
| fs.rmSync(TARGET_DIR, {recursive: true, force: true}); | ||
| fs.renameSync(STAGING_DIR, TARGET_DIR); |
There was a problem hiding this comment.
Deleting the target directory before renaming the staging directory is not safe. If the rename fails (e.g., due to permission issues or locked files), the site is left with a deleted/missing utility directory. Using a temporary backup directory for rollback ensures atomic updates and zero downtime.
| fs.rmSync(TARGET_DIR, {recursive: true, force: true}); | |
| fs.renameSync(STAGING_DIR, TARGET_DIR); | |
| const backupDir = `${TARGET_DIR}.backup`; | |
| fs.rmSync(backupDir, {recursive: true, force: true}); | |
| if (fs.existsSync(TARGET_DIR)) { | |
| fs.renameSync(TARGET_DIR, backupDir); | |
| } | |
| try { | |
| fs.renameSync(STAGING_DIR, TARGET_DIR); | |
| fs.rmSync(backupDir, {recursive: true, force: true}); | |
| } catch (renameError) { | |
| if (fs.existsSync(backupDir)) { | |
| fs.rmSync(TARGET_DIR, {recursive: true, force: true}); | |
| fs.renameSync(backupDir, TARGET_DIR); | |
| } | |
| throw renameError; | |
| } |
| <link rel="canonical" href="https://cadautoscript.com/utilities/whisper-transcriber/" /> | ||
| <script type="application/ld+json">{`{ | ||
| "@context": "https://schema.org", | ||
| "@type": "SoftwareApplication", | ||
| "name": "Whisper Transcriber", | ||
| "applicationCategory": "MultimediaApplication", | ||
| "operatingSystem": "Web", | ||
| "description": "Audio- und Videodateien oder Mikrofonaufnahmen mit Whisper vollständig im Browser in bearbeitbaren Text transkribieren und als TXT, SRT oder WebVTT exportieren.", | ||
| "url": "https://cadautoscript.com/utilities/whisper-transcriber/", |
There was a problem hiding this comment.
The canonical link and schema URL point to the English version of the page (https://cadautoscript.com/utilities/whisper-transcriber/). For localized pages, they should point to the localized URL (https://cadautoscript.com/de/utilities/whisper-transcriber/) to ensure search engines index the translated content correctly.
<link rel="canonical" href="https://cadautoscript.com/de/utilities/whisper-transcriber/" />
<script type="application/ld+json">{`{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Whisper Transcriber",
"applicationCategory": "MultimediaApplication",
"operatingSystem": "Web",
"description": "Audio- und Videodateien oder Mikrofonaufnahmen mit Whisper vollständig im Browser in bearbeitbaren Text transkribieren und als TXT, SRT oder WebVTT exportieren.",
"url": "https://cadautoscript.com/de/utilities/whisper-transcriber/",
| <link rel="canonical" href="https://cadautoscript.com/utilities/whisper-transcriber/" /> | ||
| <script type="application/ld+json">{`{ | ||
| "@context": "https://schema.org", | ||
| "@type": "SoftwareApplication", | ||
| "name": "Whisper Transcriber", | ||
| "applicationCategory": "MultimediaApplication", | ||
| "operatingSystem": "Web", | ||
| "description": "Transcribe archivos de audio y vídeo o grabaciones de micrófono a texto editable con Whisper ejecutándose por completo en el navegador, y exporta TXT, SRT o WebVTT.", | ||
| "url": "https://cadautoscript.com/utilities/whisper-transcriber/", |
There was a problem hiding this comment.
The canonical link and schema URL point to the English version of the page (https://cadautoscript.com/utilities/whisper-transcriber/). For localized pages, they should point to the localized URL (https://cadautoscript.com/es/utilities/whisper-transcriber/) to ensure search engines index the translated content correctly.
<link rel="canonical" href="https://cadautoscript.com/es/utilities/whisper-transcriber/" />
<script type="application/ld+json">{`{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Whisper Transcriber",
"applicationCategory": "MultimediaApplication",
"operatingSystem": "Web",
"description": "Transcribe archivos de audio y vídeo o grabaciones de micrófono a texto editable con Whisper ejecutándose por completo en el navegador, y exporta TXT, SRT o WebVTT.",
"url": "https://cadautoscript.com/es/utilities/whisper-transcriber/",
| <link rel="canonical" href="https://cadautoscript.com/utilities/whisper-transcriber/" /> | ||
| <script type="application/ld+json">{`{ | ||
| "@context": "https://schema.org", | ||
| "@type": "SoftwareApplication", | ||
| "name": "Whisper Transcriber", | ||
| "applicationCategory": "MultimediaApplication", | ||
| "operatingSystem": "Web", | ||
| "description": "Transkribeeri heli- ja videofaile või mikrofonisalvestisi redigeeritavaks tekstiks — Whisper töötab täielikult brauseris, eksport TXT, SRT ja WebVTT vormingusse.", | ||
| "url": "https://cadautoscript.com/utilities/whisper-transcriber/", |
There was a problem hiding this comment.
The canonical link and schema URL point to the English version of the page (https://cadautoscript.com/utilities/whisper-transcriber/). For localized pages, they should point to the localized URL (https://cadautoscript.com/et/utilities/whisper-transcriber/) to ensure search engines index the translated content correctly.
<link rel="canonical" href="https://cadautoscript.com/et/utilities/whisper-transcriber/" />
<script type="application/ld+json">{`{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Whisper Transcriber",
"applicationCategory": "MultimediaApplication",
"operatingSystem": "Web",
"description": "Transkribeeri heli- ja videofaile või mikrofonisalvestisi redigeeritavaks tekstiks — Whisper töötab täielikult brauseris, eksport TXT, SRT ja WebVTT vormingusse.",
"url": "https://cadautoscript.com/et/utilities/whisper-transcriber/",
| <link rel="canonical" href="https://cadautoscript.com/utilities/whisper-transcriber/" /> | ||
| <script type="application/ld+json">{`{ | ||
| "@context": "https://schema.org", | ||
| "@type": "SoftwareApplication", | ||
| "name": "Whisper Transcriber", | ||
| "applicationCategory": "MultimediaApplication", | ||
| "operatingSystem": "Web", | ||
| "description": "Расшифровка аудио- и видеофайлов или записи с микрофона в редактируемый текст: Whisper работает целиком в браузере, экспорт в TXT, SRT и WebVTT.", | ||
| "url": "https://cadautoscript.com/utilities/whisper-transcriber/", |
There was a problem hiding this comment.
The canonical link and schema URL point to the English version of the page (https://cadautoscript.com/utilities/whisper-transcriber/). For localized pages, they should point to the localized URL (https://cadautoscript.com/ru/utilities/whisper-transcriber/) to ensure search engines index the translated content correctly.
<link rel="canonical" href="https://cadautoscript.com/ru/utilities/whisper-transcriber/" />
<script type="application/ld+json">{`{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Whisper Transcriber",
"applicationCategory": "MultimediaApplication",
"operatingSystem": "Web",
"description": "Расшифровка аудио- и видеофайлов или записи с микрофона в редактируемый текст: Whisper работает целиком в браузере, экспорт в TXT, SRT и WebVTT.",
"url": "https://cadautoscript.com/ru/utilities/whisper-transcriber/",
| <link rel="canonical" href="https://cadautoscript.com/utilities/whisper-transcriber/" /> | ||
| <script type="application/ld+json">{`{ | ||
| "@context": "https://schema.org", | ||
| "@type": "SoftwareApplication", | ||
| "name": "Whisper Transcriber", | ||
| "applicationCategory": "MultimediaApplication", | ||
| "operatingSystem": "Web", | ||
| "description": "Розшифровка аудіо- та відеофайлів або запису з мікрофона в редагований текст: Whisper працює цілком у браузері, експорт у TXT, SRT і WebVTT.", | ||
| "url": "https://cadautoscript.com/utilities/whisper-transcriber/", |
There was a problem hiding this comment.
The canonical link and schema URL point to the English version of the page (https://cadautoscript.com/utilities/whisper-transcriber/). For localized pages, they should point to the localized URL (https://cadautoscript.com/ua/utilities/whisper-transcriber/) to ensure search engines index the translated content correctly.
<link rel="canonical" href="https://cadautoscript.com/ua/utilities/whisper-transcriber/" />
<script type="application/ld+json">{`{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Whisper Transcriber",
"applicationCategory": "MultimediaApplication",
"operatingSystem": "Web",
"description": "Розшифровка аудіо- та відеофайлів або запису з мікрофона в редагований текст: Whisper працює цілком у браузері, експорт у TXT, SRT і WebVTT.",
"url": "https://cadautoscript.com/ua/utilities/whisper-transcriber/",
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9aa4345384
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| { | ||
| "key": "Content-Security-Policy", | ||
| "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://challenges.cloudflare.com https://va.vercel-scripts.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https://*.supabase.co wss://*.supabase.co https://api.stripe.com https://challenges.cloudflare.com https://*.vercel-insights.com https://vitals.vercel-insights.com; frame-src 'self' blob: https://challenges.cloudflare.com; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" | ||
| "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://challenges.cloudflare.com https://va.vercel-scripts.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https://*.supabase.co wss://*.supabase.co https://api.stripe.com https://challenges.cloudflare.com https://*.vercel-insights.com https://vitals.vercel-insights.com https://huggingface.co https://*.huggingface.co https://*.hf.co; frame-src 'self' blob: https://challenges.cloudflare.com; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" |
There was a problem hiding this comment.
Allow the ONNX wasm URL used by the worker
When the transcriber initializes ONNX Runtime, the committed worker sets env.backends.onnx.wasm.wasmPaths to https://cdn.jsdelivr.net/npm/@huggingface/transformers@.../dist/ when no path is configured, but this CSP update only allows Hugging Face origins in connect-src. In deployed browsers that take the WASM/ORT path, the fetch for ort-wasm-simd-threaded.jsep.wasm is blocked before inference starts, so the advertised WASM fallback (and likely model initialization) fails despite the local .wasm file being shipped. Configure the worker to use the packaged ./assets/ort-...wasm URL or add the actual CDN origin here.
Useful? React with 👍 / 👎.
Fixed:
|
| Runtime | Result | Prep |
|---|---|---|
auto → WebGPU |
transcription complete, no console errors | 15.0 s |
wasm only |
transcription complete, no console errors | 4.4 s |
The only third-party request left at runtime is the pinned model download from the Hugging Face Hub, which the connect-src entries in this PR already cover.
Publishes the client-side Whisper speech-to-text app from
YurMil/ws-speech-textas a full site utility, following the same micro-architecture as the other embedded apps, and automates future updates.Route:
/utilities/whisper-transcriber/· Docs:/docs/utilities/whisper-transcriber/Companion PR: YurMil/ws-speech-text#1
Artifact and sync
scripts/sync-whisper-transcriber.mjs(npm run sync:whisper-transcriber) audits the bundle — no symlinks, no path traversal, no source maps, no dev/localhost references, entry HTML may only reference packaged relative assets — stages it and swaps it in atomically. It publishes either from a local checkout or, with--archive … --sha256 …, from a release archive whose checksum is verified before anything is extracted.manifest.jsonrecords version, build id and a SHA-256 per file, and takes its build time from the source build rather thannow(), so republishing an unchanged release produces no diff. A.gitattributesrule keeps the artifact byte-exact through checkout so those checksums stay verifiable.index.htmlis published next toapp.htmlso the directory URL also serves the app under trailing-slash hosting.Automated publication
.github/workflows/sync-whisper-transcriber.ymlreacts to awhisper-transcriber-releaserepository_dispatchfrom the source repository (or runs manually with a tag): it downloads the release archive, refuses it unless the SHA-256 matches, republishes the artifact, runs typecheck/lint/build, and opens a pull request. Nothing deploys without review.Requires: the
SITE_DISPATCH_TOKENsecret inYurMil/ws-speech-text— a fine-grained PAT scoped to this repository with Contents: read and write. Optional here:SOURCE_RELEASE_TOKEN, only needed if the source repository becomes private.Site wiring
Catalog descriptor, shell config, route, sidebar and overview entries; the name and description in all six locale dictionaries; English documentation plus ru/ua/de/es/et translations; catalog thumbnail and social card.
Security headers — please review
UtilityPageConfig.iframeAllow. The transcriber is the only utility that receivesmicrophone 'self'; every other utility keeps the previous camera-only default.vercel.jsonrelaxesPermissions-Policytomicrophone=(self)only on/utilities/whisper-transcriber/*and/utility-apps/whisper-transcriber/*. The site default staysmicrophone=().connect-srcgains the Hugging Face Hub origins the model is fetched from. This was added to the global policy rather than a path rule on purpose: if Vercel sent both policies for those paths the browser would enforce the intersection and the model download would break. The Permissions-Policy path rule fails closed the same way — worst case the microphone stays blocked and file upload still works.Preview checks worth doing before merge: confirm the response headers on both transcriber paths, that the microphone prompt appears only after clicking Record, that the file workflow still works when the microphone is denied, and that the WASM fallback works without WebGPU.
Verification
npm run typecheck,npm run lint(0 errors) and a six-localenpm run buildpass. The embedded app boots in the local preview and reports its injected build id; the docs pages render in en and ru; the route is in the sitemap. The archive path was exercised both with a matching checksum (byte-identical republish) and a deliberate mismatch (rejected, exit 1, target untouched).🤖 Generated with Claude Code