poddrill 0.2.1 → 0.3.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 +11 -9
- package/.agents/skills/poddrill/references/create-summary.md +19 -9
- package/.agents/skills/poddrill/references/fetch-audio.md +3 -3
- package/.agents/skills/poddrill/references/select-models.md +1 -1
- package/.agents/skills/poddrill/scripts/fetch-audio.mjs +5 -4
- package/README.md +26 -7
- package/dist/client.js +1 -1
- package/dist/owner.js +26 -3
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
|
|
10
10
|
# PodDrill
|
|
11
11
|
|
|
12
|
-
PodDrill maps an episode (the audio) to cues, clusters those cues into speaker groups, and stores summaries. You
|
|
12
|
+
PodDrill maps an episode (the audio) to cues, clusters those cues into speaker groups, and stores summaries. You name the voices and you write the summary words. PodDrill stores the episode. When the user wants that episode handled, follow the order below. Do not skip a step.
|
|
13
13
|
|
|
14
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.
|
|
15
15
|
|
|
@@ -19,15 +19,15 @@ Do this once on the computer that will transcribe. Each step is safe to repeat.
|
|
|
19
19
|
|
|
20
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`.
|
|
21
21
|
|
|
22
|
-
2. Install the npm package.
|
|
22
|
+
2. Install the npm package. Run `npm install -g poddrill@0.3.0`. Do not use `@latest`. Cloning the git repository is not required. If you clone it, set `GIT_TERMINAL_PROMPT=0`.
|
|
23
23
|
|
|
24
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.
|
|
25
25
|
|
|
26
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.
|
|
27
27
|
|
|
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. `
|
|
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. Skip a check whose `state` is `optional`. Do not install it. Do not mention it. Do not ask the user about it. `workspace` stays `pending` until setup. Stop this loop when doctor exits 0, or when the only `bad` check is `link`.
|
|
29
29
|
|
|
30
|
-
6. Run `poddrill setup --provider localhost --queue-length 4`. Without `PODDRILL_WORKSPACE`, setup and every other command use `~/poddrill`.
|
|
30
|
+
6. Run `poddrill setup --provider localhost --queue-length 4`. Without `PODDRILL_WORKSPACE`, setup and every other command use `~/poddrill`. When the episode language is `ja`, `ko`, or `zh`, read the weights guide before this command. Ask the user before you install anything that guide names.
|
|
31
31
|
|
|
32
32
|
`poddrill bootstrap --provider localhost --queue-length 4` runs steps 3, 4, and 6.
|
|
33
33
|
|
|
@@ -39,7 +39,7 @@ A later refresh of clients that already have an entry is `poddrill client plan -
|
|
|
39
39
|
|
|
40
40
|
## Python
|
|
41
41
|
|
|
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`.
|
|
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`.
|
|
43
43
|
|
|
44
44
|
## Stop
|
|
45
45
|
|
|
@@ -55,7 +55,9 @@ Read cues only after the embedding is ready. A ready transcript can still be not
|
|
|
55
55
|
|
|
56
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.
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
Label the speaker groups before you write a summary. Do not write a summary from the transcript text alone.
|
|
59
|
+
|
|
60
|
+
The order is `poddrill start`, then poll `poddrill episode` until the transcript and the embedding are `ready`, then `poddrill cues` and `poddrill label-groups` from the speaker guide, then `poddrill start-summary`, `poddrill post-paragraph`, and `poddrill set-summary` from the summary guide. Present the text from `poddrill summary`. A summary written in chat before those commands is not the result.
|
|
59
61
|
|
|
60
62
|
## Summaries
|
|
61
63
|
|
|
@@ -83,7 +85,7 @@ Parse the JSON line on stdout. `episode` exits 0 when the id is unknown. `not-re
|
|
|
83
85
|
|
|
84
86
|
## Present the summary
|
|
85
87
|
|
|
86
|
-
Present the summary
|
|
88
|
+
Present the text returned by `poddrill summary`. Match the format the user asked for. Do not replace that text with a summary you wrote from the transcript.
|
|
87
89
|
|
|
88
90
|
## Guides
|
|
89
91
|
|
|
@@ -92,8 +94,8 @@ Each file tells you how to do one part of the task.
|
|
|
92
94
|
- For a feed or a YouTube page, and for telling a new episode from one already handed to poddrill, read [references/track-sources.md](references/track-sources.md).
|
|
93
95
|
- For finding the audio, downloading it, and writing the 16 kHz WAV, read [references/fetch-audio.md](references/fetch-audio.md).
|
|
94
96
|
- For Japanese, Korean, or Chinese weights, read [references/select-models.md](references/select-models.md).
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
+
- Before any summary, read [references/assign-speakers.md](references/assign-speakers.md) and label the groups.
|
|
98
|
+
- After those labels, read [references/create-summary.md](references/create-summary.md) and store the summary with its commands.
|
|
97
99
|
|
|
98
100
|
## Finish
|
|
99
101
|
|
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
# Create an episode summary
|
|
2
2
|
|
|
3
|
-
Build one summary of an episode from its cues. One episode can hold many summaries. Each one is a YAML file. You do not write that file yourself. Use the commands below.
|
|
3
|
+
Build one summary of an episode from its cues. Do this after the speaker groups are labeled. Do not write a summary from the transcript text. One episode can hold many summaries. Each one is a YAML file. You do not write that file yourself. Use the commands below.
|
|
4
4
|
|
|
5
5
|
Read the cues first with `poddrill cues`. Page them as in [Speaker assignment](assign-speakers.md).
|
|
6
6
|
|
|
7
7
|
The requirement is what the user asked you to summarize. Cite the cues that answer the requirement. You do not need to include a cue if it does not contribute to answering the requirement.
|
|
8
8
|
|
|
9
|
-
Before you write, mark the parts that are actually there. An episode can hold several. A part the cues do not support is left out.
|
|
9
|
+
Before you write, mark the parts that are actually there. An episode can hold several. A part the cues do not support is left out. When you mark a part, mark which cues hold each piece below. A piece the speaker never gives is absent. Do not invent it.
|
|
10
10
|
|
|
11
11
|
### News commentary
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
What happened. Each opinion about it. Who holds that opinion.
|
|
14
14
|
|
|
15
15
|
### Explanation
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
How it works or why it is that way. Naming the subject is not that piece.
|
|
18
18
|
|
|
19
19
|
### Advice
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
What the listener should do. When the speaker gives a how or a decision rule, include that piece.
|
|
22
22
|
|
|
23
23
|
### Claim
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
What the speaker holds, including a hedge. How they explain it. The evidence they give. When the cues correct who is speaking, or show where they stop, include that piece.
|
|
26
26
|
|
|
27
27
|
### Discussion
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
The topic. Each person's view, written as a claim. How the people respond to each other.
|
|
30
|
+
|
|
31
|
+
## Check coverage
|
|
32
|
+
|
|
33
|
+
The paragraph `text` and its `cues` must include every piece the speaker gave for that part. When a part spans several paragraphs, check those paragraphs together.
|
|
34
|
+
|
|
35
|
+
When a piece the type normally has is absent, say so in the paragraph. Write "He gives no reason." Do not leave the gap unstated.
|
|
36
|
+
|
|
37
|
+
A hedge is part of what the speaker holds. Keep it. The rule in [Orwell's rules](orwell.md) that cuts a word does not remove a hedge.
|
|
38
|
+
|
|
39
|
+
Before `setSummary`, check every marked part. Replace a paragraph that misses a piece the speaker gave with `putParagraph`. The summary must not state a part more completely, or more certainly, than its paragraphs do.
|
|
30
40
|
|
|
31
41
|
## Paragraphs and the summary
|
|
32
42
|
|
|
33
43
|
A cue belongs in a paragraph when it supports a part you marked for the requirement.
|
|
34
44
|
|
|
35
|
-
You decide how the parts sit in the paragraphs. A part may take more than one paragraph. Link the cues that belong to one part
|
|
45
|
+
You decide how the parts sit in the paragraphs. A part may take more than one paragraph. Link the cues that belong to one part. Then run the coverage check.
|
|
36
46
|
|
|
37
47
|
Write `summary` after those paragraphs exist. Write it from the paragraph `text`.
|
|
38
48
|
|
|
@@ -94,7 +104,7 @@ The MCP tool is `putParagraph`. Pass `episodeId`, `summaryId`, `index`, `text`,
|
|
|
94
104
|
|
|
95
105
|
## Set the episode summary
|
|
96
106
|
|
|
97
|
-
Write this after the paragraphs. `setSummary` rejects a file that has no paragraphs.
|
|
107
|
+
Write this after the paragraphs, and after the coverage check. `setSummary` rejects a file that has no paragraphs.
|
|
98
108
|
|
|
99
109
|
`summary` is the prose for the whole episode. Those words follow [Orwell's rules](orwell.md). An empty string is rejected.
|
|
100
110
|
|
|
@@ -12,18 +12,18 @@ Download that URL with the script in this skill. The file is `scripts/fetch-audi
|
|
|
12
12
|
node <skill-dir>/scripts/fetch-audio.mjs <url> <output-path> [expected-bytes]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Run this script even when the installed copy is not inside a git repository. Do not replace it with a script of your own. It refuses a destination inside a git tree that contains the script. Pass the byte length when the feed lists one. The script leaves an existing file of that size in place.
|
|
16
16
|
|
|
17
17
|
## yt-dlp
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Ask the user before you install yt-dlp. Do that only when the feed has no audio URL, or the script did not write the file. Then 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
22
|
"$PODDRILL_PYTHON" -m pip install --upgrade yt-dlp
|
|
23
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
|
-
|
|
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).
|
|
27
27
|
|
|
28
28
|
## Other audio
|
|
29
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Set up Moonshine weights
|
|
2
2
|
|
|
3
|
-
PodDrill does not download the
|
|
3
|
+
Read this only when the episode language is `ja`, `ko`, or `zh`. Ask the user before you install moonshine-voice. PodDrill does not download the weights. After the user agrees, 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
5
|
Install the package first. `$PODDRILL_PYTHON` is the interpreter PodDrill spawns.
|
|
6
6
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createWriteStream } from "node:fs";
|
|
1
|
+
import { createWriteStream, realpathSync } from "node:fs";
|
|
2
2
|
import { mkdir, realpath, rename, rm, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
@@ -15,7 +15,7 @@ export async function downloadTo(url, outputPath, expectedByteLength) {
|
|
|
15
15
|
const expected = readExpectedByteLength(expectedByteLength);
|
|
16
16
|
const destination = await canonicalPath(outputPath);
|
|
17
17
|
const root = await gitRoot();
|
|
18
|
-
if (isInside(root, destination)) {
|
|
18
|
+
if (root !== undefined && isInside(root, destination)) {
|
|
19
19
|
throw new Error(`refusing to write inside the git tree: ${destination}`);
|
|
20
20
|
}
|
|
21
21
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
@@ -90,7 +90,7 @@ async function gitRoot() {
|
|
|
90
90
|
}
|
|
91
91
|
const parent = path.dirname(current);
|
|
92
92
|
if (parent === current) {
|
|
93
|
-
|
|
93
|
+
return undefined;
|
|
94
94
|
}
|
|
95
95
|
current = parent;
|
|
96
96
|
}
|
|
@@ -131,7 +131,8 @@ function parseCliByteLength(raw) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const entry = process.argv[1];
|
|
134
|
-
|
|
134
|
+
const invoked = entry === undefined ? undefined : realpathSync(entry);
|
|
135
|
+
if (invoked !== undefined && invoked === fileURLToPath(import.meta.url)) {
|
|
135
136
|
const url = process.argv[2];
|
|
136
137
|
const outputPath = process.argv[3];
|
|
137
138
|
const rawLength = process.argv[4];
|
package/README.md
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
# PodDrill
|
|
2
2
|
|
|
3
|
-
PodDrill is a
|
|
3
|
+
PodDrill is a tool that helps an agent to transcribe podcast audio into timed text, group the voices, and store a summary. It provides an agent skill and a set of best practices how to summarize a podcast efficiently and with clear structures.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### Set up your agent
|
|
8
|
+
|
|
9
|
+
Ask your agent:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Set up PodDrill skill and tool: https://github.com/JeromeZhao/PodDrill
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Monitor a podcast
|
|
16
|
+
|
|
17
|
+
Set up a monitor on a podcast and summarize new episode:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
/PodDrill monitor <podcast name you want to monitor> podcast and summary newly released episode.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Summarize your own way
|
|
24
|
+
|
|
25
|
+
Identify books mentioned in an episode and what it is about:
|
|
8
26
|
|
|
9
27
|
```text
|
|
10
|
-
|
|
28
|
+
/PodDrill for new episode from <podcast name you want to monitor>, extract what books it mentioned and what the book is about.
|
|
11
29
|
```
|
|
12
30
|
|
|
13
|
-
|
|
31
|
+
## For LLM and Agent
|
|
32
|
+
The supported install is the npm package. Run `npm install -g poddrill@0.3.0`. Do not use `@latest`. 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
33
|
|
|
15
|
-
|
|
34
|
+
### Requirements
|
|
16
35
|
|
|
17
36
|
The computer that runs PodDrill needs:
|
|
18
37
|
|
|
@@ -31,7 +50,7 @@ Grok Bot cannot write its server entry. It prints the custom server to add: the
|
|
|
31
50
|
|
|
32
51
|
`npm test` runs the unit tests. Those tests use `node --experimental-strip-types`.
|
|
33
52
|
|
|
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.
|
|
53
|
+
`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. Run `npm run test:smoke` on a computer that has those tools. Build `dist/` with `npm run build` first.
|
|
35
54
|
|
|
36
55
|
## License
|
|
37
56
|
|
package/dist/client.js
CHANGED
|
@@ -885,7 +885,7 @@ async function ytDlpCheck(env, python) {
|
|
|
885
885
|
return {
|
|
886
886
|
name: "yt-dlp",
|
|
887
887
|
state: "optional",
|
|
888
|
-
detail: "
|
|
888
|
+
detail: "Not required for setup.",
|
|
889
889
|
fix: ytDlpFix(python),
|
|
890
890
|
};
|
|
891
891
|
}
|
package/dist/owner.js
CHANGED
|
@@ -393,10 +393,33 @@ function parseRequest(value) {
|
|
|
393
393
|
if (op === "labelGroups" && typeof record["episodeId"] === "string") {
|
|
394
394
|
return { op: "labelGroups", episodeId: record["episodeId"], groups: record["groups"] };
|
|
395
395
|
}
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
return socketSummary(record);
|
|
397
|
+
}
|
|
398
|
+
function socketSummary(record) {
|
|
399
|
+
const op = record["op"];
|
|
400
|
+
const episodeId = record["episodeId"];
|
|
401
|
+
if (typeof episodeId !== "string") {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
if (op === "startSummary") {
|
|
405
|
+
return { op: "startSummary", episodeId };
|
|
406
|
+
}
|
|
407
|
+
const summaryId = record["summaryId"];
|
|
408
|
+
if (typeof summaryId !== "string") {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
switch (op) {
|
|
412
|
+
case "setSummary":
|
|
413
|
+
return { op: "setSummary", episodeId, summaryId, summary: record["summary"] };
|
|
414
|
+
case "putParagraph":
|
|
415
|
+
return { op: "putParagraph", episodeId, summaryId, body: record["body"] };
|
|
416
|
+
case "postParagraph":
|
|
417
|
+
return { op: "postParagraph", episodeId, summaryId, body: record["body"] };
|
|
418
|
+
case "summary":
|
|
419
|
+
return { op: "summary", episodeId, summaryId };
|
|
420
|
+
default:
|
|
421
|
+
return undefined;
|
|
398
422
|
}
|
|
399
|
-
return undefined;
|
|
400
423
|
}
|
|
401
424
|
function summaryRequest(name, args) {
|
|
402
425
|
const episodeId = args["episodeId"];
|