@fugood/buttress-server 2.25.2 → 2.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,8 +8,8 @@
8
8
  */
9
9
  import type { GeneratorCacheEntry } from './generator-cache';
10
10
  export declare const TTS_TYPES: string[];
11
- /** Get the TTS backend API for a generator type (only onnx-tts today). */
12
- export declare function getTtsBackend(backend: any, _type: string): any;
11
+ /** Get the TTS backend API for a generator type. */
12
+ export declare function getTtsBackend(backend: any, type: string): any;
13
13
  export declare function getOrCreateTtsGenerator(backend: any, config: any, requestedModel?: string, logPrefix?: string): Promise<GeneratorCacheEntry>;
14
14
  export declare function releaseTtsGenerator(backend: any, entry: GeneratorCacheEntry | null | undefined, logPrefix?: string): Promise<void>;
15
15
  export type SynthesizeRequest = {
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ import type { ReadableStream } from 'node:stream/web';
3
+ import type { ServiceContext, EventStream, Expand } from '../types';
4
+ export declare const schemas: {
5
+ initContext: z.ZodTuple<[z.ZodString, z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>], null>;
6
+ synthesize: z.ZodTuple<[z.ZodString, z.ZodObject<{
7
+ text: z.ZodString;
8
+ options: z.ZodOptional<z.ZodObject<{
9
+ speaker: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
10
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
11
+ speaker: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
12
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
13
+ speaker: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
14
+ }, z.ZodTypeAny, "passthrough">>>;
15
+ }, "strip", z.ZodTypeAny, {
16
+ text: string;
17
+ options?: z.objectOutputType<{
18
+ speaker: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
19
+ }, z.ZodTypeAny, "passthrough"> | undefined;
20
+ }, {
21
+ text: string;
22
+ options?: z.objectInputType<{
23
+ speaker: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
24
+ }, z.ZodTypeAny, "passthrough"> | undefined;
25
+ }>], null>;
26
+ releaseContext: z.ZodTuple<[z.ZodString], null>;
27
+ };
28
+ export interface SynthesizeResult {
29
+ filename: string;
30
+ sampling_rate: number;
31
+ channels: number;
32
+ }
33
+ export interface Service {
34
+ initContext: (ctx: ServiceContext, ...args: z.infer<typeof schemas.initContext>) => ReadableStream<Expand<EventStream>>;
35
+ synthesize: (ctx: ServiceContext, ...args: z.infer<typeof schemas.synthesize>) => Promise<SynthesizeResult>;
36
+ releaseContext: (ctx: ServiceContext, ...args: z.infer<typeof schemas.releaseContext>) => Promise<{
37
+ released: boolean;
38
+ alreadyReleased?: boolean;
39
+ }>;
40
+ }
41
+ declare const _default: Service;
42
+ export default _default;
@@ -2,6 +2,7 @@ import type { ServiceContext } from '../types';
2
2
  import type { Service as CommonService } from './common';
3
3
  import type { Service as GgmlLlmService } from './ggml-llm';
4
4
  import type { Service as GgmlSttService } from './ggml-stt';
5
+ import type { Service as GgmlTtsService } from './ggml-tts';
5
6
  import type { Service as MlxLlmService } from './mlx-llm';
6
7
  import type { Service as OnnxSttService } from './onnx-stt';
7
8
  import type { Service as OnnxTtsService } from './onnx-tts';
@@ -19,6 +20,7 @@ declare const services: {
19
20
  }, id: any, audioData: any, options: any): Promise<any>;
20
21
  releaseContext: ({ backend, session }: ServiceContext, id: any, force: any) => Promise<any>;
21
22
  };
23
+ ggmlTts: GgmlTtsService;
22
24
  mlxLlm: MlxLlmService;
23
25
  onnxStt: OnnxSttService;
24
26
  onnxTts: OnnxTtsService;
