@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 +32 -20
- package/package.json +4 -2
- package/ui/version.ts +69 -0
- package/version.ts +23 -0
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
|
|
97
|
-
` resident: ${setup
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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.
|
|
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
|
+
}
|