@maheidem/pi-audio-transcribe 0.2.0 → 0.2.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.
package/index.ts CHANGED
@@ -29,6 +29,7 @@ import { extractAudioPaths, expandPath, tokenizeArgs } from "./paths.ts";
29
29
  import { expandInputs, formatSummary, resolveSttModel, transcribeOne, type FileResult, type PipelineCtx, type Setup } from "./pipeline.ts";
30
30
  import { AudioSettingsController, type AudioSettingKey } from "./settings.ts";
31
31
  import { buildAudioPanelSnapshot } from "./ui/audio-panel.ts";
32
+ import { audioTranscribeVersion } from "./version.ts";
32
33
  import { SettingsPanel, type PanelResult } from "./ui/settings-panel.ts";
33
34
 
34
35
  const TAG = "[audio-transcribe]";
@@ -83,7 +84,7 @@ export default function (pi: ExtensionAPI) {
83
84
  async function reportStatus(ctx: ExtensionContext): Promise<void> {
84
85
  const cfg = resolveConfig();
85
86
  const health = await serverHealth(cfg.baseUrl, cfg.apiKey, 8_000, ctx.signal);
86
- const setup = await getSetup(true);
87
+ const setup = await getSetup(true).catch(() => null); // an unreachable server must never break --status
87
88
  const models = health.reachable
88
89
  ? await listSttModels(cfg.baseUrl, cfg.apiKey, 8_000, ctx.signal).catch(() => [] as SttModel[])
89
90
  : [];
@@ -91,10 +92,11 @@ export default function (pi: ExtensionAPI) {
91
92
  ctx,
92
93
  [
93
94
  `${TAG} status`,
95
+ ` version: v${audioTranscribeVersion()} (loaded at session start; /reload picks up newer installs)`,
94
96
  ` server: ${cfg.baseUrl}`,
95
97
  ` status: ${health.reachable ? `ok (oMLX ${health.version})` : `UNREACHABLE — ${health.error}`}`,
96
- ` model: ${setup.model ?? "none discovered"}`,
97
- ` resident: ${setup.modelInfo?.loaded ? "yes" : "no (cold load on first use)"}`,
98
+ ` model: ${setup?.model ?? "none discovered"}`,
99
+ ` resident: ${setup?.modelInfo?.loaded ? "yes" : "no (cold load on first use)"}`,
98
100
  ` language: ${cfg.language ?? "auto-detect (default)"}`,
99
101
  ` cross-check: ${cfg.crossCheck ? "on" : "off"}`,
100
102
  ` input hook: ${cfg.autoDetect ? "on" : "off"}`,
@@ -298,25 +300,35 @@ export default function (pi: ExtensionAPI) {
298
300
  const found = extractAudioPaths(event.text ?? "");
299
301
  if (!found.length) return { action: "continue" as const };
300
302
 
301
- const setup = await getSetup();
302
- if (!setup.model) return { action: "continue" as const };
303
+ // Session safety: this hook must NEVER break a prompt. If the oMLX
304
+ // server is unreachable (getSetup -> listSttModels throws) or anything
305
+ // else in the setup/transcribe path fails, log one tagged line and
306
+ // pass the text through untranscribed.
307
+ try {
308
+ const setup = await getSetup();
309
+ if (!setup.model) return { action: "continue" as const };
303
310
 
304
- const blocks: string[] = [];
305
- for (const p of found) {
306
- const abs = expandPath(p, ctx.cwd);
307
- const st = await (await import("node:fs/promises")).stat(abs).catch(() => null);
308
- if (!st || st.isDirectory() || !isAudioPath(abs)) continue; // ignore passing mentions of absent files
309
- say(ctx, `${TAG} auto-transcribing ${abs.split("/").pop()}`, "info");
310
- const r = await transcribeOne(abs, setup, hostCtx(ctx, (c, a, o) => pi.exec(c, a, o)));
311
- if (r.error) blocks.push(`[audio-transcribe] ${abs}: FAILED — ${r.error}`);
312
- else if (r.skipped) blocks.push(`[audio-transcribe] ${abs}: skipped — ${r.skipReason}`);
313
- else {
314
- const head = `[audio-transcribe] ${abs}\nverdict=${r.verdict} confidence=${((r.confidence ?? 0) * 100).toFixed(0)}% language=${r.language ?? "?"} duration=${r.durationSeconds?.toFixed(1) ?? "?"}s sidecar=${r.sidecarPath}`;
315
- blocks.push(`${head}\ntranscript:\n${r.text}`);
316
- if (r.recommendations?.length) blocks.push(`advice: ${r.recommendations.join(" | ")}`);
311
+ const blocks: string[] = [];
312
+ for (const p of found) {
313
+ const abs = expandPath(p, ctx.cwd);
314
+ const st = await (await import("node:fs/promises")).stat(abs).catch(() => null);
315
+ if (!st || st.isDirectory() || !isAudioPath(abs)) continue; // ignore passing mentions of absent files
316
+ say(ctx, `${TAG} auto-transcribing ${abs.split("/").pop()}`, "info");
317
+ const r = await transcribeOne(abs, setup, hostCtx(ctx, (c, a, o) => pi.exec(c, a, o)));
318
+ if (r.error) blocks.push(`[audio-transcribe] ${abs}: FAILED — ${r.error}`);
319
+ else if (r.skipped) blocks.push(`[audio-transcribe] ${abs}: skipped — ${r.skipReason}`);
320
+ else {
321
+ const head = `[audio-transcribe] ${abs}\nverdict=${r.verdict} confidence=${((r.confidence ?? 0) * 100).toFixed(0)}% language=${r.language ?? "?"} duration=${r.durationSeconds?.toFixed(1) ?? "?"}s sidecar=${r.sidecarPath}`;
322
+ blocks.push(`${head}\ntranscript:\n${r.text}`);
323
+ if (r.recommendations?.length) blocks.push(`advice: ${r.recommendations.join(" | ")}`);
324
+ }
317
325
  }
326
+ if (!blocks.length) return { action: "continue" as const };
327
+ return { action: "transform" as const, text: `${event.text}\n\n${blocks.join("\n\n")}` };
328
+ } catch (err) {
329
+ const msg = err instanceof Error ? err.message : String(err);
330
+ console.log(`${TAG} input hook failed (${msg}); continuing without transcription`);
331
+ return { action: "continue" as const };
318
332
  }
319
- if (!blocks.length) return { action: "continue" as const };
320
- return { action: "transform" as const, text: `${event.text}\n\n${blocks.join("\n\n")}` };
321
333
  });
322
334
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maheidem/pi-audio-transcribe",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Pi extension: transcribe audio with oMLX STT (Qwen3-ASR), validate the speech, and persist a JSON transcript sidecar beside each audio file.",
6
6
  "keywords": [
@@ -23,8 +23,10 @@
23
23
  "settings.ts",
24
24
  "sidecar.ts",
25
25
  "validate.ts",
26
+ "version.ts",
26
27
  "lib/json-store.ts",
27
28
  "ui/audio-panel.ts",
29
+ "ui/version.ts",
28
30
  "ui/settings-panel.ts",
29
31
  "README.md",
30
32
  "LICENSE"
@@ -66,4 +68,4 @@
66
68
  "./index.ts"
67
69
  ]
68
70
  }
69
- }
71
+ }
package/ui/version.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * ui/version.ts — canonical loaded-version provenance (S2, pi-panel-kit
3
+ * stage 1).
4
+ *
5
+ * WHY THIS EXISTS
6
+ * A running Pi session keeps whatever extension code it loaded at startup;
7
+ * newer installs (npm store or by-path) only apply after `/reload`. A stale
8
+ * in-process copy is indistinguishable from a fresh one unless every status
9
+ * line, panel summary, and tool card carries the version actually EXECUTING.
10
+ * This helper reads that version from the extension's own `package.json` —
11
+ * never hard-coded, never baked at build time.
12
+ *
13
+ * Vendored byte-identically into consumers (guard: `node
14
+ * skills/pi-extension-builder/scripts/check-vendored.mjs`); each extension
15
+ * keeps a thin wrapper (`version.ts` at its package root) that exports its
16
+ * original function name and passes the `package.json` path resolved from
17
+ * THE WRAPPER'S module URL — a vendored copy sits in `ui/` and cannot find
18
+ * the package root by itself, which is exactly why the path is a parameter.
19
+ *
20
+ * Pi-free by design: node built-ins only, no imports from sibling modules.
21
+ */
22
+
23
+ import * as fs from "node:fs";
24
+
25
+ export interface ExtensionVersionOptions {
26
+ /**
27
+ * Absolute path to the extension's `package.json`. Resolve it from the
28
+ * wrapper's own module URL (`path.join(dirname(fileURLToPath(import.meta.url)), "package.json")`)
29
+ * so the string is anchored to the code actually executing, not the cwd.
30
+ */
31
+ packageJsonPath: string;
32
+ /**
33
+ * Short extension name for composed headers. When set, the result is
34
+ * `label vX.Y.Z` (the `[delegate v0.3.2 …]` pattern used in result
35
+ * headers). Default: no label, bare version (status lines and panel
36
+ * summaries render their own prefixes).
37
+ */
38
+ label?: string;
39
+ /**
40
+ * Verbatim suffix appended after the version, e.g.
41
+ * `" (loaded at session start; /reload picks up newer installs)"`.
42
+ * Default: none. Call sites that interpolate the suffix themselves keep
43
+ * passing nothing — identical output either way.
44
+ */
45
+ suffix?: string;
46
+ /** Returned when `package.json` is unreadable/unparsable or lacks `version`. Default `"unknown"`. */
47
+ fallback?: string;
48
+ }
49
+
50
+ /** Raw versions keyed by resolved package.json path; read once per process. */
51
+ const cache = new Map<string, string>();
52
+
53
+ /** The `version` field of the extension's package.json (never a hardcoded string). */
54
+ export function extensionVersion(options: ExtensionVersionOptions): string {
55
+ const fallback = options.fallback ?? "unknown";
56
+ let version = cache.get(options.packageJsonPath);
57
+ if (version === undefined) {
58
+ try {
59
+ const pkg = JSON.parse(fs.readFileSync(options.packageJsonPath, "utf8")) as {
60
+ version?: string;
61
+ };
62
+ version = pkg.version ?? fallback;
63
+ } catch {
64
+ version = fallback;
65
+ }
66
+ cache.set(options.packageJsonPath, version);
67
+ }
68
+ return `${options.label ? `${options.label} v` : ""}${version}${options.suffix ?? ""}`;
69
+ }
package/version.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * audio-transcribe — loaded-version provenance.
3
+ *
4
+ * A running Pi session keeps whatever extension code it loaded at startup;
5
+ * newer npm-store installs only apply after /reload. The /transcribe
6
+ * --status output therefore carries the version actually executing, so a
7
+ * stale copy in this process is self-evident instead of mysterious.
8
+ *
9
+ * Thin wrapper over the canonical helper vendored at `ui/version.ts`
10
+ * (kit source: `skills/pi-extension-builder/assets/control-panel/…`); the
11
+ * package.json path is resolved from THIS module's URL.
12
+ */
13
+
14
+ import * as path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ import { extensionVersion } from "./ui/version.ts";
18
+
19
+ export function audioTranscribeVersion(): string {
20
+ return extensionVersion({
21
+ packageJsonPath: path.join(path.dirname(fileURLToPath(import.meta.url)), "package.json"),
22
+ });
23
+ }