@@ -32,6 +34,7 @@ export type Services = {
32
34
  common: ClientService<CommonService>;
33
35
  ggmlLlm: ClientService<GgmlLlmService>;
34
36
  ggmlStt: ClientService<GgmlSttService>;
37
+ ggmlTts: ClientService<GgmlTtsService>;
35
38
  mlxLlm: ClientService<MlxLlmService>;
36
39
  onnxStt: ClientService<OnnxSttService>;
37
40
  onnxTts: ClientService<OnnxTtsService>;
package/lib/types.d.ts CHANGED
@@ -25,7 +25,7 @@ export type RuntimeConfig = {
25
25
  huggingface_token?: string;
26
26
  session_cache?: SessionCacheConfig;
27
27
  } & Record<string, any>;
28
- export type GeneratorType = 'ggml-llm' | 'ggml-stt' | 'mlx-llm' | 'onnx-stt' | 'onnx-tts';
28
+ export type GeneratorType = 'ggml-llm' | 'ggml-stt' | 'ggml-tts' | 'mlx-llm' | 'onnx-stt' | 'onnx-tts';
29
29
  export type GeneratorConfig = {
30
30
  type: GeneratorType;
31
31
  } & Record<string, any>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Serve a local file from a route.
3
+ *
4
+ * Not elysia's `file()` on Bun: it returns an ElysiaFile, which elysia's
5
+ * adapter dispatches by constructor *name* — and bundling renames the class
6
+ * (always under identifier minification, and under plain bundling whenever
7
+ * another module declares the same symbol). The compiled distribution shipped
8
+ * that way, degrading every file response to `String(response)` — HTTP 200
9
+ * "[object Object]". A BunFile dispatches through the global `Blob` case onto
10
+ * the same `handleFile` path, and globals survive any bundler. The Node
11
+ * fallback runs unbundled from lib/, where `file()` is safe.
12
+ * Covered by scripts/__tests__/bundle-file-response.test.js.
13
+ */
14
+ export declare const fileResponse: (path: string) => any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fugood/buttress-server",
3
- "version": "2.25.2",
3
+ "version": "2.25.3",
4
4
  "main": "lib/index.mjs",
5
5
  "types": "lib/index.d.ts",
6
6
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@elysiajs/cors": "^1.1.1",
41
41
  "@elysiajs/node": "^1.4.2",
42
- "@fugood/llama.node": "1.7.14",
42
+ "@fugood/llama.node": "1.8.0-rc.2",
43
43
  "@fugood/whisper.node": "^1.1.1",
44
44
  "@huggingface/gguf": "^0.3.2",
45
45
  "@iarna/toml": "^3.0.0",
@@ -69,5 +69,5 @@
69
69
  "tsdown": "^0.22.4",
70
70
  "typescript": "^7.0.2"
71
71
  },
72
- "gitHead": "c1dd33ba3bfaecacf5650b93f55cb5211f61f077"
72
+ "gitHead": "0a712b850bf86dc30defb32658122438a38d39ba"
73
73
  }
@@ -582,6 +582,27 @@
582
582
  </div>
583
583
  </div>
584
584
 
585
+ <!-- GGML-TTS Status (hidden unless active) -->
586
+ <div class="card" id="ggmlTtsCard" style="display:none">
587
+ <div class="card-header">
588
+ <span class="card-title">GGML-TTS Status</span>
589
+ <span class="badge badge-info" id="ggmlTtsGeneratorCount">0 generators</span>
590
+ </div>
591
+ <div id="ggmlTtsGenerators"></div>
592
+ <div class="section">
593
+ <div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
594
+ <div class="collapsible-content" id="ggmlTtsModelHistory">
595
+ <div class="empty-state">No model load history</div>
596
+ </div>
597
+ </div>
598
+ <div class="section">
599
+ <div class="section-title collapsible" onclick="toggleSection(this)">Synthesis History</div>
600
+ <div class="collapsible-content" id="ggmlTtsSynthesisHistory">
601
+ <div class="empty-state">No synthesis history</div>
602
+ </div>
603
+ </div>
604
+ </div>
605
+
585
606
  <!-- Local Functions (hidden unless enabled) -->
586
607
  <div class="card" id="functionsCard" style="display:none">
587
608
  <div class="card-header">
@@ -972,6 +993,52 @@
972
993
  })
973
994
  }
974
995
 
