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.
@@ -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 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.
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. 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`.
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. `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`.
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`. 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.
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`. Install yt-dlp and moonshine-voice into that same interpreter. The current yt-dlp and moonshine-voice releases are unpinned.
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
- A summary does not require those labels.
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 in the format preferred or prescribed by users, but always grounded by the summary constructed via poddrill.
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
- - For a voice name, a quote, an ending, or cues left undetermined, read [references/assign-speakers.md](references/assign-speakers.md).
96
- - For an episode summary, read [references/create-summary.md](references/create-summary.md).
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
- News commentary is what happened, and the opinions on what happened. An opinion is that person's statement about what happened.
13
+ What happened. Each opinion about it. Who holds that opinion.
14
14
 
15
15
  ### Explanation
16
16
 
17
- An explanation is how something works or why it is that way.
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
- Advice is what the listener should do, how they should decide, or how to get better.
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
- A claim is what someone holds, how they elaborate it, and the backup they offer. The elaboration is how they explain the claim. A claim can include a correction of who is speaking, and a place they stop.
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
- A discussion is one topic, the perspectives on that topic, and how the people respond to each other. A perspective is a claim.
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, and cover what holds that part together. The summary written from those paragraphs stays clear, concise, and structured.
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
- The script refuses a destination inside the 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.
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
- 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.
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
- `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).
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 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`.
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
- throw new Error("refusing to download because no git tree contains the script");
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
- if (entry !== undefined && path.resolve(entry) === fileURLToPath(import.meta.url)) {
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 skill that transcribes episode audio into timed text, groups the voices, and stores a summary.
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
- ## Say this
5
+ ## Quick start
6
6
 
7
- Copy this to the agent.
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
- Set up PodDrill on this computer.
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
- 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.
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
- ## Requirements
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. 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.
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: "yt-dlp is optional. Install the current release into the PodDrill Python, or use a standalone yt-dlp binary.",
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
- if (typeof op === "string") {
397
- return summaryRequest(op, record);
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"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poddrill",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Transcribe episode audio into timed text, group the voices, and store a summary.",
5
5
  "license": "MIT",
6
6
  "author": "Jianyu Zhao",