poddrill 0.0.0-stage → 0.1.0
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/.agents/skills/poddrill/SKILL.md +96 -0
- package/.agents/skills/poddrill/references/assign-speakers.md +22 -0
- package/.agents/skills/poddrill/references/create-summary.md +140 -0
- package/.agents/skills/poddrill/references/fetch-audio.md +40 -0
- package/.agents/skills/poddrill/references/orwell.md +71 -0
- package/.agents/skills/poddrill/references/select-models.md +58 -0
- package/.agents/skills/poddrill/references/track-sources.md +59 -0
- package/.agents/skills/poddrill/scripts/fetch-audio.mjs +154 -0
- package/LICENSE +21 -0
- package/README.md +28 -2
- package/dist/audio.js +262 -0
- package/dist/audit.js +834 -0
- package/dist/cli.js +455 -0
- package/dist/client.js +1450 -0
- package/dist/disk.js +47 -0
- package/dist/embed.js +127 -0
- package/dist/episode.js +259 -0
- package/dist/fleet.js +176 -0
- package/dist/groups.js +512 -0
- package/dist/idle-fleet.js +71 -0
- package/dist/jobs.js +1053 -0
- package/dist/models.js +45 -0
- package/dist/moonshine.js +185 -0
- package/dist/needle.js +42 -0
- package/dist/owner.js +770 -0
- package/dist/package-identity.js +73 -0
- package/dist/provider.js +123 -0
- package/dist/stretch.js +46 -0
- package/dist/summary.js +258 -0
- package/dist/transcribe.js +53 -0
- package/dist/worker.js +215 -0
- package/dist/workspace.js +306 -0
- package/package.json +31 -4
package/dist/models.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One row per language. `fleetLimit` is the size for every model, and `modelFleet` overrides one id.
|
|
3
|
+
*/
|
|
4
|
+
export const modelIds = [
|
|
5
|
+
"whistle",
|
|
6
|
+
"moonshine-tiny-ja",
|
|
7
|
+
"moonshine-tiny-ko",
|
|
8
|
+
"moonshine-base-zh",
|
|
9
|
+
];
|
|
10
|
+
const languageModels = [
|
|
11
|
+
{ id: "whistle", engine: "whistle", language: "en" },
|
|
12
|
+
{ id: "whistle", engine: "whistle", language: "de" },
|
|
13
|
+
{ id: "whistle", engine: "whistle", language: "fr" },
|
|
14
|
+
{ id: "whistle", engine: "whistle", language: "es" },
|
|
15
|
+
{ id: "whistle", engine: "whistle", language: "it" },
|
|
16
|
+
{ id: "whistle", engine: "whistle", language: "nl" },
|
|
17
|
+
{ id: "whistle", engine: "whistle", language: "pl" },
|
|
18
|
+
{ id: "moonshine-tiny-ja", engine: "moonshine", language: "ja", arch: "tiny" },
|
|
19
|
+
{ id: "moonshine-tiny-ko", engine: "moonshine", language: "ko", arch: "tiny" },
|
|
20
|
+
{ id: "moonshine-base-zh", engine: "moonshine", language: "zh", arch: "base" },
|
|
21
|
+
];
|
|
22
|
+
export function isModelId(value) {
|
|
23
|
+
for (const id of modelIds) {
|
|
24
|
+
if (id === value) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
/** `undefined` for a language that has no row. */
|
|
31
|
+
export function modelForLanguage(language) {
|
|
32
|
+
for (const row of languageModels) {
|
|
33
|
+
if (row.language === language) {
|
|
34
|
+
return row;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export function fleetSize(modelId, config) {
|
|
40
|
+
const sized = config.modelFleet[modelId];
|
|
41
|
+
if (sized === undefined) {
|
|
42
|
+
return config.fleetLimit;
|
|
43
|
+
}
|
|
44
|
+
return sized;
|
|
45
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local Moonshine worker. One python3 process constructs one `Transcriber`.
|
|
3
|
+
* Ready frame: { "kind": "ready" }. Result: lines mapped onto `WhistleResult`.
|
|
4
|
+
*/
|
|
5
|
+
import { asRecord } from "./disk.js";
|
|
6
|
+
import { PYTHON_FRAMES, spawnFramedWorker } from "./worker.js";
|
|
7
|
+
const INSTALL_MESSAGE = "pip install moonshine-voice";
|
|
8
|
+
/** A missing `moonshine_voice` module becomes the install line. Any other text stays. */
|
|
9
|
+
export function moonshineFailureMessage(text) {
|
|
10
|
+
if (text.includes("ModuleNotFoundError") && text.includes("moonshine_voice")) {
|
|
11
|
+
return INSTALL_MESSAGE;
|
|
12
|
+
}
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Map one Moonshine transcript onto the Whistle result cue building already reads.
|
|
17
|
+
* A line with text and no words becomes one word from `start_time` through `duration`.
|
|
18
|
+
* No words at all is silence.
|
|
19
|
+
*/
|
|
20
|
+
export function moonshineToWhistle(value, language) {
|
|
21
|
+
const record = asRecord(value);
|
|
22
|
+
if (record === undefined) {
|
|
23
|
+
throw new Error("Moonshine result must be an object");
|
|
24
|
+
}
|
|
25
|
+
const lines = record["lines"];
|
|
26
|
+
if (!Array.isArray(lines)) {
|
|
27
|
+
throw new Error("Moonshine result must include lines");
|
|
28
|
+
}
|
|
29
|
+
const words = [];
|
|
30
|
+
const texts = [];
|
|
31
|
+
for (const item of lines) {
|
|
32
|
+
const line = readLine(item);
|
|
33
|
+
if (line === undefined) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
texts.push(line.text);
|
|
37
|
+
words.push(...line.words);
|
|
38
|
+
}
|
|
39
|
+
if (words.length === 0) {
|
|
40
|
+
return { kind: "silence", text: "", language: "", ttftMs: 0, decodeTps: 0 };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
kind: "speech",
|
|
44
|
+
text: texts.join(" "),
|
|
45
|
+
language,
|
|
46
|
+
ttftMs: 0,
|
|
47
|
+
decodeTps: 0,
|
|
48
|
+
words,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function spawnMoonshineWorker(model, weightsDir) {
|
|
52
|
+
const arch = archInt(model.arch);
|
|
53
|
+
return (events) => {
|
|
54
|
+
const link = spawnFramedWorker({
|
|
55
|
+
label: "moonshine",
|
|
56
|
+
args: ["-c", MOONSHINE_SCRIPT, weightsDir, String(arch)],
|
|
57
|
+
checkReady: async () => undefined,
|
|
58
|
+
parseResult: (value) => moonshineToWhistle(value, model.language),
|
|
59
|
+
}, {
|
|
60
|
+
ready: () => {
|
|
61
|
+
events.ready();
|
|
62
|
+
},
|
|
63
|
+
exit: (reason) => {
|
|
64
|
+
events.exit(moonshineFailureMessage(reason));
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
request: async (frame) => {
|
|
69
|
+
try {
|
|
70
|
+
return await link.request(frame);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
throw new Error(moonshineFailureMessage(message));
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
close: () => {
|
|
78
|
+
link.close();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function archInt(arch) {
|
|
84
|
+
switch (arch) {
|
|
85
|
+
case "tiny":
|
|
86
|
+
return 0;
|
|
87
|
+
case "base":
|
|
88
|
+
return 1;
|
|
89
|
+
default: {
|
|
90
|
+
const leftover = arch;
|
|
91
|
+
throw new Error(`unexpected moonshine architecture ${String(leftover)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function readLine(value) {
|
|
96
|
+
const record = asRecord(value);
|
|
97
|
+
if (record === undefined) {
|
|
98
|
+
throw new Error("Moonshine line must be an object");
|
|
99
|
+
}
|
|
100
|
+
const text = readString(record, "text");
|
|
101
|
+
const timed = readWords(record["words"]);
|
|
102
|
+
if (timed.length > 0) {
|
|
103
|
+
return { text: text === "" ? timed.map((word) => word.word).join(" ") : text, words: timed };
|
|
104
|
+
}
|
|
105
|
+
if (text === "") {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const start = readSeconds(record, "start_time");
|
|
109
|
+
const duration = readSeconds(record, "duration");
|
|
110
|
+
return {
|
|
111
|
+
text,
|
|
112
|
+
words: [{ word: text, start, end: start + duration, probability: 1 }],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function readWords(value) {
|
|
116
|
+
if (value === undefined) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
if (!Array.isArray(value)) {
|
|
120
|
+
throw new Error("Moonshine words must be an array");
|
|
121
|
+
}
|
|
122
|
+
return value.map((item) => {
|
|
123
|
+
const word = asRecord(item);
|
|
124
|
+
if (word === undefined) {
|
|
125
|
+
throw new Error("Moonshine word must be an object");
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
word: readString(word, "word"),
|
|
129
|
+
start: readSeconds(word, "start"),
|
|
130
|
+
end: readSeconds(word, "end"),
|
|
131
|
+
probability: readFinite(word, "confidence"),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function readString(record, key) {
|
|
136
|
+
const value = record[key];
|
|
137
|
+
if (typeof value !== "string") {
|
|
138
|
+
throw new Error(`Moonshine field ${key} must be a string`);
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
function readFinite(record, key) {
|
|
143
|
+
const value = record[key];
|
|
144
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
145
|
+
throw new Error(`Moonshine field ${key} must be a finite number`);
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
function readSeconds(record, key) {
|
|
150
|
+
const value = readFinite(record, key);
|
|
151
|
+
if (value < 0) {
|
|
152
|
+
throw new Error(`Moonshine field ${key} must be non-negative`);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
const MOONSHINE_SCRIPT = [
|
|
157
|
+
PYTHON_FRAMES,
|
|
158
|
+
"import wave",
|
|
159
|
+
"from moonshine_voice import Transcriber, ModelArch",
|
|
160
|
+
"transcriber = Transcriber(",
|
|
161
|
+
" model_path=sys.argv[1],",
|
|
162
|
+
" model_arch=ModelArch(int(sys.argv[2])),",
|
|
163
|
+
" options={\"word_timestamps\": \"true\", \"max_tokens_per_second\": \"13.0\"},",
|
|
164
|
+
")",
|
|
165
|
+
"def read_pcm(path):",
|
|
166
|
+
" with wave.open(path, \"rb\") as reader:",
|
|
167
|
+
" if reader.getnchannels() != 1 or reader.getsampwidth() != 2:",
|
|
168
|
+
" raise ValueError(f\"{path} is not 16-bit mono wav\")",
|
|
169
|
+
" pcm = reader.readframes(reader.getnframes())",
|
|
170
|
+
" count = len(pcm) // 2",
|
|
171
|
+
" return [int.from_bytes(pcm[index * 2:index * 2 + 2], \"little\", signed=True) / 32768.0 for index in range(count)]",
|
|
172
|
+
"def dump_word(word):",
|
|
173
|
+
" return {\"word\": word.word, \"start\": float(word.start), \"end\": float(word.end), \"confidence\": float(word.confidence)}",
|
|
174
|
+
"def dump_line(line):",
|
|
175
|
+
" words = []",
|
|
176
|
+
" if line.words:",
|
|
177
|
+
" for word in line.words:",
|
|
178
|
+
" words.append(dump_word(word))",
|
|
179
|
+
" return {\"text\": line.text, \"start_time\": float(line.start_time), \"duration\": float(line.duration), \"words\": words}",
|
|
180
|
+
"def transcribe(message):",
|
|
181
|
+
" transcript = transcriber.transcribe_without_streaming(read_pcm(message[\"wavPath\"]))",
|
|
182
|
+
" return {\"lines\": [dump_line(line) for line in transcript.lines]}",
|
|
183
|
+
"send({\"kind\": \"ready\"})",
|
|
184
|
+
"serve(transcribe)",
|
|
185
|
+
].join("\n");
|
package/dist/needle.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local Mac Whistle worker: one python3 process that imports cactus-needle once.
|
|
3
|
+
* Its ready frame is { "kind": "ready", "version": string }, checked against package.json.
|
|
4
|
+
* Result: <needle.transcribe output>, parsed as `WhistleResult`.
|
|
5
|
+
*/
|
|
6
|
+
import { parseWhistle } from "./episode.js";
|
|
7
|
+
import { readPackageIdentity } from "./package-identity.js";
|
|
8
|
+
import { PYTHON_FRAMES, spawnFramedWorker } from "./worker.js";
|
|
9
|
+
/** Start one warm cactus-needle process. */
|
|
10
|
+
export const spawnNeedleWorker = (events) => {
|
|
11
|
+
const pin = pinnedNeedleVersion();
|
|
12
|
+
// A rejection is read by `checkReady`. Until then it must not count as unhandled.
|
|
13
|
+
pin.catch(() => { });
|
|
14
|
+
return spawnFramedWorker({
|
|
15
|
+
label: "cactus-needle",
|
|
16
|
+
args: ["-c", WORKER_SCRIPT],
|
|
17
|
+
async checkReady(ready) {
|
|
18
|
+
const version = ready["version"];
|
|
19
|
+
if (typeof version !== "string") {
|
|
20
|
+
return "cactus-needle worker sent a malformed reply";
|
|
21
|
+
}
|
|
22
|
+
const expected = await pin;
|
|
23
|
+
return version === expected ? undefined : `cactus-needle ${version} is not ${expected}`;
|
|
24
|
+
},
|
|
25
|
+
parseResult: parseWhistle,
|
|
26
|
+
}, events);
|
|
27
|
+
};
|
|
28
|
+
export async function pinnedNeedleVersion() {
|
|
29
|
+
const identity = await readPackageIdentity(import.meta.url);
|
|
30
|
+
return identity.cactusNeedle;
|
|
31
|
+
}
|
|
32
|
+
const WORKER_SCRIPT = [
|
|
33
|
+
PYTHON_FRAMES,
|
|
34
|
+
"import needle",
|
|
35
|
+
"send({\"kind\": \"ready\", \"version\": needle.__version__})",
|
|
36
|
+
"def transcribe(message):",
|
|
37
|
+
" language = message.get(\"language\", \"en\")",
|
|
38
|
+
" if not isinstance(language, str) or language == \"\":",
|
|
39
|
+
" language = \"en\"",
|
|
40
|
+
" return needle.transcribe(message[\"wavPath\"], language=language, word_timestamps=True)",
|
|
41
|
+
"serve(transcribe)",
|
|
42
|
+
].join("\n");
|