poddrill 0.1.1 → 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/.agents/skills/poddrill/SKILL.md +34 -38
- package/.agents/skills/poddrill/references/fetch-audio.md +4 -4
- package/.agents/skills/poddrill/references/select-models.md +6 -4
- package/README.md +19 -4
- package/dist/cli.js +163 -10
- package/dist/client.js +222 -36
- package/dist/embed.js +11 -6
- package/dist/fleet.js +1 -1
- package/dist/moonshine.js +3 -2
- package/dist/needle.js +8 -1
- package/dist/node-version.js +59 -0
- package/dist/owner.js +73 -60
- package/dist/provider.js +27 -23
- package/dist/python-env.js +222 -0
- package/dist/weights.js +88 -0
- package/dist/worker.js +6 -3
- package/dist/workspace.js +37 -5
- package/package.json +6 -4
- package/requirements.txt +3 -0
|
@@ -2,74 +2,68 @@
|
|
|
2
2
|
name: poddrill
|
|
3
3
|
description: >-
|
|
4
4
|
Transcribe a podcast episode into timed text, group the voices, and store
|
|
5
|
-
a summary whose paragraphs cite that text.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
episode audio, starting a transcript, labeling speakers, or writing a summary.
|
|
9
|
-
When a session has no PodDrill tools, install the requirements, register the
|
|
10
|
-
client, and set up the workspace before transcribing. Do not use it to change
|
|
11
|
-
the poddrill TypeScript.
|
|
5
|
+
a summary whose paragraphs cite that text. Use when the user says to set up
|
|
6
|
+
PodDrill, fetch episode audio, start a transcript, label speakers, or write
|
|
7
|
+
a summary. Do not use it to change the poddrill TypeScript.
|
|
12
8
|
---
|
|
13
9
|
|
|
14
10
|
# PodDrill
|
|
15
11
|
|
|
16
|
-
PodDrill
|
|
12
|
+
PodDrill maps an episode (the audio) to cues, clusters those cues into speaker groups, and stores summaries. You decide how to use the toolset to answer the user's request. You read the text, name the voices, and write the summary words. PodDrill stores the episode.
|
|
17
13
|
|
|
18
|
-
|
|
14
|
+
Install once, with the sequence below. After that, read the one guide for the part you are doing. Leave the other guides unread. If an episode already has the asset you need, read it.
|
|
19
15
|
|
|
20
|
-
##
|
|
16
|
+
## Install on this computer
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
Do this once on the computer that will transcribe. Each step is safe to repeat. `localhost`, `grok-bot`, and `cursor-cloud` all run the local Python host on this computer. The provider id names that computer.
|
|
23
19
|
|
|
24
|
-
Install Node.js
|
|
20
|
+
1. Install Node.js 20.19 or newer and `ffmpeg`. Use the Node `.tar.gz` archive from `https://nodejs.org/dist/v20.19.0/`, or run `npx -y node@20.19.0`. On macOS, `brew install ffmpeg`. On Linux, `sudo apt-get install -y ffmpeg`.
|
|
25
21
|
|
|
26
|
-
|
|
22
|
+
2. Install the npm package. Read the published `version` and run `npm install -g poddrill@<version>`. Do not use `@latest`. Cloning the git repository is not required. If you clone it, set `GIT_TERMINAL_PROMPT=0`.
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
3. Run `poddrill install-python`. This creates `~/.cache/poddrill/venv` and installs cactus-needle 3.2.0, numpy 2.5.4, and onnxruntime 1.31.0. It uses `uv` when `uv` is on PATH, and otherwise `python3 -m venv`. The npm package named cactus-needle is a different package.
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
4. Run `poddrill fetch-weights`. This downloads `voxceleb_ECAPA512.onnx` from the pinned WeSpeaker commit, checks the byte size and SHA-256, and writes `~/.cache/poddrill/ecapa/voxceleb_ECAPA512.onnx`. WeSpeaker ECAPA-TDNN512 pretrained on VoxCeleb. VoxCeleb is used under CC BY 4.0. A later run leaves a matching file in place.
|
|
31
27
|
|
|
32
|
-
|
|
28
|
+
5. Run `poddrill client doctor --client cursor --json`. Use `grok_build` or `grok_bot` in place of `cursor` when that is the client. For every check whose `state` is `bad`, run the `fix` command on that check, then run doctor again. `yt-dlp` is optional and does not fail the command. `workspace` stays `pending` until setup. Stop this loop when doctor exits 0, or when the only `bad` check is `link`.
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
6. Run `poddrill setup --provider localhost --queue-length 4`. Without `PODDRILL_WORKSPACE`, setup and every other command use `~/poddrill`. For `ja`, `ko`, or `zh`, read the weights guide and pass `--model-weights` on that same command. `en`, `de`, `fr`, `es`, `it`, `nl`, and `pl` do not need Moonshine weights.
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
`poddrill bootstrap --provider localhost --queue-length 4` runs steps 3, 4, and 6.
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
7. Register the client. Run `poddrill client plan --client cursor --json`. After that plan is approved, run `poddrill client apply --yes --client cursor`. Grok Bot prints a sentence with the absolute Node binary, `dist/cli.js`, and an env block (`PATH`, `PODDRILL_PYTHON`, `PODDRILL_WORKSPACE`, `PODDRILL_EMBED_WEIGHTS`). Add that custom server in the Grok Bot chat. It does not write a config file.
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
8. Run `poddrill client doctor --client cursor --json` again. Exit 0 means every required check is ok.
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
A later refresh of clients that already have an entry is `poddrill client plan --linked --json`, then `poddrill client apply --yes --linked` after approval.
|
|
43
39
|
|
|
44
|
-
|
|
40
|
+
## Python
|
|
45
41
|
|
|
46
|
-
|
|
42
|
+
`PODDRILL_PYTHON` selects the interpreter. Pass `--python` or set `PODDRILL_PYTHON` on setup to store an absolute path in `poddrill.json`. Otherwise PodDrill uses `~/.cache/poddrill/venv/bin/python` when that file exists, then `python3`. Install yt-dlp and moonshine-voice into that same interpreter. The current yt-dlp and moonshine-voice releases are unpinned.
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
## Stop
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
`poddrill stop` stops the running owner. Setup rejects a change while the owner is running. Run `poddrill stop` before you change the provider, queue length, fleet limit, or embedding limit.
|
|
51
47
|
|
|
52
|
-
|
|
48
|
+
## Cues
|
|
53
49
|
|
|
54
|
-
|
|
50
|
+
An asset maps each segment of the audio to text. That segment is a cue. A cue has the text and the times. You read the cues. You do not invent the text or the times.
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
Read cues only after the embedding is ready. A ready transcript can still be not-ready for cues.
|
|
57
53
|
|
|
58
|
-
##
|
|
54
|
+
## Speaker groups
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
An asset clusters cues that sound like one voice. That cluster is a speaker group. You assign the group a name, `quote`, `ending`, or `undetermined`. The utility does not choose the name.
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
A summary does not require those labels.
|
|
63
59
|
|
|
64
|
-
|
|
65
|
-
poddrill setup --provider <id> --queue-length 4
|
|
66
|
-
```
|
|
60
|
+
## Summaries
|
|
67
61
|
|
|
68
|
-
|
|
62
|
+
Summary is an asset of an episode. An episode can have multiple summary assets. You use the cues when you write a summary. PodDrill stores paragraphs that cite cues, then an episode summary written from the paragraph text. You choose which cues to include and you write the words. You do not write the file yourself.
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
## The episode
|
|
71
65
|
|
|
72
|
-
|
|
66
|
+
You keep the source list. The utility keeps the episode. The episode holds the transcript, the cues, the groups, the labels, and the summaries in your filesystem. Do not copy artifact state, progress, paths, or labels into the source list.
|
|
73
67
|
|
|
74
68
|
## Start an episode
|
|
75
69
|
|
|
@@ -79,12 +73,14 @@ Pass `--language <code>` on `poddrill start` when the episode is not English. Th
|
|
|
79
73
|
|
|
80
74
|
`poddrill start` returns before the transcript exists. Poll `poddrill episode <id>` until the transcript artifact `state` is `ready` or `failed`. The id is `ep_` plus the first 16 hex characters of the sha256 of the file bytes.
|
|
81
75
|
|
|
82
|
-
The transcript is done when that `state` is `ready`. Exit 0 does not mean the transcript is done. If
|
|
76
|
+
The transcript is done when that `state` is `ready`. Exit 0 does not mean the transcript is done. If the transcript or the embedding is missing or `failed`, pass the same WAV to `poddrill start` again. The episode id does not change. `created` is false, and the failed artifact runs again.
|
|
83
77
|
|
|
84
78
|
## Read a result
|
|
85
79
|
|
|
86
80
|
Parse the JSON line on stdout. `episode` exits 0 when the id is unknown. `not-ready` exits 0. `start` exits 1 when `kind` is `rejected` or `full`. MCP returns that same JSON. `isError` is true only when the text starts with `{"kind":"error"`.
|
|
87
81
|
|
|
82
|
+
`poddrill --help` and `poddrill --version` print plain text and exit 0.
|
|
83
|
+
|
|
88
84
|
## Present the summary
|
|
89
85
|
|
|
90
86
|
Present the summary in the format preferred or prescribed by users, but always grounded by the summary constructed via poddrill.
|
|
@@ -16,14 +16,14 @@ The script refuses a destination inside the git tree that contains the script. P
|
|
|
16
16
|
|
|
17
17
|
## yt-dlp
|
|
18
18
|
|
|
19
|
-
If the feed has no audio URL, or the script does not get the file, install the current yt-dlp and run that module. The printed line is the download path.
|
|
19
|
+
If the feed has no audio URL, or the script does not get the file, install the current yt-dlp into the PodDrill Python and run that module. The version is unpinned. On a PEP 668 Python, install into the venv from `poddrill install-python`, or set `PODDRILL_PYTHON` to an interpreter that allows pip. A pipx install or a standalone yt-dlp binary also works. The printed line is the download path.
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
"$PODDRILL_PYTHON" -m pip install --upgrade yt-dlp
|
|
23
|
+
"$PODDRILL_PYTHON" -m yt_dlp -x --no-playlist --print after_move:filepath -o "<directory>/download.%(ext)s" "<page-url>"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Write `<directory>` outside this git repository. If that command does not get the audio, read [Usage and options](https://github.com/yt-dlp/yt-dlp#usage-and-options).
|
|
26
|
+
`poddrill client doctor` reports yt-dlp as optional and prints that pip command when it is missing. Write `<directory>` outside this git repository. If that command does not get the audio, read [Usage and options](https://github.com/yt-dlp/yt-dlp#usage-and-options).
|
|
27
27
|
|
|
28
28
|
## Other audio
|
|
29
29
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# Set up Moonshine weights
|
|
2
2
|
|
|
3
|
-
PodDrill does not download the weights. Install the
|
|
3
|
+
PodDrill does not download the Moonshine weights. Install the current moonshine-voice release into the PodDrill Python, download the model with word timestamps, then pass the returned directory to setup. The version is unpinned. On a PEP 668 Python, use the venv from `poddrill install-python`.
|
|
4
4
|
|
|
5
|
-
Install the package first.
|
|
5
|
+
Install the package first. `$PODDRILL_PYTHON` is the interpreter PodDrill spawns.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
pip install moonshine-voice
|
|
8
|
+
"$PODDRILL_PYTHON" -m pip install moonshine-voice
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
A missing package fails with: `Install moonshine-voice into the PodDrill Python: "$PODDRILL_PYTHON" -m pip install moonshine-voice`.
|
|
12
|
+
|
|
13
|
+
`"$PODDRILL_PYTHON" -m moonshine_voice.download` on moonshine-voice 0.1.5 exits unless you pass `--stt`. That command prints `Downloaded model path:`. The directory on that line does not include the attention decoder, so word timestamps stay empty. Call `get_model_for_language` with `include_word_timestamps=True` instead. The returned path is the directory setup stores.
|
|
12
14
|
|
|
13
15
|
Setup still needs `--provider` and `--queue-length`. Use the provider the skill already named. Add `--model-weights` to that command.
|
|
14
16
|
|
package/README.md
CHANGED
|
@@ -10,16 +10,31 @@ Copy this to the agent.
|
|
|
10
10
|
Set up PodDrill on this computer.
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The npm package
|
|
13
|
+
The supported install is the npm package. Read the published `version` and run `npm install -g poddrill@<version>`. Cloning this repository is not required. If you clone it, set `GIT_TERMINAL_PROMPT=0`. The agent then follows the skill shipped in the package.
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
17
|
-
The computer that runs PodDrill needs
|
|
17
|
+
The computer that runs PodDrill needs:
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
- Node.js 20.19 or newer. Install the `.tar.gz` archive from `https://nodejs.org/dist/v20.19.0/`, or run `npx -y node@20.19.0`. The published CLI is plain JavaScript. The tests in this repository use `node --experimental-strip-types` and need a newer Node.
|
|
20
|
+
- `ffmpeg` on `PATH`.
|
|
21
|
+
- A Python interpreter with cactus-needle 3.2.0, numpy 2.5.4, and onnxruntime 1.31.0. The npm package named cactus-needle is a different package. `poddrill install-python` creates `~/.cache/poddrill/venv` and installs those pins. It uses `uv` when `uv` is on PATH, and otherwise `python3 -m venv`. `PODDRILL_PYTHON` selects another absolute interpreter, and setup stores that path in `poddrill.json`.
|
|
22
|
+
- The ECAPA weights file. `poddrill fetch-weights` downloads `voxceleb_ECAPA512.onnx` from the pinned WeSpeaker commit, checks its size and SHA-256, and writes `~/.cache/poddrill/ecapa/voxceleb_ECAPA512.onnx`. `PODDRILL_EMBED_WEIGHTS` selects another path. WeSpeaker ECAPA-TDNN512 pretrained on VoxCeleb. VoxCeleb is used under CC BY 4.0.
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
`poddrill client doctor` prints a `fix` command for every check that is not ok. `poddrill bootstrap` runs `install-python`, `fetch-weights`, and `setup`.
|
|
25
|
+
|
|
26
|
+
Without `PODDRILL_WORKSPACE`, every command uses `~/poddrill`. `poddrill stop` stops the running owner.
|
|
27
|
+
|
|
28
|
+
Grok Bot cannot write its server entry. It prints the custom server to add: the absolute Node binary, `dist/cli.js`, and an env block. Add that server in the Grok Bot chat.
|
|
29
|
+
|
|
30
|
+
## Tests
|
|
31
|
+
|
|
32
|
+
`npm test` runs the unit tests. Those tests use `node --experimental-strip-types`.
|
|
33
|
+
|
|
34
|
+
`npm run test:smoke` runs a clean-home install, downloads the ECAPA file, and transcribes a short generated clip through `dist/cli.js`. It needs `python3` with `ensurepip`, `ffmpeg`, and `espeak-ng`. If one of those is missing, the test skips and names the missing tool. This repository has no CI workflow. Run `npm run test:smoke` on a computer that has those tools. Build `dist/` with `npm run build` first.
|
|
22
35
|
|
|
23
36
|
## License
|
|
24
37
|
|
|
25
38
|
MIT License. Copyright 2026 Jianyu Zhao.
|
|
39
|
+
|
|
40
|
+
The ECAPA weights are separate from this license. WeSpeaker ECAPA-TDNN512 pretrained on VoxCeleb. VoxCeleb is used under CC BY 4.0.
|
package/dist/cli.js
CHANGED
|
@@ -1,26 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { realpath } from "node:fs/promises";
|
|
4
|
-
import { isAbsolute
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { clientMain, workspaceSetupCollision } from "./client.js";
|
|
7
|
-
import { asRecord, errorCode } from "./disk.js";
|
|
7
|
+
import { asRecord, errorCode, pathExists } from "./disk.js";
|
|
8
|
+
import { embedWeightsPath } from "./embed.js";
|
|
8
9
|
import { isModelId } from "./models.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { nodeInstallMessage, nodeMeetsMinimum } from "./node-version.js";
|
|
11
|
+
import { callOwner, ownerAlive, proxyMcp, pythonForOwner, render, serve } from "./owner.js";
|
|
12
|
+
import { readPackageIdentity } from "./package-identity.js";
|
|
13
|
+
import { installPython } from "./python-env.js";
|
|
14
|
+
import { fetchEcapa } from "./weights.js";
|
|
15
|
+
import { defaultWorkspacePath, resolveWorkspace, setupWorkspace } from "./workspace.js";
|
|
11
16
|
const usage = [
|
|
12
17
|
"usage: poddrill setup --provider <id> --queue-length <n> [--fleet-limit <n>] [--whistle-limit <n>]",
|
|
13
|
-
"[--model-fleet <id>=<count>] [--model-weights <id>=<path>] [--embedding-limit <n>]",
|
|
18
|
+
"[--model-fleet <id>=<count>] [--model-weights <id>=<path>] [--embedding-limit <n>] [--python <path>]",
|
|
19
|
+
"| install-python | fetch-weights | bootstrap [--provider <id>] [--queue-length <n>]",
|
|
14
20
|
"| start <path> [--language <code>] | episode <episodeId> | cues <episodeId> [--limit <n>] [--after <cueId>] [--group-id <id>] [--hosts]",
|
|
15
21
|
"| label-groups <episodeId> <json> | start-summary <episodeId> | set-summary <episodeId> <summaryId> <json>",
|
|
16
|
-
"| put-paragraph <episodeId> <summaryId> <json> | post-paragraph <episodeId> <summaryId> <json> | summary <episodeId> <summaryId> | audit <episodeId>
|
|
22
|
+
"| put-paragraph <episodeId> <summaryId> <json> | post-paragraph <episodeId> <summaryId> <json> | summary <episodeId> <summaryId> | audit <episodeId>",
|
|
23
|
+
"| serve | stop | mcp [--selftest] | --help | --version",
|
|
17
24
|
].join(" ");
|
|
18
25
|
async function main() {
|
|
26
|
+
if (!nodeMeetsMinimum(process.versions.node)) {
|
|
27
|
+
await emit(render({ kind: "error", message: nodeInstallMessage(process.versions.node) }), 1);
|
|
28
|
+
}
|
|
19
29
|
const command = process.argv[2];
|
|
20
30
|
switch (command) {
|
|
31
|
+
case "--help":
|
|
32
|
+
case "-h":
|
|
33
|
+
case "help":
|
|
34
|
+
await emit(helpText(), 0);
|
|
35
|
+
case "--version":
|
|
36
|
+
case "-v":
|
|
37
|
+
case "version":
|
|
38
|
+
await emit(await versionText(), 0);
|
|
21
39
|
case "setup":
|
|
22
40
|
await runSetup(process.argv.slice(3));
|
|
23
41
|
return;
|
|
42
|
+
case "install-python":
|
|
43
|
+
await runInstallPython();
|
|
44
|
+
return;
|
|
45
|
+
case "fetch-weights":
|
|
46
|
+
await runFetchWeights();
|
|
47
|
+
return;
|
|
48
|
+
case "bootstrap":
|
|
49
|
+
await runBootstrap(process.argv.slice(3));
|
|
50
|
+
return;
|
|
51
|
+
case "stop":
|
|
52
|
+
await runStop();
|
|
53
|
+
return;
|
|
24
54
|
case "client": {
|
|
25
55
|
const rendered = await clientMain(process.argv.slice(3), process.env, import.meta.url);
|
|
26
56
|
await emit(rendered.text, rendered.exitCode);
|
|
@@ -60,7 +90,7 @@ async function main() {
|
|
|
60
90
|
await serve(await resolveWorkspace(process.env), false);
|
|
61
91
|
return;
|
|
62
92
|
case "mcp":
|
|
63
|
-
await runMcp();
|
|
93
|
+
await runMcp(process.argv.slice(3));
|
|
64
94
|
return;
|
|
65
95
|
default:
|
|
66
96
|
await emit(render({ kind: "error", message: usage }), 1);
|
|
@@ -91,6 +121,7 @@ async function runSetup(args) {
|
|
|
91
121
|
const fleetLimit = sharedLimit(fleetText, whistleText);
|
|
92
122
|
const modelFleet = modelFleetFrom(fleetArgs);
|
|
93
123
|
const modelWeights = modelWeightsFrom(weightArgs);
|
|
124
|
+
const python = await chosenPython(args);
|
|
94
125
|
const root = await workspaceArg();
|
|
95
126
|
const alive = await ownerAlive(root);
|
|
96
127
|
const result = await setupWorkspace({
|
|
@@ -101,6 +132,7 @@ async function runSetup(args) {
|
|
|
101
132
|
...(modelFleet === undefined ? {} : { modelFleet }),
|
|
102
133
|
...(modelWeights === undefined ? {} : { modelWeights }),
|
|
103
134
|
...(embeddingText === undefined ? {} : { embeddingLimit: Number(embeddingText) }),
|
|
135
|
+
...(python === undefined ? {} : { python }),
|
|
104
136
|
ownerAlive: alive,
|
|
105
137
|
});
|
|
106
138
|
await emit(render(publicSetup(result)), result.kind === "ready" ? 0 : 1);
|
|
@@ -262,7 +294,66 @@ async function runEpisode(episodeId) {
|
|
|
262
294
|
await ensureOwner(root);
|
|
263
295
|
await emit(await callOwner(root, { op: "episode", episodeId }), 0);
|
|
264
296
|
}
|
|
265
|
-
async function
|
|
297
|
+
async function runInstallPython() {
|
|
298
|
+
const installed = await installPython(process.env, import.meta.url);
|
|
299
|
+
await emit(render({ kind: "ready", ...installed }), 0);
|
|
300
|
+
}
|
|
301
|
+
async function runFetchWeights() {
|
|
302
|
+
const fetched = await fetchEcapa(embedWeightsPath(process.env));
|
|
303
|
+
await emit(render({ kind: "ready", ...fetched }), 0);
|
|
304
|
+
}
|
|
305
|
+
async function runBootstrap(args) {
|
|
306
|
+
const provider = flag(args, "--provider") ?? "localhost";
|
|
307
|
+
const queueText = flag(args, "--queue-length") ?? "4";
|
|
308
|
+
if (!isCountText(queueText) || flag(args, "--provider") === "") {
|
|
309
|
+
await emit(render({ kind: "error", message: usage }), 1);
|
|
310
|
+
}
|
|
311
|
+
const installed = await installPython(process.env, import.meta.url);
|
|
312
|
+
const weights = await fetchEcapa(embedWeightsPath(process.env));
|
|
313
|
+
const root = await workspaceArg();
|
|
314
|
+
const alive = await ownerAlive(root);
|
|
315
|
+
const result = await setupWorkspace({
|
|
316
|
+
root,
|
|
317
|
+
provider,
|
|
318
|
+
queueLength: Number(queueText),
|
|
319
|
+
python: installed.python,
|
|
320
|
+
ownerAlive: alive,
|
|
321
|
+
});
|
|
322
|
+
if (result.kind !== "ready") {
|
|
323
|
+
await emit(render(publicSetup(result)), 1);
|
|
324
|
+
}
|
|
325
|
+
await emit(render({
|
|
326
|
+
kind: "ready",
|
|
327
|
+
python: installed.python,
|
|
328
|
+
installer: installed.installer,
|
|
329
|
+
weights,
|
|
330
|
+
setup: publicSetup(result),
|
|
331
|
+
}), 0);
|
|
332
|
+
}
|
|
333
|
+
async function runStop() {
|
|
334
|
+
const root = await resolveWorkspace(process.env);
|
|
335
|
+
if (!(await ownerAlive(root))) {
|
|
336
|
+
await emit(render({ kind: "ready", state: "stopped" }), 0);
|
|
337
|
+
}
|
|
338
|
+
const text = await callOwner(root, { op: "stop" });
|
|
339
|
+
await emit(text, text.includes("\"kind\":\"error\"") ? 1 : 0);
|
|
340
|
+
}
|
|
341
|
+
async function runMcp(args) {
|
|
342
|
+
if (args.includes("--selftest")) {
|
|
343
|
+
const root = await resolveWorkspace(process.env);
|
|
344
|
+
const python = await pythonForOwner(process.env, root);
|
|
345
|
+
await emit(render({
|
|
346
|
+
kind: "ready",
|
|
347
|
+
execPath: process.execPath,
|
|
348
|
+
script: fileURLToPath(import.meta.url),
|
|
349
|
+
env: {
|
|
350
|
+
PATH: process.env["PATH"] ?? "",
|
|
351
|
+
PODDRILL_PYTHON: python,
|
|
352
|
+
PODDRILL_WORKSPACE: defaultWorkspacePath(process.env),
|
|
353
|
+
PODDRILL_EMBED_WEIGHTS: embedWeightsPath(process.env),
|
|
354
|
+
},
|
|
355
|
+
}), 0);
|
|
356
|
+
}
|
|
266
357
|
const root = await resolveWorkspace(process.env);
|
|
267
358
|
if (await ownerAlive(root)) {
|
|
268
359
|
await proxyMcp(root);
|
|
@@ -296,6 +387,7 @@ function publicSetup(result) {
|
|
|
296
387
|
queueLength: result.report.config.queueLength,
|
|
297
388
|
fleetLimit: result.report.config.fleetLimit,
|
|
298
389
|
embeddingLimit: result.report.config.embeddingLimit,
|
|
390
|
+
...(result.report.config.python === undefined ? {} : { python: result.report.config.python }),
|
|
299
391
|
};
|
|
300
392
|
case "rejected":
|
|
301
393
|
return {
|
|
@@ -317,8 +409,7 @@ function isCountText(text) {
|
|
|
317
409
|
return /^[1-9]\d*$/.test(text);
|
|
318
410
|
}
|
|
319
411
|
async function workspaceArg() {
|
|
320
|
-
const
|
|
321
|
-
const candidate = resolve(configured === undefined || configured === "" ? process.cwd() : configured);
|
|
412
|
+
const candidate = defaultWorkspacePath(process.env);
|
|
322
413
|
try {
|
|
323
414
|
return await realpath(candidate);
|
|
324
415
|
}
|
|
@@ -430,6 +521,68 @@ function splitPair(text) {
|
|
|
430
521
|
}
|
|
431
522
|
return { left: text.slice(0, index), right: text.slice(index + 1) };
|
|
432
523
|
}
|
|
524
|
+
async function chosenPython(args) {
|
|
525
|
+
const flagged = flag(args, "--python");
|
|
526
|
+
const fromEnv = process.env["PODDRILL_PYTHON"];
|
|
527
|
+
const flaggedSet = flagged !== undefined && flagged !== "";
|
|
528
|
+
const envSet = fromEnv !== undefined && fromEnv !== "";
|
|
529
|
+
if (!flaggedSet && !envSet) {
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
532
|
+
if (flaggedSet && envSet && flagged !== fromEnv) {
|
|
533
|
+
await emit(render({ kind: "error", message: `python ${flagged} and PODDRILL_PYTHON ${fromEnv} differ` }), 1);
|
|
534
|
+
}
|
|
535
|
+
const chosen = flaggedSet ? flagged : fromEnv;
|
|
536
|
+
if (typeof chosen !== "string" || !isAbsolute(chosen)) {
|
|
537
|
+
await emit(render({ kind: "error", message: `python must be an absolute path, got ${String(chosen)}` }), 1);
|
|
538
|
+
}
|
|
539
|
+
if (typeof chosen !== "string" || !(await pathExists(chosen))) {
|
|
540
|
+
await emit(render({ kind: "error", message: `python interpreter not found at ${String(chosen)}` }), 1);
|
|
541
|
+
}
|
|
542
|
+
if (typeof chosen !== "string") {
|
|
543
|
+
await emit(render({ kind: "error", message: "python must be an absolute path" }), 1);
|
|
544
|
+
}
|
|
545
|
+
return chosen;
|
|
546
|
+
}
|
|
547
|
+
function helpText() {
|
|
548
|
+
const lines = [
|
|
549
|
+
"poddrill transcribes audio, groups voices, and stores a summary.",
|
|
550
|
+
"",
|
|
551
|
+
"Commands:",
|
|
552
|
+
" setup --provider <id> --queue-length <n> [--fleet-limit <n>] [--embedding-limit <n>]",
|
|
553
|
+
" [--model-fleet <id>=<count>] [--model-weights <id>=<path>] [--python <path>]",
|
|
554
|
+
" install-python create ~/.cache/poddrill/venv and install pinned Python packages",
|
|
555
|
+
" fetch-weights download the pinned ECAPA file and check its sha256",
|
|
556
|
+
" bootstrap [--provider <id>] [--queue-length <n>]",
|
|
557
|
+
" install-python, fetch-weights, then setup",
|
|
558
|
+
" start <path> [--language <code>]",
|
|
559
|
+
" episode <episodeId>",
|
|
560
|
+
" cues <episodeId> [--limit <n>] [--after <cueId>] [--group-id <id>] [--hosts]",
|
|
561
|
+
" label-groups <episodeId> <json>",
|
|
562
|
+
" start-summary <episodeId>",
|
|
563
|
+
" set-summary <episodeId> <summaryId> <json>",
|
|
564
|
+
" put-paragraph <episodeId> <summaryId> <json>",
|
|
565
|
+
" post-paragraph <episodeId> <summaryId> <json>",
|
|
566
|
+
" summary <episodeId> <summaryId>",
|
|
567
|
+
" audit <episodeId>",
|
|
568
|
+
" serve",
|
|
569
|
+
" stop stop the running owner",
|
|
570
|
+
" mcp [--selftest]",
|
|
571
|
+
" client doctor|plan|apply",
|
|
572
|
+
"",
|
|
573
|
+
" --help show this text",
|
|
574
|
+
" --version print the package version",
|
|
575
|
+
"",
|
|
576
|
+
"Providers localhost, grok-bot, and cursor-cloud all run the local Python host on this computer.",
|
|
577
|
+
"Every command uses PODDRILL_WORKSPACE, or ~/poddrill when that variable is unset.",
|
|
578
|
+
"Python is PODDRILL_PYTHON, then the path stored by setup, then ~/.cache/poddrill/venv/bin/python, then python3.",
|
|
579
|
+
];
|
|
580
|
+
return `${lines.join("\n")}\n`;
|
|
581
|
+
}
|
|
582
|
+
async function versionText() {
|
|
583
|
+
const identity = await readPackageIdentity(import.meta.url);
|
|
584
|
+
return `poddrill ${identity.version}\n`;
|
|
585
|
+
}
|
|
433
586
|
function delay(ms) {
|
|
434
587
|
return new Promise((resolve) => {
|
|
435
588
|
setTimeout(resolve, ms);
|