ldidukh commited on
Commit
fcbeeae
·
1 Parent(s): a5efc6c

Fix: load transformers.js from CDN, remove paths option; ignore dist/

Browse files
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
- import { VoxtralProcessor, env } from '@huggingface/transformers'
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
- // Use `any` for the model to avoid fighting internal-only type requirements
 
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 processor…')
 
 
 
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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 = (generated_ids as { slice: (...args: unknown[]) => unknown })
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
  })