996
+ function renderGgmlTtsGenerators(generators) {
997
+ const container = document.getElementById('ggmlTtsGenerators')
998
+ document.getElementById('ggmlTtsGeneratorCount').textContent =
999
+ `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
1000
+
1001
+ withScrollPreserve(container, () => {
1002
+ if (generators.length === 0) {
1003
+ container.innerHTML = '<div class="empty-state">No GGML TTS generators loaded</div>'
1004
+ return
1005
+ }
1006
+
1007
+ container.innerHTML = `
1008
+ <div class="table-wrapper">
1009
+ <div class="table-inner">
1010
+ <table>
1011
+ <thead>
1012
+ <tr>
1013
+ <th>ID</th>
1014
+ <th>Model</th>
1015
+ <th>Quant</th>
1016
+ <th>Variant</th>
1017
+ <th>GPU</th>
1018
+ <th>Model Size</th>
1019
+ <th>Context</th>
1020
+ </tr>
1021
+ </thead>
1022
+ <tbody>
1023
+ ${generators.map(g => `
1024
+ <tr>
1025
+ <td class="mono">${escapeHtml(g.id?.slice(0, 8) || '-')}</td>
1026
+ <td>${formatModelName(g.repoId)}${g.vocoderRepoId ? '<br><span style="font-size:11px;color:var(--text-muted)">+ ' + formatModelName(g.vocoderRepoId) + '</span>' : ''}</td>
1027
+ <td>${escapeHtml(g.quantization || '-')}</td>
1028
+ <td>${escapeHtml(g.variant || '-')}</td>
1029
+ <td><span class="badge ${g.useGpu ? 'badge-success' : 'badge-info'}">${g.useGpu ? 'yes' : 'no'}</span></td>
1030
+ <td>${formatBytes(g.modelBytes)}</td>
1031
+ <td>${g.hasContext ? '<span class="badge badge-success">loaded</span>' : '-'}</td>
1032
+ </tr>
1033
+ `).join('')}
1034
+ </tbody>
1035
+ </table>
1036
+ </div>
1037
+ </div>
1038
+ `
1039
+ })
1040
+ }
1041
+
975
1042
  // Escape HTML
976
1043
  function escapeHtml(str) {
977
1044
  if (str == null) return ''
@@ -999,6 +1066,7 @@
999
1066
  const mlx = status.mlxLlm || {}
1000
1067
  const onnxStt = status.onnxStt || {}
1001
1068
  const onnxTts = status.onnxTts || {}
1069
+ const ggmlTts = status.ggmlTts || {}
1002
1070
 
1003
1071
  // LLM
1004
1072
  renderLlmGenerators(llm.generators || [])
@@ -1178,6 +1246,38 @@
1178
1246
  },
1179
1247
  ])
1180
1248
 
1249
+ // GGML-TTS
1250
+ const hasGgmlTts = (ggmlTts.generators?.length > 0) ||
1251
+ (ggmlTts.history?.modelLoads?.length > 0) ||
1252
+ (ggmlTts.history?.syntheses?.length > 0)
1253
+ document.getElementById('ggmlTtsCard').style.display = hasGgmlTts ? '' : 'none'
1254
+ renderGgmlTtsGenerators(ggmlTts.generators || [])
1255
+
1256
+ renderHistory('ggmlTtsModelHistory', ggmlTts.history?.modelLoads || [], [
1257
+ { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1258
+ { label: 'Model', render: i => formatModelName(i.repoId) },
1259
+ { label: 'Quant', render: i => escapeHtml(i.quantization || '-') },
1260
+ { label: 'Variant', render: i => escapeHtml(i.variant || '-') },
1261
+ { label: 'Vocoder', render: i => i.vocoderRepoId ? formatModelName(i.vocoderRepoId) : '-' },
1262
+ { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1263
+ { label: 'Status', render: i => i.success ?
1264
+ '<span class="badge badge-success">Success</span>' :
1265
+ `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1266
+ },
1267
+ ])
1268
+
1269
+ renderHistory('ggmlTtsSynthesisHistory', ggmlTts.history?.syntheses || [], [
1270
+ { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1271
+ { label: 'Model', render: i => formatModelName(i.repoId) },
1272
+ { label: 'Variant', render: i => escapeHtml(i.variant || '-') },
1273
+ { label: 'Text Len', render: i => i.textLength ?? '-' },
1274
+ { label: 'Duration', render: i => i.cached ? '<span class="badge badge-info">Cached</span>' : `${(i.durationMs / 1000).toFixed(1)}s` },
1275
+ { label: 'Status', render: i => i.success ?
1276
+ '<span class="badge badge-success">Success</span>' :
1277
+ `<span class="badge badge-error">Failed</span>`
1278
+ },
1279
+ ])
1280
+
1181
1281
  renderFunctionsStatus(status.functions || {})
1182
1282
  }
1183
1283
 
@@ -1271,7 +1371,7 @@
1271
1371
  const response = await fetch('/buttress/status')
1272
1372
  const data = await response.json()
1273
1373
  // Direct JSON response (not tRPC wrapped)
1274
- if (data.ggmlLlm || data.ggmlStt || data.mlxLlm || data.onnxStt || data.onnxTts) {
1374
+ if (data.ggmlLlm || data.ggmlStt || data.mlxLlm || data.onnxStt || data.onnxTts || data.ggmlTts) {
1275
1375
  updateUI(data)
1276
1376
  setConnectionStatus('connected')
1277
1377
  }