pi-voicekit 0.3.0 → 0.3.1

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.
@@ -1103,14 +1103,8 @@ async function transcribeInProcess(
1103
1103
  config: VoiceConfig,
1104
1104
  onSegment?: (text: string, index: number) => void
1105
1105
  ): Promise<string> {
1106
- const {
1107
- initSherpa,
1108
- isSherpaAvailable,
1109
- getSherpaError,
1110
- getOrCreateRecognizer,
1111
- transcribeBuffer,
1112
- transcribeBufferSegmented,
1113
- } = await import("./sherpa-engine");
1106
+ const { initSherpa, isSherpaAvailable, getSherpaError, getOrCreateRecognizer, transcribeBufferSegmented } =
1107
+ await import("./sherpa-engine");
1114
1108
  const { ensureModelDownloaded } = await import("./model-download");
1115
1109
 
1116
1110
  // Initialize sherpa if needed
@@ -1131,21 +1125,10 @@ async function transcribeInProcess(
1131
1125
 
1132
1126
  // Create/reuse recognizer and transcribe
1133
1127
  const recognizer = getOrCreateRecognizer(model, modelDir, config.language || "en");
1134
- // Qwen3-ASR caps context at 512 tokens (~18s); segment long audio via VAD before decode.
1135
- if (model.sherpaModel.type === "qwen3_asr") {
1136
- return transcribeBufferSegmented(pcmData, recognizer, undefined, onSegment);
1137
- }
1138
- const text = await transcribeBuffer(pcmData, recognizer);
1139
- // Every other in-process model decodes the whole buffer as a single segment. Report it so
1140
- // a pipelining caller sees one segment for any in-process dictation (a short qwen3
1141
- // dictation takes the same single-segment fast path). Observational, like the recogniser's
1142
- // callback: a throwing observer must not cost the transcript.
1143
- if (onSegment) {
1144
- try {
1145
- onSegment(text, 0);
1146
- } catch {}
1147
- }
1148
- return text;
1128
+ // Every in-process model decodes long audio in VAD-sized pieces, so a pipelining caller can
1129
+ // polish one piece while the rest still decodes; audio at or below the segment threshold takes
1130
+ // the same single-decode fast path as before and the concatenation is unchanged.
1131
+ return transcribeBufferSegmented(pcmData, recognizer, undefined, onSegment);
1149
1132
  }
1150
1133
 
1151
1134
  /** Check if a local transcription server is reachable. */
@@ -393,7 +393,7 @@ export function createPolishQueue(options: PolishQueueOptions): PolishQueue {
393
393
  const call: LiveCall = { settled: false, orphaned: false };
394
394
  let issued = false;
395
395
  try {
396
- const sampling = polishSamplingOptions(options.model, job.raw.length, forceOff);
396
+ const sampling = polishSamplingOptions(options.model);
397
397
  const result = await polishTranscript({
398
398
  raw: job.raw,
399
399
  entries: entriesForSegment(job.index),
@@ -331,36 +331,23 @@ export function buildPolishAudit(input: {
331
331
  }
332
332
 
333
333
  /**
334
- * Extra request fields for the polish call, or nothing when the model has no thinking to turn
335
- * off.
334
+ * Extra request fields for the polish call, or nothing when the model has no thinking to turn off.
336
335
  *
337
- * Short transcripts keep thinking on: it is cheap there and the wording comes out better.
338
- * Measured 2026-09-26 on the acceptance corpus, thinking on won exactly the samples this pass
339
- * exists for — a self-correction merged for +1.71 CER with it on against 0 with it off, and two
340
- * zh-en term samples +0.08/+0.10 against 0 — while a 161-character transcript spent only 66
341
- * reasoning tokens in 0.47 s.
342
- *
343
- * Long transcripts turn it off: there thinking grows far past the token budget (309 characters
344
- * needed ~1700 reasoning tokens, 471 characters ~2800-4400, against a budget of 1130-1454), so the
345
- * answer was truncated and the pass fell back to the raw transcript — intermittently, which is
346
- * what made a long dictation look unpolished. With thinking off the same input finished in ~1.2 s
347
- * and spent no reasoning tokens at all.
336
+ * Thinking is off for every polish call on a reasoning model. It buys quality on short samples
337
+ * (measured 2026-09-26: a self-correction merged for +1.71 CER with thinking on against 0 with it
338
+ * off, two zh-en term samples +0.08/+0.10) but it spends a budget nobody can predict: the same
339
+ * spend that truncated long transcripts also hit short ones - a 76-character dictation burned
340
+ * ~1,200 reasoning tokens against a 664-1,024 budget and came back truncated after 7.6 s, and
341
+ * every real-corpus failure recorded on 2026-09-26 was `stop-reason:length` on a transcript of
342
+ * under 200 characters. On the real corpus the measured correction gain was ~0 either way, so
343
+ * the pass keeps the wording it can improve and gives up the truncation class entirely.
348
344
  *
349
345
  * `samplingParams` is applied by OpenAI-compatible adapters only, and the `reasoning` gate keeps
350
346
  * the field away from models with no thinking at all.
351
- *
352
- * `forceOff` is the segmented queue's retry: the attempt already burned its deadline, so it runs
353
- * small and cheap regardless of length. It overrides the length gate only - a model with no
354
- * thinking still gets no sampling fields at all.
355
347
  */
356
- export const THINKING_MAX_CHARS = 200;
357
-
358
- export function polishSamplingOptions(
359
- model: { reasoning?: boolean } | undefined | null,
360
- rawLength: number,
361
- forceOff = false
362
- ): { samplingParams?: { reasoning_effort: string } } {
348
+ export function polishSamplingOptions(model: { reasoning?: boolean } | undefined | null): {
349
+ samplingParams?: { reasoning_effort: string };
350
+ } {
363
351
  if (!model || model.reasoning !== true) return {};
364
- if (!forceOff && Number.isFinite(rawLength) && rawLength <= THINKING_MAX_CHARS) return {};
365
352
  return { samplingParams: { reasoning_effort: "none" } };
366
353
  }
@@ -1029,10 +1029,9 @@ export default function (pi: ExtensionAPI) {
1029
1029
  {
1030
1030
  signal,
1031
1031
  maxTokens: request.maxTokens,
1032
- // Measured 2026-09-26: without this a reasoning model spends the whole budget thinking
1033
- // about a long dictation and the pass falls back to the raw text — see
1034
- // polishSamplingOptions.
1035
- ...polishSamplingOptions(model as { reasoning?: boolean }, raw.length),
1032
+ // Measured 2026-09-26: without this a reasoning model can spend the whole budget thinking,
1033
+ // truncating the answer on any dictation — see polishSamplingOptions.
1034
+ ...polishSamplingOptions(model as { reasoning?: boolean }),
1036
1035
  }
1037
1036
  ),
1038
1037
  debug: (reason, data) => voiceDebug(`polish ${reason}`, data),
@@ -1042,7 +1041,7 @@ export default function (pi: ExtensionAPI) {
1042
1041
  configured: choice.ref,
1043
1042
  // Pure and cheap, so computing it twice (here and in the call options) is fine, and it
1044
1043
  // keeps the audit entry honest about what the pass decided.
1045
- thinkingOff: Boolean(polishSamplingOptions(model as { reasoning?: boolean }, raw.length).samplingParams),
1044
+ thinkingOff: Boolean(polishSamplingOptions(model as { reasoning?: boolean }).samplingParams),
1046
1045
  maxTokens: polishMaxTokens(raw.length),
1047
1046
  status: result.status,
1048
1047
  ms: Date.now() - started,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-voicekit",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Voice in + voice out for Pi CLI — hold-to-talk STT (Deepgram streaming or 21 offline models) plus TTS (Kitten Nano, Piper, Kokoro, or Deepgram Aura)",
5
5
  "type": "module",
6
6
  "keywords": [