@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.
- package/build/Commands.d.ts +9 -0
- package/build/Commands.js +61 -0
- package/build/Whisper.d.ts +33 -0
- package/build/Whisper.js +61 -0
- package/build/index.d.ts +44 -0
- package/build/index.js +54 -0
- package/package.json +44 -0
|
@@ -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>;
|
package/build/Whisper.js
ADDED
|
@@ -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
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -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
|
+
}
|