@getfamiliar/plugin-transcribe-whisper 0.1.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.
@@ -0,0 +1,9 @@
1
+ import { type CommandDef } from "citty";
2
+ import type { HostContext } from "@getfamiliar/shared";
3
+ /**
4
+ * Build the citty subcommands exposed under
5
+ * `./cli.sh transcribe-whisper`. Currently a single smoke-test
6
+ * command; future operational commands (e.g. `cost`, `models`) land
7
+ * here.
8
+ */
9
+ export declare function buildCommands(_ctx: HostContext): readonly CommandDef<any>[];
@@ -0,0 +1,61 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { defineCommand } from "citty";
4
+ import { transcribeAudio } from "./Whisper.js";
5
+ /**
6
+ * Build the citty subcommands exposed under
7
+ * `./cli.sh transcribe-whisper`. Currently a single smoke-test
8
+ * command; future operational commands (e.g. `cost`, `models`) land
9
+ * here.
10
+ */
11
+ // biome-ignore lint/suspicious/noExplicitAny: matches citty's SubCommandsDef pattern.
12
+ export function buildCommands(_ctx) {
13
+ return [testCommand()];
14
+ }
15
+ /**
16
+ * `./cli.sh transcribe-whisper test <path>` — read a local audio
17
+ * file, send it through {@link transcribeAudio}, and print the
18
+ * transcript. The operator's smoke test for
19
+ * "is `inference.apiKeys.openai` set up correctly?" without needing
20
+ * a live channel plugin.
21
+ *
22
+ * No explicit init call here — the host wraps every plugin command's
23
+ * `run` so `prepare(ctx)` has already fired by the time we land in
24
+ * the body, populating the module-level API key.
25
+ */
26
+ function testCommand() {
27
+ return defineCommand({
28
+ meta: {
29
+ name: "test",
30
+ description: "Transcribe a local audio file with Whisper and print the result. Useful for verifying inference.apiKeys.openai.",
31
+ },
32
+ args: {
33
+ path: {
34
+ type: "positional",
35
+ required: true,
36
+ description: "Path to a local audio file (.ogg, .m4a, .mp3, .wav, .webm, .flac).",
37
+ },
38
+ },
39
+ async run({ args }) {
40
+ const absolute = path.resolve(args.path);
41
+ let audio;
42
+ try {
43
+ audio = readFileSync(absolute);
44
+ }
45
+ catch (err) {
46
+ const msg = err instanceof Error ? err.message : String(err);
47
+ process.stderr.write(`failed to read ${absolute}: ${msg}\n`);
48
+ process.exit(1);
49
+ }
50
+ try {
51
+ const transcript = await transcribeAudio(audio, path.basename(absolute));
52
+ process.stdout.write(`${transcript}\n`);
53
+ }
54
+ catch (err) {
55
+ const msg = err instanceof Error ? err.message : String(err);
56
+ process.stderr.write(`transcription failed: ${msg}\n`);
57
+ process.exit(1);
58
+ }
59
+ },
60
+ });
61
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Install the OpenAI API key. Called once by the plugin manifest's
3
+ * `start(ctx)` hook with `ctx.config.getString("inference.apiKeys.openai")`,
4
+ * and from the plugin's own CLI commands before they run (because
5
+ * one-shot CLI invocations don't go through `startDaemons`).
6
+ *
7
+ * Idempotent — safe to call multiple times. Empty strings are
8
+ * rejected so a misconfigured field surfaces here instead of as an
9
+ * opaque OpenAI auth failure on the next transcription.
10
+ */
11
+ export declare function setApiKey(key: string): void;
12
+ /**
13
+ * Transcribe a buffered audio file using OpenAI's Whisper API
14
+ * (model `whisper-1`).
15
+ *
16
+ * `filename` is used only so OpenAI can infer the audio format from
17
+ * the extension (e.g. `.ogg`, `.m4a`, `.mp3`, `.wav`, `.webm`,
18
+ * `.flac`); the `audio` Buffer holds the actual bytes. Whisper
19
+ * auto-detects the spoken language, so no language hint is needed.
20
+ *
21
+ * Errors propagate to the caller as plain `Error`s — the SDK throws
22
+ * `OpenAI.APIError` for HTTP failures, which carries enough context
23
+ * (`status`, `message`) for the caller to handle. Whisper's per-
24
+ * request limit is 25 MB; we don't pre-validate, so an oversize file
25
+ * surfaces as a clear API error.
26
+ *
27
+ * @param audio The raw audio bytes.
28
+ * @param filename The original filename (only the extension matters).
29
+ * @returns The transcribed text.
30
+ * @throws If the plugin has not been initialized via {@link setApiKey}
31
+ * or the API call fails.
32
+ */
33
+ export declare function transcribeAudio(audio: Buffer, filename: string): Promise<string>;
@@ -0,0 +1,61 @@
1
+ import OpenAI, { toFile } from "openai";
2
+ /**
3
+ * Module-private OpenAI key. Populated once when the plugin is
4
+ * initialized (see {@link setApiKey} and the `start(ctx)` hook in
5
+ * `index.ts`); read on every {@link transcribeAudio} call.
6
+ *
7
+ * Module-level rather than parameter-passed so callers in other
8
+ * plugins don't have to know about — or read — Whisper's
9
+ * authentication: `transcribeAudio(audio, name)` is the entire
10
+ * surface they see.
11
+ */
12
+ let apiKey;
13
+ /**
14
+ * Install the OpenAI API key. Called once by the plugin manifest's
15
+ * `start(ctx)` hook with `ctx.config.getString("inference.apiKeys.openai")`,
16
+ * and from the plugin's own CLI commands before they run (because
17
+ * one-shot CLI invocations don't go through `startDaemons`).
18
+ *
19
+ * Idempotent — safe to call multiple times. Empty strings are
20
+ * rejected so a misconfigured field surfaces here instead of as an
21
+ * opaque OpenAI auth failure on the next transcription.
22
+ */
23
+ export function setApiKey(key) {
24
+ if (!key) {
25
+ throw new Error("setApiKey requires a non-empty key.");
26
+ }
27
+ apiKey = key;
28
+ }
29
+ /**
30
+ * Transcribe a buffered audio file using OpenAI's Whisper API
31
+ * (model `whisper-1`).
32
+ *
33
+ * `filename` is used only so OpenAI can infer the audio format from
34
+ * the extension (e.g. `.ogg`, `.m4a`, `.mp3`, `.wav`, `.webm`,
35
+ * `.flac`); the `audio` Buffer holds the actual bytes. Whisper
36
+ * auto-detects the spoken language, so no language hint is needed.
37
+ *
38
+ * Errors propagate to the caller as plain `Error`s — the SDK throws
39
+ * `OpenAI.APIError` for HTTP failures, which carries enough context
40
+ * (`status`, `message`) for the caller to handle. Whisper's per-
41
+ * request limit is 25 MB; we don't pre-validate, so an oversize file
42
+ * surfaces as a clear API error.
43
+ *
44
+ * @param audio The raw audio bytes.
45
+ * @param filename The original filename (only the extension matters).
46
+ * @returns The transcribed text.
47
+ * @throws If the plugin has not been initialized via {@link setApiKey}
48
+ * or the API call fails.
49
+ */
50
+ export async function transcribeAudio(audio, filename) {
51
+ if (!apiKey) {
52
+ throw new Error("transcribe-whisper is not initialized: call setApiKey, or run inside the host daemon so the plugin's start(ctx) hook fires.");
53
+ }
54
+ const client = new OpenAI({ apiKey });
55
+ const file = await toFile(audio, filename);
56
+ const result = await client.audio.transcriptions.create({
57
+ file,
58
+ model: "whisper-1",
59
+ });
60
+ return result.text;
61
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Re-export the plugin's public library API. Sibling plugins
3
+ * consume it as `import { transcribeAudio } from "@getfamiliar/plugin-transcribe-whisper"`.
4
+ *
5
+ * Cross-plugin imports are wired the same way as the existing
6
+ * `@getfamiliar/shared` package: the consuming plugin adds
7
+ * `"@getfamiliar/plugin-transcribe-whisper": "*"` to its `dependencies`, and npm's
8
+ * workspace resolution does the rest.
9
+ *
10
+ * Note: `setApiKey` is intentionally **not** re-exported. Callers
11
+ * never see Whisper's auth — the plugin owns its own key, populated
12
+ * here in `start(ctx)`.
13
+ */
14
+ export { transcribeAudio } from "./Whisper.js";
15
+ /**
16
+ * `transcribe-whisper` plugin manifest.
17
+ *
18
+ * "Library plugin": no event subscription, no workspace template;
19
+ * its purpose is to expose {@link transcribeAudio} to other plugins.
20
+ * The CLI subcommand (`./cli.sh transcribe-whisper test <path>`) is
21
+ * a smoke test for the OpenAI API key.
22
+ *
23
+ * The `prepare(ctx)` hook reads the OpenAI key from the host config
24
+ * and installs it into the module-private state in `Whisper.ts`, so
25
+ * sibling plugins call `transcribeAudio(audio, name)` without
26
+ * thinking about authentication. `prepare` runs before any plugin's
27
+ * `start` and before any plugin one-shot CLI command, so the order
28
+ * in `Registry.ts` doesn't matter for siblings that depend on this
29
+ * plugin.
30
+ *
31
+ * A missing `inference.apiKeys.openai` is **not** fatal at boot —
32
+ * the host stays up and `transcribeAudio` throws a clear
33
+ * "not initialized" error at the first call site. This lets users
34
+ * run the daemon for text-only flows (e.g. telegram without voice)
35
+ * without configuring an OpenAI key.
36
+ */
37
+ declare const _default: {
38
+ id: string;
39
+ host: {
40
+ prepare: (ctx: import("@getfamiliar/shared").HostContext) => void;
41
+ commands: (ctx: import("@getfamiliar/shared").HostContext) => readonly import("citty").CommandDef<any>[];
42
+ };
43
+ };
44
+ export default _default;
package/build/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import { definePlugin } from "@getfamiliar/shared";
2
+ import { buildCommands } from "./Commands.js";
3
+ import { setApiKey } from "./Whisper.js";
4
+ /**
5
+ * Re-export the plugin's public library API. Sibling plugins
6
+ * consume it as `import { transcribeAudio } from "@getfamiliar/plugin-transcribe-whisper"`.
7
+ *
8
+ * Cross-plugin imports are wired the same way as the existing
9
+ * `@getfamiliar/shared` package: the consuming plugin adds
10
+ * `"@getfamiliar/plugin-transcribe-whisper": "*"` to its `dependencies`, and npm's
11
+ * workspace resolution does the rest.
12
+ *
13
+ * Note: `setApiKey` is intentionally **not** re-exported. Callers
14
+ * never see Whisper's auth — the plugin owns its own key, populated
15
+ * here in `start(ctx)`.
16
+ */
17
+ export { transcribeAudio } from "./Whisper.js";
18
+ /**
19
+ * `transcribe-whisper` plugin manifest.
20
+ *
21
+ * "Library plugin": no event subscription, no workspace template;
22
+ * its purpose is to expose {@link transcribeAudio} to other plugins.
23
+ * The CLI subcommand (`./cli.sh transcribe-whisper test <path>`) is
24
+ * a smoke test for the OpenAI API key.
25
+ *
26
+ * The `prepare(ctx)` hook reads the OpenAI key from the host config
27
+ * and installs it into the module-private state in `Whisper.ts`, so
28
+ * sibling plugins call `transcribeAudio(audio, name)` without
29
+ * thinking about authentication. `prepare` runs before any plugin's
30
+ * `start` and before any plugin one-shot CLI command, so the order
31
+ * in `Registry.ts` doesn't matter for siblings that depend on this
32
+ * plugin.
33
+ *
34
+ * A missing `inference.apiKeys.openai` is **not** fatal at boot —
35
+ * the host stays up and `transcribeAudio` throws a clear
36
+ * "not initialized" error at the first call site. This lets users
37
+ * run the daemon for text-only flows (e.g. telegram without voice)
38
+ * without configuring an OpenAI key.
39
+ */
40
+ export default definePlugin({
41
+ id: "transcribe-whisper",
42
+ host: {
43
+ prepare: (ctx) => {
44
+ const key = ctx.config.getString("inference.apiKeys.openai", null);
45
+ if (key !== null) {
46
+ setApiKey(key);
47
+ }
48
+ else {
49
+ ctx.logger.info("transcribe-whisper inactive: inference.apiKeys.openai not set in config/config.yml");
50
+ }
51
+ },
52
+ commands: (ctx) => buildCommands(ctx),
53
+ },
54
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@getfamiliar/plugin-transcribe-whisper",
3
+ "type": "module",
4
+ "version": "0.1.2",
5
+ "description": "Familiar plugin: transcribeAudio() backed by OpenAI's Whisper API.",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "main": "build/index.js",
10
+ "types": "build/index.d.ts",
11
+ "familiar": {
12
+ "bundled": true
13
+ },
14
+ "files": [
15
+ "build",
16
+ "workspace-template"
17
+ ],
18
+ "engines": {
19
+ "node": ">=24"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "format": "biome format --write --config-path ../.. src/",
24
+ "format:check": "biome format --config-path ../.. src/",
25
+ "lint": "biome lint --config-path ../.. src/",
26
+ "check": "biome check --config-path ../.. src/"
27
+ },
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "citty": "^0.1.6",
31
+ "@getfamiliar/shared": "0.1.2",
32
+ "openai": "^4.77.0"
33
+ },
34
+ "devDependencies": {
35
+ "@biomejs/biome": "^2.4.12",
36
+ "@types/node": "^25.6.0",
37
+ "typescript": "^6.0.3"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/getfamiliar/familiar.git",
42
+ "directory": "plugins/transcribe-whisper"
43
+ }
44
+ }