Spaces:
Build error
Build error
Fix: load transformers.js from CDN, remove paths option; ignore dist/
Browse files- dist/index.html +0 -15
- dist/server/app/feed/loading.js +0 -2
- dist/server/app/feed/page.js +0 -2
- dist/server/app/layout.js +0 -2
- dist/server/app/page.js +0 -2
- dist/server/image.json +0 -36
- dist/server/lib/mistral-summarize.js +0 -2
- dist/server/manifest.json +0 -66
- dist/server/routes.json +0 -41
- src/lib/voxtral-local.ts +21 -16
- vite.config.ts +0 -11
dist/index.html
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
|
| 5 |
-
<meta charset="UTF-8" />
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>HN Radio - Listen to Hacker News</title>
|
| 8 |
-
<script type="module" crossorigin src="/assets/index-Dbk3viwN.js"></script>
|
| 9 |
-
<link rel="stylesheet" crossorigin href="/assets/index-uzjJOG7t.css">
|
| 10 |
-
</head>
|
| 11 |
-
|
| 12 |
-
<body class="min-h-screen bg-white-950 text-black-100">
|
| 13 |
-
<div id="root" />
|
| 14 |
-
</body>
|
| 15 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dist/server/app/feed/loading.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// Built: 2026-03-01T21:17:29.353Z
|
| 2 |
-
function e(){return React.createElement(`div`,{className:`max-w-2xl mx-auto px-6 py-8`},React.createElement(`div`,{className:`flex items-center justify-between mb-8 p-4 rounded-2xl bg-white border border-gray-200 animate-pulse`},React.createElement(`div`,{className:`flex items-center gap-3`},React.createElement(`div`,{className:`w-10 h-10 rounded-full bg-gray-200`}),React.createElement(`div`,{className:`space-y-2`},React.createElement(`div`,{className:`h-3 w-24 bg-gray-200 rounded`}),React.createElement(`div`,{className:`h-2 w-32 bg-gray-200 rounded`})))),React.createElement(`div`,{className:`space-y-4`},[1,2,3].map(e=>React.createElement(`div`,{key:e,className:`p-6 rounded-2xl bg-white border border-gray-200 animate-pulse`},React.createElement(`div`,{className:`flex items-center gap-3 mb-4`},React.createElement(`div`,{className:`w-12 h-12 rounded-full bg-gray-200`}),React.createElement(`div`,{className:`space-y-2`},React.createElement(`div`,{className:`h-3 w-28 bg-gray-200 rounded`}),React.createElement(`div`,{className:`h-2 w-40 bg-gray-200 rounded`}))),React.createElement(`div`,{className:`space-y-2`},React.createElement(`div`,{className:`h-3 w-full bg-gray-200 rounded`}),React.createElement(`div`,{className:`h-3 w-full bg-gray-200 rounded`}),React.createElement(`div`,{className:`h-3 w-3/4 bg-gray-200 rounded`}))))))}export{e as default};
|
|
|
|
|
|
|
|
|
dist/server/app/feed/page.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// Built: 2026-03-01T21:17:29.364Z
|
| 2 |
-
function e(e,t,n){let r=t+`#`+n,i={};Object.defineProperty(i,`$$typeof`,{value:Symbol.for(`react.client.reference`),enumerable:!1}),Object.defineProperty(i,`$$id`,{value:r,enumerable:!1}),Object.defineProperty(i,`$$async`,{value:!1,enumerable:!1});try{globalThis[`~rari`]?.bridge!==void 0&&typeof globalThis[`~rari`].bridge.registerClientReference==`function`&&globalThis[`~rari`].bridge.registerClientReference(r,t,n)}catch(e){console.error(`[rari] Build: Failed to register client reference with Rust bridge:`,e)}return i}const t=e(null,`src/components/HNPostFeed.tsx`,`HNPostFeed`);function n(){return React.createElement(`div`,{className:`max-w-6xl mx-auto px-6 py-4`},React.createElement(t,null))}export{n as default};
|
|
|
|
|
|
|
|
|
dist/server/app/layout.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// Built: 2026-03-01T21:17:29.378Z
|
| 2 |
-
function e({children:e}){return React.createElement(`div`,{className:`min-h-screen text-gray-900`,style:{backgroundColor:`rgb(255, 240, 194)`}},React.createElement(`nav`,{className:`border-b border-gray-200 bg-white/80 backdrop-blur-sm sticky top-0 z-50`},React.createElement(`div`,{className:`max-w-4xl mx-auto px-6 h-16 flex items-center justify-between`},React.createElement(`a`,{href:`/`,className:`flex items-center gap-3 no-underline`},React.createElement(`div`,{className:`w-10 h-10 rounded-xl bg-gradient-to-br from-orange-500 to-amber-400 flex items-center justify-center`},React.createElement(`svg`,{width:`20`,height:`20`,viewBox:`0 0 24 24`,fill:`none`,stroke:`white`,strokeWidth:`2.5`,strokeLinecap:`round`,strokeLinejoin:`round`},React.createElement(`path`,{d:`M9 18V5l12-2v13`}),React.createElement(`circle`,{cx:`6`,cy:`18`,r:`3`}),React.createElement(`circle`,{cx:`18`,cy:`16`,r:`3`}))),React.createElement(`span`,{className:`text-xl font-bold text-gray-900`},`HN Radio`)))),React.createElement(`main`,null,e))}const t={title:`HN Radio - Listen to Hacker News`,description:`Transform your Hacker News feed into an audio experience`};export{e as default,t as metadata};
|
|
|
|
|
|
|
|
|
dist/server/app/page.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// Built: 2026-03-01T21:17:29.386Z
|
| 2 |
-
const e={fontFamily:`'Quivert', sans-serif`};function t(){return React.createElement(`div`,{className:`max-w-4xl mx-auto px-6 py-20`,style:e},React.createElement(`div`,{className:`text-center space-y-8`},React.createElement(`div`,{className:`space-y-4`},React.createElement(`div`,{className:`inline-flex items-center gap-2 px-4 py-2 rounded-full bg-orange-500/10 border border-orange-500/20 text-orange-600 text-sm font-medium`},React.createElement(`svg`,{width:`16`,height:`16`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`},React.createElement(`polygon`,{points:`11 5 6 9 2 9 2 15 6 15 11 19 11 5`}),React.createElement(`path`,{d:`M15.54 8.46a5 5 0 0 1 0 7.07`}),React.createElement(`path`,{d:`M19.07 4.93a10 10 0 0 1 0 14.14`})),`Audio-first Hacker News`),React.createElement(`h1`,{className:`text-5xl md:text-7xl tracking-tight`},React.createElement(`span`,{className:`text-gray-900`},`Listen to`),React.createElement(`br`,null),React.createElement(`span`,{className:`bg-gradient-to-r from-orange-500 to-amber-400 bg-clip-text text-transparent`},`Hacker News`)),React.createElement(`p`,{className:`text-xl text-gray-500 max-w-2xl mx-auto leading-relaxed`},`Turn top HN stories into a podcast-like experience. Listen hands-free while commuting, exercising, or cooking.`)),React.createElement(`div`,{className:`flex flex-col items-center gap-4`},React.createElement(`a`,{href:`/feed`,className:`inline-flex items-center gap-2 px-8 py-4 rounded-xl bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-400 hover:to-amber-400 text-white font-semibold text-lg transition-all shadow-lg shadow-orange-500/25 no-underline`},React.createElement(`svg`,{width:`20`,height:`20`,viewBox:`0 0 24 24`,fill:`currentColor`},React.createElement(`polygon`,{points:`5 3 19 12 5 21 5 3`})),`Start Listening`),React.createElement(`p`,{className:`text-sm text-gray-400`},`No login required — streams live from the HN API`)),React.createElement(`div`,{className:`grid grid-cols-1 md:grid-cols-3 gap-6 mt-16 text-left`},React.createElement(`div`,{className:`p-6 rounded-2xl bg-white border border-gray-200 shadow-sm`},React.createElement(`div`,{className:`w-12 h-12 rounded-xl bg-orange-500/10 flex items-center justify-center mb-4`},React.createElement(`svg`,{width:`24`,height:`24`,viewBox:`0 0 24 24`,fill:`none`,stroke:`#f97316`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`},React.createElement(`polyline`,{points:`18 15 12 9 6 15`}))),React.createElement(`h3`,{className:`text-lg font-semibold text-gray-900 mb-2`},`Live Stories`),React.createElement(`p`,{className:`text-gray-500 text-sm`},`Stream top, new, and best stories directly from Hacker News`)),React.createElement(`div`,{className:`p-6 rounded-2xl bg-white border border-gray-200 shadow-sm`},React.createElement(`div`,{className:`w-12 h-12 rounded-xl bg-amber-500/10 flex items-center justify-center mb-4`},React.createElement(`svg`,{width:`24`,height:`24`,viewBox:`0 0 24 24`,fill:`none`,stroke:`#f59e0b`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`},React.createElement(`path`,{d:`M9 18V5l12-2v13`}),React.createElement(`circle`,{cx:`6`,cy:`18`,r:`3`}),React.createElement(`circle`,{cx:`18`,cy:`16`,r:`3`}))),React.createElement(`h3`,{className:`text-lg font-semibold text-gray-900 mb-2`},`Radio Mode`),React.createElement(`p`,{className:`text-gray-500 text-sm`},`Auto-play through stories like a podcast with adjustable speed`)),React.createElement(`div`,{className:`p-6 rounded-2xl bg-white border border-gray-200 shadow-sm`},React.createElement(`div`,{className:`w-12 h-12 rounded-xl bg-yellow-500/10 flex items-center justify-center mb-4`},React.createElement(`svg`,{width:`24`,height:`24`,viewBox:`0 0 24 24`,fill:`none`,stroke:`#eab308`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`},React.createElement(`polygon`,{points:`11 5 6 9 2 9 2 15 6 15 11 19 11 5`}),React.createElement(`path`,{d:`M15.54 8.46a5 5 0 0 1 0 7.07`}))),React.createElement(`h3`,{className:`text-lg font-semibold text-gray-900 mb-2`},`Text to Speech`),React.createElement(`p`,{className:`text-gray-500 text-sm`},`Listen to story summaries read aloud with natural voices`)))))}export{t as default};
|
|
|
|
|
|
|
|
|
dist/server/image.json
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"remotePatterns": [],
|
| 3 |
-
"localPatterns": [],
|
| 4 |
-
"deviceSizes": [
|
| 5 |
-
640,
|
| 6 |
-
750,
|
| 7 |
-
828,
|
| 8 |
-
1080,
|
| 9 |
-
1200,
|
| 10 |
-
1920,
|
| 11 |
-
2048,
|
| 12 |
-
3840
|
| 13 |
-
],
|
| 14 |
-
"imageSizes": [
|
| 15 |
-
16,
|
| 16 |
-
32,
|
| 17 |
-
48,
|
| 18 |
-
64,
|
| 19 |
-
96,
|
| 20 |
-
128,
|
| 21 |
-
256,
|
| 22 |
-
384
|
| 23 |
-
],
|
| 24 |
-
"formats": [
|
| 25 |
-
"avif"
|
| 26 |
-
],
|
| 27 |
-
"qualityAllowlist": [
|
| 28 |
-
25,
|
| 29 |
-
50,
|
| 30 |
-
75,
|
| 31 |
-
100
|
| 32 |
-
],
|
| 33 |
-
"minimumCacheTTL": 60,
|
| 34 |
-
"maxCacheSize": 104857600,
|
| 35 |
-
"preoptimizeManifest": []
|
| 36 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dist/server/lib/mistral-summarize.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// Built: 2026-03-01T21:17:29.340Z
|
| 2 |
-
async function e(e,t,n){let r=await fetch(`https://api.mistral.ai/v1/chat/completions`,{method:`POST`,headers:{Authorization:`Bearer ${n}`,"Content-Type":`application/json`},body:JSON.stringify({model:`mistral-small-latest`,messages:[{role:`system`,content:`You summarize articles for audio playback. Write 2–4 natural spoken sentences. No bullet points, no markdown, no special characters. Clean up any HTML artifacts or boilerplate.`},{role:`user`,content:`Title: ${e}\n\n${t}`}],max_tokens:350,temperature:.4})});if(!r.ok){let e=await r.text().catch(()=>r.statusText);throw Error(`Mistral summarize error ${r.status}: ${e}`)}return((await r.json()).choices?.[0]?.message?.content??``).trim()}export{e as summarizeForAudio};
|
|
|
|
|
|
|
|
|
dist/server/manifest.json
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"components": {
|
| 3 |
-
"lib/mistral-summarize": {
|
| 4 |
-
"id": "lib/mistral-summarize",
|
| 5 |
-
"filePath": "/Users/leonid/Desktop/LinkedinRadio/src/lib/mistral-summarize.ts",
|
| 6 |
-
"relativePath": "src/lib/mistral-summarize.ts",
|
| 7 |
-
"bundlePath": "server/lib/mistral-summarize.js",
|
| 8 |
-
"moduleSpecifier": "file:///Users/leonid/Desktop/LinkedinRadio/dist/server/lib/mistral-summarize.js",
|
| 9 |
-
"dependencies": [],
|
| 10 |
-
"hasNodeImports": false
|
| 11 |
-
},
|
| 12 |
-
"app/feed/loading": {
|
| 13 |
-
"id": "app/feed/loading",
|
| 14 |
-
"filePath": "/Users/leonid/Desktop/LinkedinRadio/src/app/feed/loading.tsx",
|
| 15 |
-
"relativePath": "src/app/feed/loading.tsx",
|
| 16 |
-
"bundlePath": "server/app/feed/loading.js",
|
| 17 |
-
"moduleSpecifier": "file:///Users/leonid/Desktop/LinkedinRadio/dist/server/app/feed/loading.js",
|
| 18 |
-
"dependencies": [],
|
| 19 |
-
"hasNodeImports": false
|
| 20 |
-
},
|
| 21 |
-
"app/feed/page": {
|
| 22 |
-
"id": "app/feed/page",
|
| 23 |
-
"filePath": "/Users/leonid/Desktop/LinkedinRadio/src/app/feed/page.tsx",
|
| 24 |
-
"relativePath": "src/app/feed/page.tsx",
|
| 25 |
-
"bundlePath": "server/app/feed/page.js",
|
| 26 |
-
"moduleSpecifier": "file:///Users/leonid/Desktop/LinkedinRadio/dist/server/app/feed/page.js",
|
| 27 |
-
"dependencies": [
|
| 28 |
-
"@/components/HNPostFeed"
|
| 29 |
-
],
|
| 30 |
-
"hasNodeImports": false
|
| 31 |
-
},
|
| 32 |
-
"app/layout": {
|
| 33 |
-
"id": "app/layout",
|
| 34 |
-
"filePath": "/Users/leonid/Desktop/LinkedinRadio/src/app/layout.tsx",
|
| 35 |
-
"relativePath": "src/app/layout.tsx",
|
| 36 |
-
"bundlePath": "server/app/layout.js",
|
| 37 |
-
"moduleSpecifier": "file:///Users/leonid/Desktop/LinkedinRadio/dist/server/app/layout.js",
|
| 38 |
-
"dependencies": [],
|
| 39 |
-
"hasNodeImports": false
|
| 40 |
-
},
|
| 41 |
-
"app/page": {
|
| 42 |
-
"id": "app/page",
|
| 43 |
-
"filePath": "/Users/leonid/Desktop/LinkedinRadio/src/app/page.tsx",
|
| 44 |
-
"relativePath": "src/app/page.tsx",
|
| 45 |
-
"bundlePath": "server/app/page.js",
|
| 46 |
-
"moduleSpecifier": "file:///Users/leonid/Desktop/LinkedinRadio/dist/server/app/page.js",
|
| 47 |
-
"dependencies": [],
|
| 48 |
-
"hasNodeImports": false
|
| 49 |
-
}
|
| 50 |
-
},
|
| 51 |
-
"importMap": {
|
| 52 |
-
"imports": {
|
| 53 |
-
"react": "npm:react@19",
|
| 54 |
-
"react-dom": "npm:react-dom@19",
|
| 55 |
-
"react/jsx-runtime": "npm:react@19/jsx-runtime",
|
| 56 |
-
"react/jsx-dev-runtime": "npm:react@19/jsx-dev-runtime",
|
| 57 |
-
"@/": "file:///Users/leonid/Desktop/LinkedinRadio/src/",
|
| 58 |
-
"react/jsx-runtime/": "file:///Users/leonid/Desktop/LinkedinRadio/node_modules/react/jsx-runtime.js/",
|
| 59 |
-
"react/jsx-dev-runtime/": "file:///Users/leonid/Desktop/LinkedinRadio/node_modules/react/jsx-dev-runtime.js/",
|
| 60 |
-
"react/": "file:///Users/leonid/Desktop/LinkedinRadio/node_modules/react/index.js/",
|
| 61 |
-
"react-dom/client/": "file:///Users/leonid/Desktop/LinkedinRadio/node_modules/react-dom/client.js/"
|
| 62 |
-
}
|
| 63 |
-
},
|
| 64 |
-
"version": "1.0.0",
|
| 65 |
-
"buildTime": "2026-03-01T21:17:29.319Z"
|
| 66 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dist/server/routes.json
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"routes": [
|
| 3 |
-
{
|
| 4 |
-
"path": "/",
|
| 5 |
-
"filePath": "page.tsx",
|
| 6 |
-
"segments": [],
|
| 7 |
-
"params": [],
|
| 8 |
-
"isDynamic": false
|
| 9 |
-
},
|
| 10 |
-
{
|
| 11 |
-
"path": "/feed",
|
| 12 |
-
"filePath": "feed/page.tsx",
|
| 13 |
-
"segments": [
|
| 14 |
-
{
|
| 15 |
-
"type": "static",
|
| 16 |
-
"value": "feed"
|
| 17 |
-
}
|
| 18 |
-
],
|
| 19 |
-
"params": [],
|
| 20 |
-
"isDynamic": false
|
| 21 |
-
}
|
| 22 |
-
],
|
| 23 |
-
"layouts": [
|
| 24 |
-
{
|
| 25 |
-
"path": "/",
|
| 26 |
-
"filePath": "layout.tsx"
|
| 27 |
-
}
|
| 28 |
-
],
|
| 29 |
-
"loading": [
|
| 30 |
-
{
|
| 31 |
-
"path": "/feed",
|
| 32 |
-
"filePath": "feed/loading.tsx",
|
| 33 |
-
"componentId": "loading:/feed"
|
| 34 |
-
}
|
| 35 |
-
],
|
| 36 |
-
"errors": [],
|
| 37 |
-
"notFound": [],
|
| 38 |
-
"apiRoutes": [],
|
| 39 |
-
"ogImages": [],
|
| 40 |
-
"generated": "2026-03-01T21:17:27.149Z"
|
| 41 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/voxtral-local.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
-
//
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
// Always fetch from HuggingFace Hub, never look for local files
|
| 5 |
-
env.allowLocalModels = false
|
| 6 |
|
| 7 |
const MODEL_ID = 'onnx-community/Voxtral-Mini-3B-2507-ONNX'
|
| 8 |
|
|
@@ -14,13 +12,24 @@ export interface LoadProgress {
|
|
| 14 |
message: string
|
| 15 |
}
|
| 16 |
|
| 17 |
-
//
|
|
|
|
| 18 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 19 |
let _processor: any = null
|
| 20 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 21 |
let _model: any = null
|
| 22 |
let _loadPromise: Promise<void> | null = null
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
/** Resample a WebM/any audio blob to a 16 kHz mono Float32Array */
|
| 25 |
async function decodeAudioTo16kHz(blob: Blob): Promise<Float32Array> {
|
| 26 |
const arrayBuffer = await blob.arrayBuffer()
|
|
@@ -59,14 +68,14 @@ export function loadVoxtralLocal(onProgress?: (info: LoadProgress) => void): Pro
|
|
| 59 |
const report = (progress: number, message: string) =>
|
| 60 |
onProgress?.({ status: 'loading', progress, message })
|
| 61 |
|
| 62 |
-
report(0, 'Loading
|
|
|
|
|
|
|
|
|
|
| 63 |
_processor = await VoxtralProcessor.from_pretrained(MODEL_ID)
|
| 64 |
|
| 65 |
report(5, 'Downloading model weights (this may take several minutes on first run)…')
|
| 66 |
|
| 67 |
-
// Dynamically import to avoid top-level type issues with the model class
|
| 68 |
-
const { VoxtralForConditionalGeneration } = await import('@huggingface/transformers')
|
| 69 |
-
|
| 70 |
let lastPct = 5
|
| 71 |
_model = await VoxtralForConditionalGeneration.from_pretrained(MODEL_ID, {
|
| 72 |
dtype: {
|
|
@@ -113,7 +122,6 @@ export async function transcribeWithVoxtralLocal(audioBlob: Blob): Promise<strin
|
|
| 113 |
|
| 114 |
const audio = await decodeAudioTo16kHz(audioBlob)
|
| 115 |
|
| 116 |
-
// Multimodal conversation format (runtime supports array content despite TS types)
|
| 117 |
const conversation = [
|
| 118 |
{
|
| 119 |
role: 'user',
|
|
@@ -124,8 +132,7 @@ export async function transcribeWithVoxtralLocal(audioBlob: Blob): Promise<strin
|
|
| 124 |
},
|
| 125 |
]
|
| 126 |
|
| 127 |
-
|
| 128 |
-
const text = _processor.apply_chat_template(conversation as any, { tokenize: false })
|
| 129 |
const inputs = await _processor(text, audio)
|
| 130 |
|
| 131 |
const generated_ids = await _model.generate({
|
|
@@ -133,10 +140,8 @@ export async function transcribeWithVoxtralLocal(audioBlob: Blob): Promise<strin
|
|
| 133 |
max_new_tokens: 256,
|
| 134 |
})
|
| 135 |
|
| 136 |
-
// Strip prompt tokens — keep only newly generated tokens
|
| 137 |
const promptLen = inputs.input_ids?.dims?.at(-1) ?? 0
|
| 138 |
-
const new_tokens =
|
| 139 |
-
.slice(null, [promptLen, null])
|
| 140 |
const [transcription] = _processor.batch_decode(new_tokens, { skip_special_tokens: true })
|
| 141 |
return transcription.trim()
|
| 142 |
}
|
|
|
|
| 1 |
+
// @huggingface/transformers is loaded from CDN at runtime (not bundled)
|
| 2 |
+
// to avoid OOMing the Docker build with its 139 MB package size.
|
| 3 |
+
const HF_CDN = 'https://esm.sh/@huggingface/transformers@3.8.1'
|
|
|
|
|
|
|
| 4 |
|
| 5 |
const MODEL_ID = 'onnx-community/Voxtral-Mini-3B-2507-ONNX'
|
| 6 |
|
|
|
|
| 12 |
message: string
|
| 13 |
}
|
| 14 |
|
| 15 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 16 |
+
let _lib: any = null
|
| 17 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 18 |
let _processor: any = null
|
| 19 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 20 |
let _model: any = null
|
| 21 |
let _loadPromise: Promise<void> | null = null
|
| 22 |
|
| 23 |
+
/** Lazy-load the CDN module once */
|
| 24 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 25 |
+
async function getLib(): Promise<any> {
|
| 26 |
+
if (_lib) return _lib
|
| 27 |
+
// @vite-ignore tells rolldown/vite not to try to bundle this URL
|
| 28 |
+
_lib = await import(/* @vite-ignore */ HF_CDN)
|
| 29 |
+
_lib.env.allowLocalModels = false
|
| 30 |
+
return _lib
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
/** Resample a WebM/any audio blob to a 16 kHz mono Float32Array */
|
| 34 |
async function decodeAudioTo16kHz(blob: Blob): Promise<Float32Array> {
|
| 35 |
const arrayBuffer = await blob.arrayBuffer()
|
|
|
|
| 68 |
const report = (progress: number, message: string) =>
|
| 69 |
onProgress?.({ status: 'loading', progress, message })
|
| 70 |
|
| 71 |
+
report(0, 'Loading transformers.js from CDN…')
|
| 72 |
+
const { VoxtralProcessor, VoxtralForConditionalGeneration } = await getLib()
|
| 73 |
+
|
| 74 |
+
report(2, 'Loading processor…')
|
| 75 |
_processor = await VoxtralProcessor.from_pretrained(MODEL_ID)
|
| 76 |
|
| 77 |
report(5, 'Downloading model weights (this may take several minutes on first run)…')
|
| 78 |
|
|
|
|
|
|
|
|
|
|
| 79 |
let lastPct = 5
|
| 80 |
_model = await VoxtralForConditionalGeneration.from_pretrained(MODEL_ID, {
|
| 81 |
dtype: {
|
|
|
|
| 122 |
|
| 123 |
const audio = await decodeAudioTo16kHz(audioBlob)
|
| 124 |
|
|
|
|
| 125 |
const conversation = [
|
| 126 |
{
|
| 127 |
role: 'user',
|
|
|
|
| 132 |
},
|
| 133 |
]
|
| 134 |
|
| 135 |
+
const text = _processor.apply_chat_template(conversation, { tokenize: false })
|
|
|
|
| 136 |
const inputs = await _processor(text, audio)
|
| 137 |
|
| 138 |
const generated_ids = await _model.generate({
|
|
|
|
| 140 |
max_new_tokens: 256,
|
| 141 |
})
|
| 142 |
|
|
|
|
| 143 |
const promptLen = inputs.input_ids?.dims?.at(-1) ?? 0
|
| 144 |
+
const new_tokens = generated_ids.slice(null, [promptLen, null])
|
|
|
|
| 145 |
const [transcription] = _processor.batch_decode(new_tokens, { skip_special_tokens: true })
|
| 146 |
return transcription.trim()
|
| 147 |
}
|
vite.config.ts
CHANGED
|
@@ -21,15 +21,4 @@ export default defineConfig({
|
|
| 21 |
'@': path.resolve(__dirname, 'src'),
|
| 22 |
},
|
| 23 |
},
|
| 24 |
-
build: {
|
| 25 |
-
rollupOptions: {
|
| 26 |
-
external: ['@huggingface/transformers'],
|
| 27 |
-
output: {
|
| 28 |
-
// Load the package from CDN at runtime instead of bundling it
|
| 29 |
-
paths: {
|
| 30 |
-
'@huggingface/transformers': 'https://esm.sh/@huggingface/transformers@3.8.1',
|
| 31 |
-
},
|
| 32 |
-
},
|
| 33 |
-
},
|
| 34 |
-
},
|
| 35 |
})
|
|
|
|
| 21 |
'@': path.resolve(__dirname, 'src'),
|
| 22 |
},
|
| 23 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
})
|