@qvac/harness 0.7.1 → 0.7.2
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.
- package/CHANGELOG.md +24 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/lib/models/model-catalog.d.ts +3 -0
- package/dist-lib/lib/models/model-catalog.js +18 -2
- package/dist-lib/lib/models/model-pool.d.ts +2 -1
- package/dist-lib/lib/models/model-pool.js +4 -2
- package/dist-lib/lib/models/tts-model-default.js +3 -3
- package/dist-lib/lib/tools/web-fetch.js +39 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,28 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.7.2] - 2026-10-01
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `web_fetch` attaches a plain-text file (.txt, .csv, .json…) to the chat
|
|
16
|
+
besides returning its text, so the user can open or save it and `exec` can
|
|
17
|
+
stage it to read past the returned text. The result carries its
|
|
18
|
+
`attachmentId`, `fileName` and `byteLength`. A body cut at the fetch cap is
|
|
19
|
+
not attached, and a host that cannot store files gets the text alone, as
|
|
20
|
+
with page images. (#147, #148)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Speech sounds cleaner on desktops. Devices on the `mid` profile and up now
|
|
25
|
+
speak with Supertonic 3 FP16 (`Supertonic3-FP16`, 207 MB) instead of Q8_0
|
|
26
|
+
(127 MB), at 16 inference steps instead of 5, which removes the artifacts
|
|
27
|
+
the 5-step output had. Synthesis takes about 2.7× as long, still around 7×
|
|
28
|
+
faster than real time on an M-series Mac. Phones (`low`) and `tiny` devices
|
|
29
|
+
keep Q4_0 at 5 steps. A host that prefetches `infraModelsFor` downloads the
|
|
30
|
+
FP16 model once; `Supertonic3-Q8_0` stays in the catalog, and now also runs
|
|
31
|
+
at 16 steps when a host asks for it by name. (#149)
|
|
32
|
+
|
|
11
33
|
## [0.7.1] - 2026-09-30
|
|
12
34
|
|
|
13
35
|
### Added
|
|
@@ -1019,7 +1041,8 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
|
|
|
1019
1041
|
- GPU stats are read through `bare-gpu-info`, so every dependency installs from
|
|
1020
1042
|
the public npm registry. (#12)
|
|
1021
1043
|
|
|
1022
|
-
[Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.7.
|
|
1044
|
+
[Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.7.2...HEAD
|
|
1045
|
+
[0.7.2]: https://github.com/tetherto/qvac-harness/compare/v0.7.1...v0.7.2
|
|
1023
1046
|
[0.7.1]: https://github.com/tetherto/qvac-harness/compare/v0.7.0...v0.7.1
|
|
1024
1047
|
[0.7.0]: https://github.com/tetherto/qvac-harness/compare/v0.6.0...v0.7.0
|
|
1025
1048
|
[0.6.0]: https://github.com/tetherto/qvac-harness/compare/v0.5.0...v0.6.0
|
package/dist-lib/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { buildWavBuffer } from './lib/wav-encode.js';
|
|
|
21
21
|
import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeOnce, transcribeStreaming, SUPERTONIC_SAMPLE_RATE } from './lib/capabilities/voice.js';
|
|
22
22
|
import { pushQueue } from './lib/queue.js';
|
|
23
23
|
import { AbortedError, raceAbort } from './lib/helpers/race-abort.js';
|
|
24
|
-
import { audioGenDownloadMetadata, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, curatedModelAsset, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
|
|
24
|
+
import { audioGenDownloadMetadata, categoryModelConfig, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, curatedModelAsset, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
|
|
25
25
|
import { createModelStatusCache, customModelRows, invalidateModelStatusCache, mergeCustomRows, listModels } from './lib/models/model-list.js';
|
|
26
26
|
import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
|
|
27
27
|
import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
|
|
@@ -662,6 +662,7 @@ export class Harness extends ReadyResource {
|
|
|
662
662
|
modelType: 'tts-ggml',
|
|
663
663
|
addon: 'tts',
|
|
664
664
|
modelConfig: TTS_MODEL_CONFIG,
|
|
665
|
+
modelConfigFor: categoryModelConfig,
|
|
665
666
|
loads: this._loads
|
|
666
667
|
});
|
|
667
668
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ModelRegistryEntry, ToolDialect } from '@qvac/sdk';
|
|
2
2
|
import type { ImageModelSet } from '../tools/generate-image.ts';
|
|
3
3
|
import type { MusicModelStages } from '../tools/generate-music.ts';
|
|
4
|
+
import type { ModelSrc } from './context.ts';
|
|
4
5
|
import type { ModelAddon } from './model-source.ts';
|
|
5
6
|
export interface SdkModelAsset extends ModelRegistryEntry {
|
|
6
7
|
readonly src: string;
|
|
@@ -22,8 +23,10 @@ export interface CategoryDefault {
|
|
|
22
23
|
readonly name: string;
|
|
23
24
|
readonly category: ModelCategory;
|
|
24
25
|
readonly asset: SdkModelAsset;
|
|
26
|
+
readonly modelConfig?: Readonly<Record<string, unknown>>;
|
|
25
27
|
}
|
|
26
28
|
export declare const CATEGORY_DEFAULTS: ReadonlyArray<CategoryDefault>;
|
|
29
|
+
export declare function categoryModelConfig(modelSrc: ModelSrc): Readonly<Record<string, unknown>> | undefined;
|
|
27
30
|
export declare function defaultVoiceModel(category: ModelCategory): string;
|
|
28
31
|
export declare function defaultTtsFor(profile: CapabilityProfile): CategoryDefault;
|
|
29
32
|
export interface CatalogModel {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0, AUDIOGEN_ACESTEP_V15_SFT_Q8_0, AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M, AUDIOGEN_ACESTEP_V15_TURBO_Q8_0, AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0, AUDIOGEN_VAE_BF16, EMBEDDINGGEMMA_300M_Q4_0, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, HEALTHCARE_1_7B_MEDICAL_Q4_K_M, HEALTHCARE_4B_MEDICAL_Q4_K_M, MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_4B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_9B_MULTIMODAL_F16, MMPROJ_QWEN3_6_27B_MULTIMODAL_F16, MMPROJ_QWEN3_6_35B_A3B_MULTIMODAL_F16, MMPROJ_QWEN3_8_27B_MULTIMODAL_F16, QWEN3_5_2B_MULTIMODAL_Q4_K_M, QWEN3_5_4B_MULTIMODAL_Q4_K_M, QWEN3_5_9B_MULTIMODAL_Q4_K_M, PARAKEET_TDT_0_6B_V3_Q8_0, QWEN3_4B_Q4_K_M, QWEN3_6_27B_MULTIMODAL_Q4_K_XL, QWEN3_6_35B_A3B_MULTIMODAL_Q4_K_M, QWEN3_8_27B_MULTIMODAL_UD_Q4_K_XL, SD_V2_1_1B_Q8_0, SDXL_BASE_1_0_3B_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q8_0 } from '@qvac/sdk/models';
|
|
1
|
+
import { AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0, AUDIOGEN_ACESTEP_V15_SFT_Q8_0, AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M, AUDIOGEN_ACESTEP_V15_TURBO_Q8_0, AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0, AUDIOGEN_VAE_BF16, EMBEDDINGGEMMA_300M_Q4_0, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, HEALTHCARE_1_7B_MEDICAL_Q4_K_M, HEALTHCARE_4B_MEDICAL_Q4_K_M, MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_4B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_9B_MULTIMODAL_F16, MMPROJ_QWEN3_6_27B_MULTIMODAL_F16, MMPROJ_QWEN3_6_35B_A3B_MULTIMODAL_F16, MMPROJ_QWEN3_8_27B_MULTIMODAL_F16, QWEN3_5_2B_MULTIMODAL_Q4_K_M, QWEN3_5_4B_MULTIMODAL_Q4_K_M, QWEN3_5_9B_MULTIMODAL_Q4_K_M, PARAKEET_TDT_0_6B_V3_Q8_0, QWEN3_4B_Q4_K_M, QWEN3_6_27B_MULTIMODAL_Q4_K_XL, QWEN3_6_35B_A3B_MULTIMODAL_Q4_K_M, QWEN3_8_27B_MULTIMODAL_UD_Q4_K_XL, SD_V2_1_1B_Q8_0, SDXL_BASE_1_0_3B_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_FP16, TTS_MULTILINGUAL_SUPERTONIC3_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q8_0 } from '@qvac/sdk/models';
|
|
2
2
|
import { detectToolDialect } from './tool-dialect.js';
|
|
3
3
|
import { defaultTtsModelName } from './tts-model-default.js';
|
|
4
4
|
import { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName } from './model-catalog-constants.js';
|
|
@@ -62,9 +62,21 @@ export function defaultProjectionMap() {
|
|
|
62
62
|
}
|
|
63
63
|
return map;
|
|
64
64
|
}
|
|
65
|
+
const TTS_FULL_STEPS = { ttsNumInferenceSteps: 16 };
|
|
65
66
|
export const CATEGORY_DEFAULTS = [
|
|
66
67
|
{ name: 'Supertonic3-Q4_0', category: 'tts', asset: TTS_MULTILINGUAL_SUPERTONIC3_Q4_0 },
|
|
67
|
-
{
|
|
68
|
+
{
|
|
69
|
+
name: 'Supertonic3-Q8_0',
|
|
70
|
+
category: 'tts',
|
|
71
|
+
asset: TTS_MULTILINGUAL_SUPERTONIC3_Q8_0,
|
|
72
|
+
modelConfig: TTS_FULL_STEPS
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'Supertonic3-FP16',
|
|
76
|
+
category: 'tts',
|
|
77
|
+
asset: TTS_MULTILINGUAL_SUPERTONIC3_FP16,
|
|
78
|
+
modelConfig: TTS_FULL_STEPS
|
|
79
|
+
},
|
|
68
80
|
{
|
|
69
81
|
name: 'Parakeet-TDT-0.6B-v3-Q8_0',
|
|
70
82
|
category: 'transcription',
|
|
@@ -72,6 +84,10 @@ export const CATEGORY_DEFAULTS = [
|
|
|
72
84
|
},
|
|
73
85
|
{ name: 'EmbeddingGemma-300M-Q4_0', category: 'embedding', asset: EMBEDDINGGEMMA_300M_Q4_0 }
|
|
74
86
|
];
|
|
87
|
+
export function categoryModelConfig(modelSrc) {
|
|
88
|
+
const src = typeof modelSrc === 'string' ? modelSrc : modelSrc.src;
|
|
89
|
+
return CATEGORY_DEFAULTS.find((entry) => entry.asset.src === src)?.modelConfig;
|
|
90
|
+
}
|
|
75
91
|
export function defaultVoiceModel(category) {
|
|
76
92
|
const entry = CATEGORY_DEFAULTS.find((candidate) => candidate.category === category);
|
|
77
93
|
if (!entry) {
|
|
@@ -156,8 +156,9 @@ export interface VoicePoolOptions {
|
|
|
156
156
|
modelType: string;
|
|
157
157
|
addon: ModelAddon;
|
|
158
158
|
modelConfig?: Record<string, unknown> | undefined;
|
|
159
|
+
modelConfigFor?: (modelSrc: ModelSrc) => Readonly<Record<string, unknown>> | undefined;
|
|
159
160
|
openclCacheDir?: (() => string | undefined) | undefined;
|
|
160
161
|
loads?: LoadTracker;
|
|
161
162
|
}
|
|
162
|
-
export declare function createVoicePool(loadSdk: LoadVoiceSdk, notify: NotifyFn, { modelType, addon, modelConfig, openclCacheDir, loads }: VoicePoolOptions): ModelPuddle;
|
|
163
|
+
export declare function createVoicePool(loadSdk: LoadVoiceSdk, notify: NotifyFn, { modelType, addon, modelConfig, modelConfigFor, openclCacheDir, loads }: VoicePoolOptions): ModelPuddle;
|
|
163
164
|
export {};
|
|
@@ -392,14 +392,16 @@ export function createEmbedPool(loadSdk, notify, { faults, batchSize = DEFAULT_E
|
|
|
392
392
|
...NO_IDLE_GC
|
|
393
393
|
});
|
|
394
394
|
}
|
|
395
|
-
export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig, openclCacheDir, loads = createLoadTracker() }) {
|
|
395
|
+
export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig, modelConfigFor, openclCacheDir, loads = createLoadTracker() }) {
|
|
396
396
|
const wantsGpu = modelConfig?.['useGPU'] === true;
|
|
397
397
|
let gpuRefused = false;
|
|
398
398
|
const load = async (model) => {
|
|
399
399
|
const sdk = await loadSdk();
|
|
400
400
|
const modelSrc = await resolveModelSrc(sdk, model, addon);
|
|
401
|
+
const tuned = modelConfigFor?.(modelSrc);
|
|
402
|
+
const loadConfig = tuned ? { ...modelConfig, ...tuned } : modelConfig;
|
|
401
403
|
const attempt = () => {
|
|
402
|
-
const config = gpuRefused ? { ...
|
|
404
|
+
const config = gpuRefused ? { ...loadConfig, useGPU: false } : loadConfig;
|
|
403
405
|
const openclCache = gpuRefused ? undefined : openclCacheDir?.();
|
|
404
406
|
return loads.track(sdk.loadModel({
|
|
405
407
|
modelSrc,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const TTS_MODEL_BY_PROFILE = {
|
|
2
2
|
tiny: 'Supertonic3-Q4_0',
|
|
3
3
|
low: 'Supertonic3-Q4_0',
|
|
4
|
-
mid: 'Supertonic3-
|
|
5
|
-
high: 'Supertonic3-
|
|
6
|
-
xhigh: 'Supertonic3-
|
|
4
|
+
mid: 'Supertonic3-FP16',
|
|
5
|
+
high: 'Supertonic3-FP16',
|
|
6
|
+
xhigh: 'Supertonic3-FP16'
|
|
7
7
|
};
|
|
8
8
|
export const TTS_MODEL_NAMES = [
|
|
9
9
|
...new Set(Object.values(TTS_MODEL_BY_PROFILE))
|
|
@@ -32,7 +32,8 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
|
|
|
32
32
|
'shares, when a search snippet is not enough to answer. ' +
|
|
33
33
|
'When the URL serves a file rather than a page — PDF, .docx, .xlsx, .pptx, an image, any ' +
|
|
34
34
|
'binary — this is also the tool that downloads it: the bytes become a chat attachment and ' +
|
|
35
|
-
'the result carries its attachmentId.
|
|
35
|
+
'the result carries its attachmentId. A plain-text file (.txt, .csv, .json…) is attached ' +
|
|
36
|
+
'too, and its text is also returned. It is the only way to get a remote file into the ' +
|
|
36
37
|
'chat; exec has no network and http_request truncates binary bodies.',
|
|
37
38
|
parameters: {
|
|
38
39
|
type: 'object',
|
|
@@ -133,13 +134,22 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
|
|
|
133
134
|
const imagesLine = attached.length
|
|
134
135
|
? `\n\n[Attached ${attached.length === 1 ? 'image' : 'images'}: ${attached.join(', ')} — shown to the user automatically]`
|
|
135
136
|
: '';
|
|
137
|
+
const file = isHtml
|
|
138
|
+
? null
|
|
139
|
+
: await attachTextFile(response, { url: currentUrl, mimeType, attach: ctx?.attach });
|
|
140
|
+
const fileLine = file
|
|
141
|
+
? `\n\n[Attached the file "${file.fileName}" (${Math.round(file.byteLength / 1024)}KB) — the user can open or save it; stage it with exec as inputs [{ "attachmentId": "${file.id}", "path": "${file.fileName}" }] to read past the text above]`
|
|
142
|
+
: '';
|
|
136
143
|
const video = isHtml ? pageVideoRef(body, currentUrl) : null;
|
|
137
144
|
const videoLine = video
|
|
138
145
|
? `\n\n[This page's video: ${video.url} — share this link when the user wants to watch it]`
|
|
139
146
|
: '';
|
|
140
147
|
const header = page.title ? `${page.title}\nSource: ${url}` : `Source: ${url}`;
|
|
141
148
|
return {
|
|
142
|
-
|
|
149
|
+
attachmentId: file?.id,
|
|
150
|
+
fileName: file?.fileName,
|
|
151
|
+
byteLength: file?.byteLength,
|
|
152
|
+
content: `${header}\n\n${text}${fileLine}${imagesLine}${videoLine}`,
|
|
143
153
|
data: {
|
|
144
154
|
results: [
|
|
145
155
|
{
|
|
@@ -201,6 +211,30 @@ async function attachDocument(response, { url, mimeType, attach }) {
|
|
|
201
211
|
'user to attach a file this call already attached.'
|
|
202
212
|
};
|
|
203
213
|
}
|
|
214
|
+
async function attachTextFile(response, { url, mimeType, attach }) {
|
|
215
|
+
if (!attach)
|
|
216
|
+
return null;
|
|
217
|
+
const bytes = await response.bytes?.();
|
|
218
|
+
if (!bytes || bytes.byteLength === 0 || bytes.byteLength >= MAX_PAGE_BYTES) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const mime = mimeType || 'text/plain';
|
|
222
|
+
try {
|
|
223
|
+
return await attach({
|
|
224
|
+
fileName: attachmentFileName(url, mime),
|
|
225
|
+
mimeType: mime,
|
|
226
|
+
bytes,
|
|
227
|
+
raw: true
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const TEXT_EXTENSIONS = {
|
|
235
|
+
'text/plain': 'txt',
|
|
236
|
+
'text/markdown': 'md'
|
|
237
|
+
};
|
|
204
238
|
const MIME_EXTENSIONS = {
|
|
205
239
|
'application/pdf': 'pdf',
|
|
206
240
|
'application/msword': 'doc',
|
|
@@ -234,7 +268,8 @@ function attachmentFileName(url, mimeType) {
|
|
|
234
268
|
if (extension && acceptableExtensions(mimeType).has(extension)) {
|
|
235
269
|
return base;
|
|
236
270
|
}
|
|
237
|
-
|
|
271
|
+
const trustsUrl = mimeType === 'application/octet-stream' || !isBinaryMime(mimeType);
|
|
272
|
+
if (trustsUrl && /^[a-z][\w-]*$/.test(extension)) {
|
|
238
273
|
return base;
|
|
239
274
|
}
|
|
240
275
|
return `${base}.${extensionFor(mimeType)}`;
|
|
@@ -248,7 +283,7 @@ function acceptableExtensions(mimeType) {
|
|
|
248
283
|
return accepted;
|
|
249
284
|
}
|
|
250
285
|
function extensionFor(mimeType) {
|
|
251
|
-
const known = MIME_EXTENSIONS[mimeType];
|
|
286
|
+
const known = MIME_EXTENSIONS[mimeType] ?? TEXT_EXTENSIONS[mimeType];
|
|
252
287
|
if (known) {
|
|
253
288
|
return known;
|
|
254
289
|
}
|