@qvac/harness 0.8.5 → 0.8.6
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 +55 -1
- package/README.md +49 -0
- package/dist-lib/index.d.ts +31 -2
- package/dist-lib/index.js +365 -53
- package/dist-lib/lib/audio-transcode-bare.d.ts +1 -1
- package/dist-lib/lib/audio-transcode-bare.js +22 -25
- package/dist-lib/lib/audio-transcode-node.d.ts +1 -1
- package/dist-lib/lib/audio-transcode-node.js +1 -3
- package/dist-lib/lib/capabilities/audio-resample.d.ts +1 -1
- package/dist-lib/lib/capabilities/audio-resample.js +7 -8
- package/dist-lib/lib/capabilities/completion.d.ts +5 -2
- package/dist-lib/lib/capabilities/completion.js +22 -4
- package/dist-lib/lib/capabilities/download.d.ts +3 -1
- package/dist-lib/lib/capabilities/download.js +1 -1
- package/dist-lib/lib/capabilities/preview-model-source.d.ts +1 -0
- package/dist-lib/lib/capabilities/preview-model-source.js +1 -0
- package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
- package/dist-lib/lib/capabilities/voice.d.ts +5 -1
- package/dist-lib/lib/capabilities/voice.js +94 -26
- package/dist-lib/lib/connect.d.ts +4 -1
- package/dist-lib/lib/connect.js +14 -1
- package/dist-lib/lib/contract-types.d.ts +10 -0
- package/dist-lib/lib/models/download-gate.d.ts +20 -0
- package/dist-lib/lib/models/download-gate.js +73 -0
- package/dist-lib/lib/models/memory-cap.d.ts +13 -0
- package/dist-lib/lib/models/memory-cap.js +21 -0
- package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
- package/dist-lib/lib/models/model-catalog-constants.js +9 -1
- package/dist-lib/lib/models/model-catalog.d.ts +6 -2
- package/dist-lib/lib/models/model-catalog.js +35 -3
- package/dist-lib/lib/models/model-handle.d.ts +8 -1
- package/dist-lib/lib/models/model-handle.js +20 -2
- package/dist-lib/lib/models/model-list.d.ts +8 -1
- package/dist-lib/lib/models/model-list.js +23 -0
- package/dist-lib/lib/models/model-pool.d.ts +33 -5
- package/dist-lib/lib/models/model-pool.js +97 -29
- package/dist-lib/lib/serve.js +18 -2
- package/dist-lib/lib/tools/generate-image.d.ts +4 -2
- package/dist-lib/lib/tools/generate-image.js +5 -2
- package/dist-lib/lib/tools/generate-music.d.ts +4 -2
- package/dist-lib/lib/tools/generate-music.js +5 -2
- package/dist-lib/lib/tools/translate.d.ts +3 -1
- package/dist-lib/lib/tools/translate.js +6 -1
- package/dist-lib/schema/harness.js +41 -3
- package/dist-lib/spec/bind.js +9 -0
- package/dist-lib/spec/calls.js +3 -0
- package/dist-lib/spec/hrpc/hrpc.json +39 -0
- package/dist-lib/spec/hrpc/index.js +39 -3
- package/dist-lib/spec/hrpc/messages.js +93 -10
- package/dist-lib/spec/hyperschema/index.js +93 -10
- package/dist-lib/spec/hyperschema/schema.json +57 -1
- package/dist-lib/spec/hyperschema/types.d.ts +19 -2
- package/package.json +4 -4
|
@@ -1,2 +1,2 @@
|
|
|
1
|
+
export declare function decodeToPcm16kMono(input: Uint8Array): AsyncGenerator<Uint8Array>;
|
|
1
2
|
export declare function transcodeToWav16kMono(input: Uint8Array): Promise<Uint8Array | null>;
|
|
2
|
-
export declare function transcodeToPcm16kMono(input: Uint8Array): Promise<Uint8Array | null>;
|
|
@@ -92,13 +92,12 @@ const YIELD_EVERY_PACKETS = 32;
|
|
|
92
92
|
function yieldToLoop() {
|
|
93
93
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
94
94
|
}
|
|
95
|
-
async function decodeToPcmChunks(inputFormat, { stream, decoder, rate }) {
|
|
96
|
-
const chunks = [];
|
|
95
|
+
async function* decodeToPcmChunks(inputFormat, { stream, decoder, rate }) {
|
|
97
96
|
const packet = new ffmpeg.Packet();
|
|
98
97
|
const flush = new ffmpeg.Packet();
|
|
99
98
|
const frame = new ffmpeg.Frame();
|
|
100
99
|
const samples = new ffmpeg.Samples();
|
|
101
|
-
const target = { frame, samples, chunks, rate };
|
|
100
|
+
const target = { frame, samples, chunks: [], rate };
|
|
102
101
|
let resampler = null;
|
|
103
102
|
let packets = 0;
|
|
104
103
|
try {
|
|
@@ -107,13 +106,15 @@ async function decodeToPcmChunks(inputFormat, { stream, decoder, rate }) {
|
|
|
107
106
|
resampler = drainDecodedFrames(decoder, resampler, target);
|
|
108
107
|
}
|
|
109
108
|
packet.unref();
|
|
109
|
+
yield* target.chunks.splice(0);
|
|
110
110
|
if (++packets % YIELD_EVERY_PACKETS === 0) {
|
|
111
111
|
await yieldToLoop();
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
decoder.sendPacket(flush);
|
|
115
115
|
resampler = drainDecodedFrames(decoder, resampler, target);
|
|
116
|
-
chunks.
|
|
116
|
+
yield* target.chunks.splice(0);
|
|
117
|
+
yield* flushResampler(resampler, samples, rate);
|
|
117
118
|
}
|
|
118
119
|
finally {
|
|
119
120
|
resampler?.destroy();
|
|
@@ -121,35 +122,31 @@ async function decodeToPcmChunks(inputFormat, { stream, decoder, rate }) {
|
|
|
121
122
|
flush.destroy();
|
|
122
123
|
packet.destroy();
|
|
123
124
|
}
|
|
124
|
-
return chunks;
|
|
125
125
|
}
|
|
126
|
-
async function
|
|
127
|
-
|
|
126
|
+
export async function* decodeToPcm16kMono(input) {
|
|
127
|
+
const inputFormat = new ffmpeg.InputFormatContext(bufferReadIO(input));
|
|
128
128
|
let decoder = null;
|
|
129
129
|
try {
|
|
130
|
-
const
|
|
131
|
-
inputFormat = format;
|
|
132
|
-
const stream = format.getBestStream(ffmpeg.constants.mediaTypes.AUDIO);
|
|
130
|
+
const stream = inputFormat.getBestStream(ffmpeg.constants.mediaTypes.AUDIO);
|
|
133
131
|
decoder = stream.decoder();
|
|
134
132
|
decoder.open();
|
|
135
|
-
|
|
136
|
-
return chunks.length === 0 ? null : b4a.concat(chunks);
|
|
137
|
-
}
|
|
138
|
-
catch (e) {
|
|
139
|
-
hlog('audio decode/resample failed', { error: errorMessage(e) });
|
|
140
|
-
return null;
|
|
133
|
+
yield* decodeToPcmChunks(inputFormat, { stream, decoder, rate: TARGET_SAMPLE_RATE });
|
|
141
134
|
}
|
|
142
135
|
finally {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
inputFormat?.destroy();
|
|
136
|
+
decoder?.destroy();
|
|
137
|
+
inputFormat.destroy();
|
|
147
138
|
}
|
|
148
139
|
}
|
|
149
140
|
export async function transcodeToWav16kMono(input) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
141
|
+
const chunks = [];
|
|
142
|
+
try {
|
|
143
|
+
for await (const chunk of decodeToPcm16kMono(input)) {
|
|
144
|
+
chunks.push(chunk);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
hlog('audio decode/resample failed', { error: errorMessage(e) });
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return chunks.length === 0 ? null : buildWavBuffer(b4a.concat(chunks));
|
|
155
152
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function transcodeToWav16kMono(_input: Uint8Array): Promise<Uint8Array | null>;
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function decodeToPcm16kMono(_input: Uint8Array): AsyncGenerator<Uint8Array>;
|
|
@@ -11,5 +11,5 @@ export interface ResampledAudio {
|
|
|
11
11
|
readonly transcoded: boolean;
|
|
12
12
|
}
|
|
13
13
|
export declare function resampleForTranscription(bytes: Uint8Array): Promise<ResampledAudio>;
|
|
14
|
-
export declare function pcmForTranscription(bytes: Uint8Array):
|
|
14
|
+
export declare function pcmForTranscription(bytes: Uint8Array): AsyncGenerator<Uint8Array>;
|
|
15
15
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import b4a from 'b4a';
|
|
2
|
-
import {
|
|
2
|
+
import { decodeToPcm16kMono, transcodeToWav16kMono } from '#audio-transcode';
|
|
3
3
|
const TARGET_SAMPLE_RATE = 16000;
|
|
4
4
|
const TARGET_CHANNELS = 1;
|
|
5
5
|
const TARGET_BITS_PER_SAMPLE = 16;
|
|
@@ -73,12 +73,11 @@ function wavPcmData(bytes) {
|
|
|
73
73
|
}
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
|
-
export async function pcmForTranscription(bytes) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
76
|
+
export async function* pcmForTranscription(bytes) {
|
|
77
|
+
const pcm = wavFormat(bytes) && !needsResample(bytes) ? wavPcmData(bytes) : null;
|
|
78
|
+
if (pcm) {
|
|
79
|
+
yield pcm;
|
|
80
|
+
return;
|
|
82
81
|
}
|
|
83
|
-
|
|
82
|
+
yield* decodeToPcm16kMono(bytes);
|
|
84
83
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompletionFinal, Tool as SdkTool, ToolDialect } from '@qvac/sdk';
|
|
1
|
+
import type { CompletionEvent as SdkCompletionEvent, CompletionFinal, Tool as SdkTool, ToolDialect } from '@qvac/sdk';
|
|
2
2
|
import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
|
|
3
3
|
import { type TextEvent } from '../helpers/thinking.ts';
|
|
4
4
|
import type { ToolCallDisplay } from '../tool-call-display.ts';
|
|
@@ -266,7 +266,7 @@ export interface InferenceTokens {
|
|
|
266
266
|
export type CompletionEvent = TextEvent | ToolCallEvent | ToolResultEvent | ToolActivityEvent | ErrorEvent | AbortedEvent | MetricsEvent | InferenceCallEvent | InferenceSentEvent;
|
|
267
267
|
export interface CompletionSdkResult {
|
|
268
268
|
requestId?: string;
|
|
269
|
-
|
|
269
|
+
events: AsyncIterable<SdkCompletionEvent>;
|
|
270
270
|
final: Promise<CompletionFinal>;
|
|
271
271
|
}
|
|
272
272
|
export type ResponseFormat = {
|
|
@@ -296,6 +296,9 @@ export interface CompletionSdk {
|
|
|
296
296
|
cancel?(params: {
|
|
297
297
|
requestId: string;
|
|
298
298
|
}): Promise<void>;
|
|
299
|
+
deleteCache?(params: {
|
|
300
|
+
kvCacheKey: string;
|
|
301
|
+
}): Promise<unknown>;
|
|
299
302
|
}
|
|
300
303
|
export interface RoundModelLease {
|
|
301
304
|
modelId: string;
|
|
@@ -14,7 +14,7 @@ import { HookPipeline } from '../hooks/pipeline.js';
|
|
|
14
14
|
import { fallbackToolLabel } from '../helpers/tool-label.js';
|
|
15
15
|
import { hlog } from '../log.js';
|
|
16
16
|
import { resolveLlmGenerationParams } from '../models/llm-presets.js';
|
|
17
|
-
import {
|
|
17
|
+
import { toolDialectFor } from '../models/model-catalog.js';
|
|
18
18
|
import { isContextOverflowError, isSdkModelWedgedError } from '../models/sdk-faults.js';
|
|
19
19
|
import { annotateLastUserTime, skillsPromptSection, systemPromptParts } from '../prompt.js';
|
|
20
20
|
import { isMetadataMethod } from '../tools/mcp/protocol.js';
|
|
@@ -459,6 +459,13 @@ function unprovenReplay(state, final) {
|
|
|
459
459
|
}
|
|
460
460
|
return state.unproven;
|
|
461
461
|
}
|
|
462
|
+
async function* contentDeltas(events) {
|
|
463
|
+
for await (const event of events) {
|
|
464
|
+
if (event.type === 'contentDelta') {
|
|
465
|
+
yield event.text;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
462
469
|
async function* runRoundTokens(result, state, { sdk, signal, startInThink = false }) {
|
|
463
470
|
const cancelThisRound = () => {
|
|
464
471
|
if (result.requestId) {
|
|
@@ -466,7 +473,7 @@ async function* runRoundTokens(result, state, { sdk, signal, startInThink = fals
|
|
|
466
473
|
}
|
|
467
474
|
};
|
|
468
475
|
signal?.addEventListener('abort', cancelThisRound, { once: true });
|
|
469
|
-
const tokens = stopAtControlToken(result.
|
|
476
|
+
const tokens = stopAtControlToken(contentDeltas(result.events), (token) => {
|
|
470
477
|
state.stopToken = token;
|
|
471
478
|
});
|
|
472
479
|
let tokenEvents = 0;
|
|
@@ -674,6 +681,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
674
681
|
let runError;
|
|
675
682
|
const runOutcomes = createRunOutcomeTracker();
|
|
676
683
|
const landed = runOutcomes.confirmedWrites;
|
|
684
|
+
let preloadCacheKey;
|
|
677
685
|
async function* runTurn() {
|
|
678
686
|
if (signal?.aborted)
|
|
679
687
|
throw new AbortedError();
|
|
@@ -753,7 +761,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
753
761
|
}
|
|
754
762
|
skillIcon = skillEmoji(skillName) ?? skillIcon;
|
|
755
763
|
}
|
|
756
|
-
const toolDialect = model ?
|
|
764
|
+
const toolDialect = model ? toolDialectFor(model) : undefined;
|
|
757
765
|
let turns = messages;
|
|
758
766
|
let preloadedSkill = null;
|
|
759
767
|
const preload = mention ?? resolveImplicitSkill(lastUserContent(messages), activeSkills);
|
|
@@ -834,7 +842,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
834
842
|
}
|
|
835
843
|
logSelection();
|
|
836
844
|
let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...turns] : turns);
|
|
837
|
-
|
|
845
|
+
preloadCacheKey = childCacheKey(cacheKey, preloadedSkill, messages.length);
|
|
846
|
+
let roundCacheKey = preloadedSkill === null ? cacheKey : preloadCacheKey;
|
|
838
847
|
const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
|
|
839
848
|
const denied = new Set();
|
|
840
849
|
const failed = new Map();
|
|
@@ -1323,7 +1332,16 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1323
1332
|
outcome: runOutcome,
|
|
1324
1333
|
error: runError
|
|
1325
1334
|
});
|
|
1335
|
+
if (preloadCacheKey) {
|
|
1336
|
+
await sdk.deleteCache?.({ kvCacheKey: preloadCacheKey }).catch(noop);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function childCacheKey(cacheKey, preloadedSkill, turnCount) {
|
|
1341
|
+
if (preloadedSkill === null || cacheKey === undefined) {
|
|
1342
|
+
return undefined;
|
|
1326
1343
|
}
|
|
1344
|
+
return `${cacheKey}/p${turnCount}`;
|
|
1327
1345
|
}
|
|
1328
1346
|
function toolDecision({ auto, skipAll, approved }) {
|
|
1329
1347
|
if (auto)
|
|
@@ -45,4 +45,6 @@ export type SdkOrLoader = DownloadSdk | (() => Promise<DownloadSdk>);
|
|
|
45
45
|
export interface DownloadEventStream extends Readable {
|
|
46
46
|
[Symbol.asyncIterator](): AsyncIterableIterator<DownloadEvent>;
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
type TerminalEvent = Exclude<DownloadEvent, DownloadProgressEvent>;
|
|
49
|
+
export declare function download(sdkOrLoader: SdkOrLoader, args: DownloadArgs, onSettled?: (event: TerminalEvent) => void): DownloadEventStream;
|
|
50
|
+
export {};
|
|
@@ -365,6 +365,7 @@ async function assess({ file, header }, taskType, deps) {
|
|
|
365
365
|
}
|
|
366
366
|
try {
|
|
367
367
|
return await deps.assessFit({
|
|
368
|
+
src: file.src,
|
|
368
369
|
sha256Checksum: file.sha256,
|
|
369
370
|
name: header.name ?? file.label,
|
|
370
371
|
contextTokens: defaultContextSize(file.src, header.contextLength)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { toolDialectFor } from '../models/model-catalog.js';
|
|
1
2
|
import { detectToolDialect } from '../models/tool-dialect.js';
|
|
2
3
|
const QWEN35 = { prose: 4.38, schema: 4.08, toolBlockTokens: 189 };
|
|
3
4
|
const DENSITY_BY_DIALECT = new Map([['qwen35', QWEN35]]);
|
|
4
5
|
export function densityFor(modelName) {
|
|
5
|
-
const dialect = modelName
|
|
6
|
+
const dialect = modelName
|
|
7
|
+
? (toolDialectFor(modelName) ?? detectToolDialect(modelName, modelName))
|
|
8
|
+
: undefined;
|
|
6
9
|
return (dialect && DENSITY_BY_DIALECT.get(dialect)) ?? QWEN35;
|
|
7
10
|
}
|
|
8
11
|
export function promptUsageParts({ system, toolSchemas }) {
|
|
@@ -75,7 +75,11 @@ export interface TranscribeStreamingOptions {
|
|
|
75
75
|
modelId: string;
|
|
76
76
|
signal?: BareAbortSignal | undefined;
|
|
77
77
|
}
|
|
78
|
-
export
|
|
78
|
+
export interface TranscribeAudioOptions extends TranscribeStreamingOptions {
|
|
79
|
+
audio: Buffer;
|
|
80
|
+
}
|
|
81
|
+
export declare function transcribeAudio(sdk: VoiceSdk, pcm: AsyncIterable<Uint8Array>, { modelId, signal, audio }: TranscribeAudioOptions): Promise<string>;
|
|
82
|
+
export declare function transcribeStreaming(sdk: VoiceSdk, pcm: AsyncIterable<Uint8Array>, opts: TranscribeStreamingOptions): Promise<string>;
|
|
79
83
|
export declare function speakStreamOnce(sdk: VoiceSdk, modelId: string, text: AsyncIterable<string>): AsyncGenerator<SpeakResult>;
|
|
80
84
|
export declare function speakable(text: string): string;
|
|
81
85
|
export declare function speakOnce(sdk: VoiceSdk, modelId: string, text: string): Promise<number[]>;
|
|
@@ -2,6 +2,7 @@ import b4a from 'b4a';
|
|
|
2
2
|
import { errorMessage } from '../guards.js';
|
|
3
3
|
import { AbortedError } from '../helpers/race-abort.js';
|
|
4
4
|
import { hlog } from '../log.js';
|
|
5
|
+
import { buildWavBuffer } from '../wav-encode.js';
|
|
5
6
|
import { normalizeForSpeech } from './speech-text.js';
|
|
6
7
|
export const SUPERTONIC_SAMPLE_RATE = 44100;
|
|
7
8
|
export function createVoiceLane() {
|
|
@@ -14,31 +15,19 @@ export function createVoiceLane() {
|
|
|
14
15
|
}
|
|
15
16
|
const TRANSCRIBE_SAMPLE_RATE = 16000;
|
|
16
17
|
const TRANSCRIBE_CHUNK_BYTES = TRANSCRIBE_SAMPLE_RATE * 2;
|
|
18
|
+
const TRANSCRIBE_STREAM_MIN_BYTES = TRANSCRIBE_CHUNK_BYTES * 60;
|
|
19
|
+
const TRANSCRIBE_SEGMENT_SECONDS = 300;
|
|
17
20
|
const PARAKEET_STREAMING = { emitPartials: false, chunkMs: 5000 };
|
|
18
21
|
const TRANSCRIBE_WITH_METADATA = false;
|
|
19
22
|
const TRANSCRIBE_PACE_MS = 40;
|
|
20
23
|
function pace(ms) {
|
|
21
24
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
25
|
}
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
async function writePaced(session, data, signal) {
|
|
26
|
-
for (let offset = 0; offset < data.length; offset += TRANSCRIBE_CHUNK_BYTES) {
|
|
26
|
+
async function writePaced(session, pcm, signal) {
|
|
27
|
+
for await (const second of pcm) {
|
|
27
28
|
if (signal?.aborted)
|
|
28
29
|
return;
|
|
29
|
-
session.write(
|
|
30
|
-
await pace(TRANSCRIBE_PACE_MS);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
async function drainUntilIdle(lastPieceAt, streamClosed, signal) {
|
|
34
|
-
const deadline = Date.now() + TRANSCRIBE_DRAIN_MAX_MS;
|
|
35
|
-
for (;;) {
|
|
36
|
-
if (signal?.aborted || streamClosed() || Date.now() >= deadline)
|
|
37
|
-
return;
|
|
38
|
-
const last = lastPieceAt();
|
|
39
|
-
if (last > 0 && Date.now() - last >= TRANSCRIBE_DRAIN_IDLE_MS) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
30
|
+
session.write(second);
|
|
42
31
|
await pace(TRANSCRIBE_PACE_MS);
|
|
43
32
|
}
|
|
44
33
|
}
|
|
@@ -59,7 +48,93 @@ function transcribePiece(piece) {
|
|
|
59
48
|
}
|
|
60
49
|
return null;
|
|
61
50
|
}
|
|
62
|
-
|
|
51
|
+
async function* inSeconds(pcm) {
|
|
52
|
+
let pending = [];
|
|
53
|
+
let size = 0;
|
|
54
|
+
for await (const chunk of pcm) {
|
|
55
|
+
pending.push(chunk);
|
|
56
|
+
size += chunk.length;
|
|
57
|
+
if (size < TRANSCRIBE_CHUNK_BYTES) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const joined = pending.length === 1 ? chunk : b4a.concat(pending);
|
|
61
|
+
let offset = 0;
|
|
62
|
+
for (; joined.length - offset >= TRANSCRIBE_CHUNK_BYTES; offset += TRANSCRIBE_CHUNK_BYTES) {
|
|
63
|
+
yield joined.subarray(offset, offset + TRANSCRIBE_CHUNK_BYTES);
|
|
64
|
+
}
|
|
65
|
+
size = joined.length - offset;
|
|
66
|
+
pending = size > 0 ? [joined.subarray(offset)] : [];
|
|
67
|
+
}
|
|
68
|
+
if (size > 0) {
|
|
69
|
+
yield b4a.concat(pending);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function readHead(seconds, signal) {
|
|
73
|
+
const head = { seconds: [], bytes: 0 };
|
|
74
|
+
try {
|
|
75
|
+
while (head.bytes < TRANSCRIBE_STREAM_MIN_BYTES && !signal?.aborted) {
|
|
76
|
+
const next = await seconds.next();
|
|
77
|
+
if (next.done) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
head.seconds.push(next.value);
|
|
81
|
+
head.bytes += next.value.length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
hlog('audio decode/resample failed', { error: errorMessage(err) });
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return head;
|
|
89
|
+
}
|
|
90
|
+
async function* resume(head, rest) {
|
|
91
|
+
yield* head.seconds;
|
|
92
|
+
yield* { [Symbol.asyncIterator]: () => rest };
|
|
93
|
+
}
|
|
94
|
+
export async function transcribeAudio(sdk, pcm, { modelId, signal, audio }) {
|
|
95
|
+
const seconds = inSeconds(pcm);
|
|
96
|
+
const head = await readHead(seconds, signal);
|
|
97
|
+
if (signal?.aborted) {
|
|
98
|
+
await seconds.return(undefined);
|
|
99
|
+
throw new AbortedError();
|
|
100
|
+
}
|
|
101
|
+
if (!head || head.bytes === 0) {
|
|
102
|
+
return transcribeOnce(sdk, modelId, audio);
|
|
103
|
+
}
|
|
104
|
+
if (head.bytes < TRANSCRIBE_STREAM_MIN_BYTES) {
|
|
105
|
+
return transcribeOnce(sdk, modelId, buildWavBuffer(b4a.concat(head.seconds)));
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return await transcribeStreaming(sdk, resume(head, seconds), { modelId, signal });
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await seconds.return(undefined);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function* segment(first, rest) {
|
|
115
|
+
yield first;
|
|
116
|
+
for (let n = 1; n < TRANSCRIBE_SEGMENT_SECONDS; n++) {
|
|
117
|
+
const next = await rest.next();
|
|
118
|
+
if (next.done) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
yield next.value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export async function transcribeStreaming(sdk, pcm, opts) {
|
|
125
|
+
const seconds = pcm[Symbol.asyncIterator]();
|
|
126
|
+
const parts = [];
|
|
127
|
+
try {
|
|
128
|
+
for (let first = await seconds.next(); !first.done; first = await seconds.next()) {
|
|
129
|
+
parts.push(await transcribeSession(sdk, segment(first.value, seconds), opts));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
await seconds.return?.();
|
|
134
|
+
}
|
|
135
|
+
return parts.filter(Boolean).join(' ');
|
|
136
|
+
}
|
|
137
|
+
async function transcribeSession(sdk, pcm, { modelId, signal }) {
|
|
63
138
|
const session = await sdk.transcribeStream({
|
|
64
139
|
modelId,
|
|
65
140
|
metadata: TRANSCRIBE_WITH_METADATA,
|
|
@@ -67,27 +142,20 @@ export async function transcribeStreaming(sdk, pcm, { modelId, signal }) {
|
|
|
67
142
|
});
|
|
68
143
|
const onAbort = () => session.destroy();
|
|
69
144
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
70
|
-
let lastPieceAt = 0;
|
|
71
|
-
let streamClosed = false;
|
|
72
145
|
const collect = (async () => {
|
|
73
146
|
let out = '';
|
|
74
147
|
for await (const piece of session) {
|
|
75
|
-
lastPieceAt = Date.now();
|
|
76
148
|
const seg = transcribePiece(piece);
|
|
77
149
|
if (!seg) {
|
|
78
150
|
continue;
|
|
79
151
|
}
|
|
80
152
|
out += !out || seg.append ? seg.text : ` ${seg.text}`;
|
|
81
153
|
}
|
|
82
|
-
streamClosed = true;
|
|
83
154
|
return out.replace(/\s+/g, ' ').trim();
|
|
84
155
|
})();
|
|
85
|
-
collect.catch(
|
|
86
|
-
streamClosed = true;
|
|
87
|
-
});
|
|
156
|
+
collect.catch(noop);
|
|
88
157
|
try {
|
|
89
158
|
await writePaced(session, pcm, signal);
|
|
90
|
-
await drainUntilIdle(() => lastPieceAt, () => streamClosed, signal);
|
|
91
159
|
if (!signal?.aborted) {
|
|
92
160
|
session.end();
|
|
93
161
|
}
|
|
@@ -3,7 +3,7 @@ import { type Transport } from './rpc-session.ts';
|
|
|
3
3
|
import { type KvCacheDeletionResult } from './capabilities/kv-cache.ts';
|
|
4
4
|
import type { CustomModelEntry } from './models/model-catalog.ts';
|
|
5
5
|
import { type ModelSourcePreview, type PreviewModelSourceContext } from './capabilities/preview-model-source.ts';
|
|
6
|
-
import type { CapabilityInfo, CompletionContext, CredentialEntry, DetectedLanguage, DetectLanguageContext, DownloadContext, EmbedContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, InfraModel, InvokeContext, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, SkillsOptions, SkillWriteResult, SpeakContext, SpeakResult, SpeakStreamContext, ToolCatalogEntry, TranscribeContext, LoadModelContext, LoadModelResult, UnloadModelContext, UnloadModelResult, WarmModelContext } from './contract-types.ts';
|
|
6
|
+
import type { CapabilityInfo, CompletionContext, CredentialEntry, DetectedLanguage, DetectLanguageContext, DownloadContext, EmbedContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, InfraModel, InvokeContext, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, SkillsOptions, SkillWriteResult, SpeakContext, SpeakResult, SpeakStreamContext, ToolCatalogEntry, TranscribeContext, LoadModelContext, LoadModelResult, MemoryUsage, UnloadModelContext, UnloadModelResult, WarmModelContext, DownloadsAllowedOptions } from './contract-types.ts';
|
|
7
7
|
import { type EmbedDevice } from './models/embed-config.ts';
|
|
8
8
|
import type { DeleteModelSummary } from './models/model-delete.ts';
|
|
9
9
|
import type { CopyBaseResult, DeleteOverlayResult, ManagedSkill, SkillFileContent } from './tools/skills/overlay-crud.ts';
|
|
@@ -36,6 +36,9 @@ export declare class RemoteHarness extends EventEmitter implements HarnessContra
|
|
|
36
36
|
setCredentials(entries: ReadonlyArray<CredentialEntry>): Promise<void>;
|
|
37
37
|
setSkillSettings(entries: ReadonlyArray<SkillSettingEntry>): Promise<void>;
|
|
38
38
|
setModelIdleTimeout(ms: number): Promise<void>;
|
|
39
|
+
setModelMemoryCap(percent: number): Promise<void>;
|
|
40
|
+
memoryUsage(): Promise<MemoryUsage>;
|
|
41
|
+
setDownloadsAllowed({ allowed }: DownloadsAllowedOptions): Promise<void>;
|
|
39
42
|
listManagedSkills(): Promise<ManagedSkill[]>;
|
|
40
43
|
readSkillFile(name: string): Promise<SkillFileContent | null>;
|
|
41
44
|
writeSkillFile(name: string, content: string): Promise<SkillWriteResult>;
|
package/dist-lib/lib/connect.js
CHANGED
|
@@ -422,6 +422,18 @@ export class RemoteHarness extends EventEmitter {
|
|
|
422
422
|
const { client } = await this._ensure();
|
|
423
423
|
await client.setModelIdleTimeout({ ms });
|
|
424
424
|
}
|
|
425
|
+
async setModelMemoryCap(percent) {
|
|
426
|
+
const { client } = await this._ensure();
|
|
427
|
+
await client.setModelMemoryCap({ percent });
|
|
428
|
+
}
|
|
429
|
+
async memoryUsage() {
|
|
430
|
+
const { client } = await this._ensure();
|
|
431
|
+
return await client.memoryUsage({});
|
|
432
|
+
}
|
|
433
|
+
async setDownloadsAllowed({ allowed }) {
|
|
434
|
+
const { client } = await this._ensure();
|
|
435
|
+
await client.setDownloadsAllowed({ allowed });
|
|
436
|
+
}
|
|
425
437
|
async listManagedSkills() {
|
|
426
438
|
const { client } = await this._ensure();
|
|
427
439
|
const response = await client.listManagedSkills({});
|
|
@@ -566,10 +578,11 @@ export class RemoteHarness extends EventEmitter {
|
|
|
566
578
|
cachedOnly: options.cachedOnly,
|
|
567
579
|
fullRegistry: options.fullRegistry
|
|
568
580
|
});
|
|
569
|
-
return (response.models ?? []).map(({ actualSize, label, ...row }) => ({
|
|
581
|
+
return (response.models ?? []).map(({ actualSize, label, memoryBytes, ...row }) => ({
|
|
570
582
|
...row,
|
|
571
583
|
actualSize: actualSize || undefined,
|
|
572
584
|
label: label || undefined,
|
|
585
|
+
memoryBytes: memoryBytes || undefined,
|
|
573
586
|
taskType: addonTaskType(row.type)
|
|
574
587
|
}));
|
|
575
588
|
}
|
|
@@ -95,6 +95,9 @@ export interface HarnessEvent {
|
|
|
95
95
|
message?: string;
|
|
96
96
|
[key: string]: unknown;
|
|
97
97
|
}
|
|
98
|
+
export interface DownloadsAllowedOptions {
|
|
99
|
+
allowed: boolean;
|
|
100
|
+
}
|
|
98
101
|
export interface ModelState {
|
|
99
102
|
status: string;
|
|
100
103
|
model?: string;
|
|
@@ -107,6 +110,7 @@ export interface ModelState {
|
|
|
107
110
|
model: string;
|
|
108
111
|
refs: number;
|
|
109
112
|
}>;
|
|
113
|
+
addon?: string | null;
|
|
110
114
|
}
|
|
111
115
|
export interface WarmModelContext {
|
|
112
116
|
model: string;
|
|
@@ -118,6 +122,9 @@ export interface LoadModelResult {
|
|
|
118
122
|
loaded: boolean;
|
|
119
123
|
reason?: 'memory';
|
|
120
124
|
}
|
|
125
|
+
export interface MemoryUsage {
|
|
126
|
+
readonly usedBytes: number;
|
|
127
|
+
}
|
|
121
128
|
export interface UnloadModelContext {
|
|
122
129
|
model: string;
|
|
123
130
|
}
|
|
@@ -223,6 +230,9 @@ export interface HarnessContract {
|
|
|
223
230
|
agentId?: string | null;
|
|
224
231
|
}>): void | Promise<void>;
|
|
225
232
|
setModelIdleTimeout?(ms: number): void | Promise<void>;
|
|
233
|
+
setModelMemoryCap?(percent: number): void | Promise<void>;
|
|
234
|
+
memoryUsage?(): Promise<MemoryUsage>;
|
|
235
|
+
setDownloadsAllowed?(options: DownloadsAllowedOptions): void | Promise<void>;
|
|
226
236
|
listManagedSkills?(): Promise<ManagedSkill[]>;
|
|
227
237
|
readSkillFile?(name: string): Promise<SkillFileContent | null>;
|
|
228
238
|
writeSkillFile?(name: string, content: string): Promise<{
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
|
|
2
|
+
import type { ModelAddon } from './model-source.ts';
|
|
3
|
+
export interface HeldModel {
|
|
4
|
+
readonly model: string;
|
|
5
|
+
readonly addon: ModelAddon;
|
|
6
|
+
}
|
|
7
|
+
export interface DownloadGateOptions {
|
|
8
|
+
isCached(model: string, addon: ModelAddon): Promise<boolean>;
|
|
9
|
+
onHeld(held: HeldModel): void;
|
|
10
|
+
onDropped(held: HeldModel): void;
|
|
11
|
+
}
|
|
12
|
+
export type HoldFn = (model: string, addon: ModelAddon, signal?: BareAbortSignal) => Promise<void>;
|
|
13
|
+
export interface DownloadGate {
|
|
14
|
+
readonly allowed: boolean;
|
|
15
|
+
setAllowed(allowed: boolean): void;
|
|
16
|
+
downloaded(): void;
|
|
17
|
+
hold: HoldFn;
|
|
18
|
+
abandon(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function createDownloadGate({ isCached, onHeld, onDropped }: DownloadGateOptions): DownloadGate;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AbortedError, raceAbort } from '../helpers/race-abort.js';
|
|
2
|
+
function deferred() {
|
|
3
|
+
const next = Promise.withResolvers();
|
|
4
|
+
next.promise.catch(noop);
|
|
5
|
+
return next;
|
|
6
|
+
}
|
|
7
|
+
export function createDownloadGate({ isCached, onHeld, onDropped }) {
|
|
8
|
+
let allowed = true;
|
|
9
|
+
let woke = deferred();
|
|
10
|
+
let dropped = deferred();
|
|
11
|
+
const wake = () => {
|
|
12
|
+
const woken = woke;
|
|
13
|
+
woke = deferred();
|
|
14
|
+
woken.resolve();
|
|
15
|
+
};
|
|
16
|
+
const wait = async (held, signal, onWait) => {
|
|
17
|
+
const drop = dropped;
|
|
18
|
+
const cancelled = () => signal?.aborted === true || drop !== dropped;
|
|
19
|
+
for (;;) {
|
|
20
|
+
const change = Promise.race([raceAbort(woke.promise, signal), drop.promise]);
|
|
21
|
+
change.catch(noop);
|
|
22
|
+
const ready = allowed || (await isCached(held.model, held.addon).catch(() => false));
|
|
23
|
+
if (cancelled()) {
|
|
24
|
+
throw new AbortedError();
|
|
25
|
+
}
|
|
26
|
+
if (ready) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
onWait();
|
|
30
|
+
await change;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
get allowed() {
|
|
35
|
+
return allowed;
|
|
36
|
+
},
|
|
37
|
+
setAllowed(next) {
|
|
38
|
+
allowed = next;
|
|
39
|
+
if (allowed) {
|
|
40
|
+
wake();
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
downloaded: wake,
|
|
44
|
+
async hold(model, addon, signal) {
|
|
45
|
+
if (allowed)
|
|
46
|
+
return;
|
|
47
|
+
if (signal?.aborted)
|
|
48
|
+
throw new AbortedError();
|
|
49
|
+
const held = { model, addon };
|
|
50
|
+
let shown = false;
|
|
51
|
+
try {
|
|
52
|
+
await wait(held, signal, () => {
|
|
53
|
+
if (!shown) {
|
|
54
|
+
shown = true;
|
|
55
|
+
onHeld(held);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (shown) {
|
|
61
|
+
onDropped(held);
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
abandon() {
|
|
67
|
+
const abandoned = dropped;
|
|
68
|
+
dropped = deferred();
|
|
69
|
+
abandoned.reject(new AbortedError());
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function noop() { }
|