@tacone/prosey 0.2.6 → 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/README.md +51 -25
- package/bin/prosey +308 -33
- package/package.json +1 -1
- package/src/config-resolve.test.ts +104 -0
- package/src/config-resolve.ts +17 -0
- package/src/config.test.ts +3 -1
- package/src/config.ts +26 -17
- package/src/default-config.toml +39 -11
- package/src/extract-chapters.test.ts +229 -0
- package/src/extract-chapters.ts +66 -0
- package/src/index.test.ts +12 -0
- package/src/index.ts +240 -10
- package/src/summarize.test.ts +74 -3
- package/src/summarize.ts +18 -4
package/README.md
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/prosey-logo.svg" alt="Prosey logo" width="400"/>
|
|
3
|
+
<br>
|
|
4
|
+
<br>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
1
7
|
# Prosey
|
|
2
8
|
|
|
3
|
-
**Prosey**
|
|
9
|
+
**Prosey** is a CLI tool to download and display video transcriptions from YouTube, so you can get the information you're looking for without wasting hours.
|
|
10
|
+
|
|
11
|
+
The main use cases are:
|
|
12
|
+
|
|
13
|
+
- **Summarize** a YouTube video and display it in the CLI with a single command (quicker).
|
|
14
|
+
- **Read** video captions, formatted and organized in chapters like the original video (often needs more time).
|
|
15
|
+
|
|
16
|
+
Both uses need an AI assistant on your system (default: [opencode](https://opencode.ai/), use `prosey config` to configure your own).
|
|
17
|
+
|
|
18
|
+
If we detect a syntax highlighter on your system, you'll get a nice colored output.
|
|
4
19
|
|
|
5
|
-
|
|
20
|
+
Prosey can also output or store the native json format verbatim.
|
|
6
21
|
|
|
7
22
|
## Features
|
|
8
23
|
|
|
9
|
-
| Feature
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
24
|
+
| Feature | Description |
|
|
25
|
+
| ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
26
|
+
| **💡 Summarize** | Fetch and summarize a video transcription |
|
|
27
|
+
| **📖 Read** | Fetch and display the transcript formatted in Markdown |
|
|
28
|
+
| **📋 Transcript download** | Fetch YouTube transcripts as plain text or JSON verbatim |
|
|
29
|
+
| **🤖 AI summarization** | Pipe transcripts to any AI agent (opencode, Claude, Copilot, Codex) |
|
|
30
|
+
| **🎨 Smart pager** | Automatic syntax highlighting if bat, glow or mdcat are installed (or configure your own pager) |
|
|
31
|
+
| **🛠️ Customization** | Use `prosey config` to access and edit your configuration / customize the prompts |
|
|
32
|
+
| **💾 Disk caching** | Transcripts and summaries are cached on disk for instant re-read |
|
|
33
|
+
| **📄 Supported formats** | Plain text and JSON |
|
|
17
34
|
|
|
18
35
|
## Quickstart
|
|
19
36
|
|
|
@@ -64,50 +81,59 @@ Grab a compiled binary from the `dist/` directory (requires no runtime).
|
|
|
64
81
|
|
|
65
82
|
```
|
|
66
83
|
prosey [options] <video-url-or-id>
|
|
84
|
+
prosey read [options] <video-url-or-id>
|
|
67
85
|
prosey info [options] <video-url-or-id>
|
|
68
86
|
prosey summarize [options] <video-url-or-id>
|
|
69
87
|
prosey config
|
|
88
|
+
prosey help
|
|
70
89
|
```
|
|
71
90
|
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
`summarize` is the default command, so `prosey <id>` runs the AI summarizer.
|
|
92
|
+
Use `prosey read <id>` to download and print a richly formatted transcript.
|
|
74
93
|
|
|
75
94
|
The `summarize` command fetches a transcript, prepends the prompt from the
|
|
76
|
-
`[summarize]` config section, and pipes the result to the configured command.
|
|
95
|
+
`[summarize]` config section, and pipes the result to the configured AI command.
|
|
96
|
+
|
|
97
|
+
The `read` command downloads the transcript and prints it to stdout (plain text,
|
|
98
|
+
markdown, or JSON). This is useful when you don't need AI processing.
|
|
99
|
+
|
|
100
|
+
The `info` command shows video metadata (title, channel, duration, views).
|
|
77
101
|
|
|
78
102
|
The `config` command opens your config file in `$EDITOR` for editing. If
|
|
79
103
|
`$EDITOR` is not set, the config file path is printed.
|
|
80
104
|
|
|
105
|
+
The `help` command shows the help message, same as `--help`.
|
|
106
|
+
|
|
81
107
|
### Examples
|
|
82
108
|
|
|
83
109
|
```bash
|
|
84
|
-
#
|
|
110
|
+
# Run the AI summarizer (default command)
|
|
85
111
|
prosey 771PQEDeRmw
|
|
86
112
|
|
|
113
|
+
# Download a plain transcript with details
|
|
114
|
+
prosey read 771PQEDeRmw
|
|
115
|
+
|
|
87
116
|
# Specify language
|
|
88
|
-
prosey https://youtu.be/771PQEDeRmw --lang es
|
|
117
|
+
prosey read https://youtu.be/771PQEDeRmw --lang es
|
|
89
118
|
|
|
90
119
|
# Include timestamps
|
|
91
|
-
prosey 771PQEDeRmw -t
|
|
120
|
+
prosey read 771PQEDeRmw -t
|
|
92
121
|
|
|
93
|
-
# Save to file
|
|
94
|
-
prosey 771PQEDeRmw -o transcript.txt
|
|
122
|
+
# Save transcript to file
|
|
123
|
+
prosey read 771PQEDeRmw -o transcript.txt
|
|
95
124
|
|
|
96
125
|
# JSON output (timestamps always included)
|
|
97
|
-
prosey 771PQEDeRmw --json
|
|
126
|
+
prosey read 771PQEDeRmw --json
|
|
98
127
|
|
|
99
128
|
# Transcript only, no video details
|
|
100
|
-
prosey 771PQEDeRmw --no-details
|
|
129
|
+
prosey read 771PQEDeRmw --no-details
|
|
101
130
|
|
|
102
131
|
# List available transcript languages
|
|
103
|
-
prosey 771PQEDeRmw --list
|
|
132
|
+
prosey read 771PQEDeRmw --list
|
|
104
133
|
|
|
105
134
|
# Show video metadata
|
|
106
135
|
prosey info 771PQEDeRmw
|
|
107
136
|
|
|
108
|
-
# Summarize via the configured command
|
|
109
|
-
prosey summarize 771PQEDeRmw
|
|
110
|
-
|
|
111
137
|
# Edit config in $EDITOR
|
|
112
138
|
prosey config
|
|
113
139
|
```
|
|
@@ -121,7 +147,7 @@ prosey config
|
|
|
121
147
|
| `--list` | List available transcript languages for the video, then exit. |
|
|
122
148
|
| `-o`, `--output <path>` | Write output to file instead of stdout. |
|
|
123
149
|
| `--json` | Output transcript as a JSON array. Each item includes `text`, `offset` (seconds), `duration`, and `timestamp`. |
|
|
124
|
-
| `--text` | Output as plain text (default).
|
|
150
|
+
| `--text` | Output as plain text (default for `prosey read`). |
|
|
125
151
|
| `--details` | Prepend video details (title, channel, duration, views, description) to the transcript (default). |
|
|
126
152
|
| `--no-details` | Suppress video details, transcript only. |
|
|
127
153
|
| `--no-decode-entities` | Preserve raw HTML entities (e.g. `'`). Decoded by default in text mode. |
|
package/bin/prosey
CHANGED
|
@@ -101612,6 +101612,11 @@ pager = "auto"
|
|
|
101612
101612
|
# Can also be set via PROSEY_HINTS env var (yes, no, 1, 0, true, false).
|
|
101613
101613
|
hints = true
|
|
101614
101614
|
|
|
101615
|
+
[ai]
|
|
101616
|
+
# Default command for AI operations (summarize, transcribe).
|
|
101617
|
+
# Can be overridden per-section via the command key below.
|
|
101618
|
+
command = "opencode run"
|
|
101619
|
+
|
|
101615
101620
|
[summarize]
|
|
101616
101621
|
# Prompt sent to the command via stdin.
|
|
101617
101622
|
# Customize this to change how transcripts are summarized.
|
|
@@ -101619,23 +101624,20 @@ prompt = """
|
|
|
101619
101624
|
Write a comprehensive summary of the following transcription.
|
|
101620
101625
|
"""
|
|
101621
101626
|
|
|
101622
|
-
# Command
|
|
101623
|
-
#
|
|
101624
|
-
#
|
|
101625
|
-
|
|
101626
|
-
|
|
101627
|
-
#
|
|
101628
|
-
#
|
|
101629
|
-
|
|
101630
|
-
|
|
101631
|
-
|
|
101632
|
-
|
|
101633
|
-
#
|
|
101634
|
-
#
|
|
101635
|
-
#
|
|
101636
|
-
# codex --sandbox default -p "" — full access
|
|
101637
|
-
# codex --sandbox read-only -p "" — read-only
|
|
101638
|
-
command = "opencode run"
|
|
101627
|
+
# Command override for summarize. Uncomment to use a different command
|
|
101628
|
+
# than the one specified in [ai].
|
|
101629
|
+
# command = "opencode run"
|
|
101630
|
+
|
|
101631
|
+
[transcribe]
|
|
101632
|
+
# Prompt sent to the command via stdin.
|
|
101633
|
+
# Customize this to change how transcripts are formatted as markdown.
|
|
101634
|
+
prompt = """
|
|
101635
|
+
Convert this transcript to clean, readable markdown.
|
|
101636
|
+
"""
|
|
101637
|
+
|
|
101638
|
+
# Command override for transcribe. Uncomment to use a different command
|
|
101639
|
+
# than the one specified in [ai].
|
|
101640
|
+
# command = "opencode run"
|
|
101639
101641
|
`;
|
|
101640
101642
|
async function readDefaultConfig() {
|
|
101641
101643
|
const paths = [
|
|
@@ -101685,7 +101687,7 @@ async function resetConfig() {
|
|
|
101685
101687
|
|
|
101686
101688
|
// src/summarize.ts
|
|
101687
101689
|
import { spawn } from "node:child_process";
|
|
101688
|
-
|
|
101690
|
+
var defaultExecuteCommand = (command, input, cwd) => {
|
|
101689
101691
|
return new Promise((resolve, reject) => {
|
|
101690
101692
|
const proc = spawn(command, [], { shell: true, stdio: "pipe", cwd });
|
|
101691
101693
|
let stdout = "";
|
|
@@ -101706,13 +101708,16 @@ function executeCommand(command, input, cwd) {
|
|
|
101706
101708
|
proc.stdin.write(input);
|
|
101707
101709
|
proc.stdin.end();
|
|
101708
101710
|
});
|
|
101709
|
-
}
|
|
101710
|
-
async function summarize(options) {
|
|
101711
|
+
};
|
|
101712
|
+
async function summarize(options, execCommand = defaultExecuteCommand) {
|
|
101711
101713
|
const { prompt, command, transcript, cwd } = options;
|
|
101714
|
+
if (!prompt) {
|
|
101715
|
+
throw new Error("No prompt configured. A prompt is required in the config.");
|
|
101716
|
+
}
|
|
101712
101717
|
const fullPrompt = `${prompt}
|
|
101713
101718
|
|
|
101714
101719
|
${transcript}`;
|
|
101715
|
-
const output = await
|
|
101720
|
+
const output = await execCommand(command, fullPrompt, cwd);
|
|
101716
101721
|
const cleaned = output.startsWith(fullPrompt) ? output.slice(fullPrompt.length).replace(/\n+$/, "") : output.replace(/\n+$/, "");
|
|
101717
101722
|
if (!cleaned || cleaned === transcript) {
|
|
101718
101723
|
throw new Error("Summarization command returned no meaningful output");
|
|
@@ -101720,6 +101725,20 @@ ${transcript}`;
|
|
|
101720
101725
|
return cleaned;
|
|
101721
101726
|
}
|
|
101722
101727
|
|
|
101728
|
+
// src/config-resolve.ts
|
|
101729
|
+
function resolveSummarizeCmd(config) {
|
|
101730
|
+
return config.summarize?.command ?? config.ai?.command ?? null;
|
|
101731
|
+
}
|
|
101732
|
+
function resolveSummarizePrompt(config) {
|
|
101733
|
+
return config.summarize?.prompt ?? null;
|
|
101734
|
+
}
|
|
101735
|
+
function resolveTranscribeCmd(config) {
|
|
101736
|
+
return config.transcribe?.command ?? config.ai?.command ?? config.summarize?.command ?? null;
|
|
101737
|
+
}
|
|
101738
|
+
function resolveTranscribePrompt(config) {
|
|
101739
|
+
return config.transcribe?.prompt ?? config.summarize?.prompt ?? null;
|
|
101740
|
+
}
|
|
101741
|
+
|
|
101723
101742
|
// src/cache.ts
|
|
101724
101743
|
import { createHash } from "node:crypto";
|
|
101725
101744
|
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
@@ -101756,10 +101775,62 @@ async function writeCache(dir, filename, data) {
|
|
|
101756
101775
|
await mkdir2(dir, { recursive: true });
|
|
101757
101776
|
await writeFile2(join2(dir, filename), data, "utf8");
|
|
101758
101777
|
}
|
|
101778
|
+
|
|
101779
|
+
// src/extract-chapters.ts
|
|
101780
|
+
var lineRegex = /^\s*[\[\(]?(?:(?:(\d{1,2}):)?(\d{1,2}):(\d{2}))[\]\)]?(?:\s*[-–—:.]\s*|\s+)(.+)$/;
|
|
101781
|
+
function extractChapters(description) {
|
|
101782
|
+
const lines = description.split(`
|
|
101783
|
+
`);
|
|
101784
|
+
const chapters = [];
|
|
101785
|
+
for (const line of lines) {
|
|
101786
|
+
const match = line.match(lineRegex);
|
|
101787
|
+
if (!match)
|
|
101788
|
+
continue;
|
|
101789
|
+
const hours = match[1] ? parseInt(match[1], 10) : 0;
|
|
101790
|
+
const minutes = parseInt(match[2], 10);
|
|
101791
|
+
const seconds = parseInt(match[3], 10);
|
|
101792
|
+
const title = match[4].trim();
|
|
101793
|
+
if (!title)
|
|
101794
|
+
continue;
|
|
101795
|
+
const time = hours * 3600 + minutes * 60 + seconds;
|
|
101796
|
+
chapters.push({ time, title });
|
|
101797
|
+
}
|
|
101798
|
+
chapters.sort((a2, b2) => a2.time - b2.time);
|
|
101799
|
+
const seen = new Set;
|
|
101800
|
+
return chapters.filter((c2) => {
|
|
101801
|
+
if (seen.has(c2.time))
|
|
101802
|
+
return false;
|
|
101803
|
+
seen.add(c2.time);
|
|
101804
|
+
return true;
|
|
101805
|
+
});
|
|
101806
|
+
}
|
|
101807
|
+
function formatChaptersAsJson(chapters) {
|
|
101808
|
+
if (chapters.length === 0)
|
|
101809
|
+
return "not available";
|
|
101810
|
+
const obj = {};
|
|
101811
|
+
for (const ch of chapters) {
|
|
101812
|
+
const h2 = Math.floor(ch.time / 3600);
|
|
101813
|
+
const m2 = Math.floor(ch.time % 3600 / 60);
|
|
101814
|
+
const s2 = ch.time % 60;
|
|
101815
|
+
const key = h2 > 0 ? `${String(h2).padStart(2, "0")}:${String(m2).padStart(2, "0")}:${String(s2).padStart(2, "0")}` : `${String(m2).padStart(2, "0")}:${String(s2).padStart(2, "0")}`;
|
|
101816
|
+
obj[key] = ch.title;
|
|
101817
|
+
}
|
|
101818
|
+
return JSON.stringify(obj);
|
|
101819
|
+
}
|
|
101820
|
+
function formatChaptersAsText(chapters) {
|
|
101821
|
+
return chapters.map((c2) => {
|
|
101822
|
+
const h2 = Math.floor(c2.time / 3600);
|
|
101823
|
+
const m2 = Math.floor(c2.time % 3600 / 60);
|
|
101824
|
+
const s2 = c2.time % 60;
|
|
101825
|
+
const timeStr = h2 > 0 ? `${String(h2).padStart(2, "0")}:${String(m2).padStart(2, "0")}:${String(s2).padStart(2, "0")}` : `${String(m2).padStart(2, "0")}:${String(s2).padStart(2, "0")}`;
|
|
101826
|
+
return `${timeStr} ${c2.title}`;
|
|
101827
|
+
}).join(`
|
|
101828
|
+
`);
|
|
101829
|
+
}
|
|
101759
101830
|
// package.json
|
|
101760
101831
|
var package_default = {
|
|
101761
101832
|
name: "@tacone/prosey",
|
|
101762
|
-
version: "0.
|
|
101833
|
+
version: "0.3.0",
|
|
101763
101834
|
description: "Download YouTube video transcripts from the CLI",
|
|
101764
101835
|
module: "src/index.ts",
|
|
101765
101836
|
type: "module",
|
|
@@ -120385,6 +120456,10 @@ var debugApis = {
|
|
|
120385
120456
|
};
|
|
120386
120457
|
|
|
120387
120458
|
// src/index.ts
|
|
120459
|
+
process.stdout.on("error", (err) => {
|
|
120460
|
+
if (err.code === "EPIPE")
|
|
120461
|
+
process.exit(0);
|
|
120462
|
+
});
|
|
120388
120463
|
var NAME2 = "prosey";
|
|
120389
120464
|
var VERSION2 = package_default.version;
|
|
120390
120465
|
var latestVersion = null;
|
|
@@ -120401,16 +120476,20 @@ function help() {
|
|
|
120401
120476
|
return `${NAME2} v${VERSION2}
|
|
120402
120477
|
|
|
120403
120478
|
Usage: ${NAME2} [options] <video-url-or-id>
|
|
120479
|
+
${NAME2} read [options] <video-url-or-id>
|
|
120404
120480
|
${NAME2} info [options] <video-url-or-id>
|
|
120405
120481
|
${NAME2} summarize [options] <video-url-or-id>
|
|
120406
120482
|
${NAME2} config
|
|
120483
|
+
${NAME2} help
|
|
120407
120484
|
|
|
120408
120485
|
Download a YouTube video transcript or show video details.
|
|
120409
120486
|
|
|
120410
120487
|
Commands:
|
|
120488
|
+
summarize Pipe transcript to the AI command (default command)
|
|
120489
|
+
read Download and print a richly formatted transcript
|
|
120411
120490
|
info Show video metadata (title, channel, duration, etc.)
|
|
120412
|
-
summarize Pipe transcript to the command configured in [summarize]
|
|
120413
120491
|
config Open config file in $EDITOR
|
|
120492
|
+
help Show this help message
|
|
120414
120493
|
|
|
120415
120494
|
Arguments:
|
|
120416
120495
|
video-url-or-id YouTube URL (full or short) or bare video ID
|
|
@@ -120420,14 +120499,18 @@ Options:
|
|
|
120420
120499
|
-t, --timestamps Include timestamps [MM:SS] in output.
|
|
120421
120500
|
--list List available transcript languages and exit.
|
|
120422
120501
|
-o, --output <path> Write output to file instead of stdout.
|
|
120423
|
-
--
|
|
120424
|
-
--
|
|
120502
|
+
--format <type> Output format: markdown (default), text, or json.
|
|
120503
|
+
--json Shortcut for --format json.
|
|
120504
|
+
--text Shortcut for --format text.
|
|
120505
|
+
--markdown Shortcut for --format markdown.
|
|
120425
120506
|
--details Prepend video details to transcript (default, text only).
|
|
120426
120507
|
--no-details Suppress video details, transcript only.
|
|
120427
120508
|
--no-decode-entities Preserve HTML entities (decoded by default).
|
|
120428
120509
|
--reset-config Reset config file to defaults and exit.
|
|
120429
120510
|
--no-cache Skip cache and overwrite cache files.
|
|
120430
120511
|
--no-format Skip prettier formatting.
|
|
120512
|
+
--dry-run Print what would be sent to the AI command and exit.
|
|
120513
|
+
--extract-timestamps Extract chapter timestamps from video description.
|
|
120431
120514
|
--no-pager Disable pager for stdout output.
|
|
120432
120515
|
--pager Use pager for stdout output (default).
|
|
120433
120516
|
--no-hints Disable hints.
|
|
@@ -120537,7 +120620,7 @@ async function outputText(text) {
|
|
|
120537
120620
|
}
|
|
120538
120621
|
var pagerCmd = null;
|
|
120539
120622
|
var args = process.argv.slice(2);
|
|
120540
|
-
if (args.length === 0 || args.includes("--help")) {
|
|
120623
|
+
if (args.length === 0 || args.includes("--help") || args.includes("help")) {
|
|
120541
120624
|
console.log(help());
|
|
120542
120625
|
exitProcess(0);
|
|
120543
120626
|
}
|
|
@@ -120551,8 +120634,8 @@ if (args.includes("--reset-config")) {
|
|
|
120551
120634
|
exitProcess(0);
|
|
120552
120635
|
}
|
|
120553
120636
|
var config = await loadConfig().catch(() => ({}));
|
|
120554
|
-
var mode = "
|
|
120555
|
-
var subcmdIndex = args.findIndex((a5) => a5 === "info" || a5 === "summarize" || a5 === "config");
|
|
120637
|
+
var mode = "summarize";
|
|
120638
|
+
var subcmdIndex = args.findIndex((a5) => a5 === "info" || a5 === "summarize" || a5 === "config" || a5 === "read");
|
|
120556
120639
|
if (subcmdIndex !== -1) {
|
|
120557
120640
|
mode = args[subcmdIndex];
|
|
120558
120641
|
args.splice(subcmdIndex, 1);
|
|
@@ -120563,12 +120646,15 @@ var timestamps = false;
|
|
|
120563
120646
|
var listOnly = false;
|
|
120564
120647
|
var outputPath;
|
|
120565
120648
|
var outputJson = false;
|
|
120649
|
+
var format3 = "markdown";
|
|
120566
120650
|
var noDecode = false;
|
|
120567
120651
|
var showDetails = true;
|
|
120568
120652
|
var noCache = false;
|
|
120569
120653
|
var noFormat = false;
|
|
120570
120654
|
var usePager = true;
|
|
120571
120655
|
var useHints = true;
|
|
120656
|
+
var dryRun = false;
|
|
120657
|
+
var extractTimestamps = false;
|
|
120572
120658
|
var logLevel = "normal";
|
|
120573
120659
|
for (let i = 0;i < args.length; i++) {
|
|
120574
120660
|
const arg = args[i];
|
|
@@ -120592,8 +120678,27 @@ for (let i = 0;i < args.length; i++) {
|
|
|
120592
120678
|
}
|
|
120593
120679
|
} else if (arg === "--json") {
|
|
120594
120680
|
outputJson = true;
|
|
120681
|
+
format3 = "json";
|
|
120595
120682
|
} else if (arg === "--text") {
|
|
120596
120683
|
outputJson = false;
|
|
120684
|
+
format3 = "text";
|
|
120685
|
+
} else if (arg === "--markdown") {
|
|
120686
|
+
format3 = "markdown";
|
|
120687
|
+
} else if (arg === "--format") {
|
|
120688
|
+
const val = args[++i];
|
|
120689
|
+
if (val === "json") {
|
|
120690
|
+
format3 = "json";
|
|
120691
|
+
outputJson = true;
|
|
120692
|
+
} else if (val === "text") {
|
|
120693
|
+
format3 = "text";
|
|
120694
|
+
outputJson = false;
|
|
120695
|
+
} else if (val === "markdown") {
|
|
120696
|
+
format3 = "markdown";
|
|
120697
|
+
outputJson = false;
|
|
120698
|
+
} else {
|
|
120699
|
+
console.error("Error: --format must be text, json, or markdown");
|
|
120700
|
+
exitProcess(1);
|
|
120701
|
+
}
|
|
120597
120702
|
} else if (arg === "--details") {
|
|
120598
120703
|
showDetails = true;
|
|
120599
120704
|
} else if (arg === "--no-details") {
|
|
@@ -120616,6 +120721,10 @@ for (let i = 0;i < args.length; i++) {
|
|
|
120616
120721
|
logLevel = "verbose";
|
|
120617
120722
|
} else if (arg === "--no-decode-entities") {
|
|
120618
120723
|
noDecode = true;
|
|
120724
|
+
} else if (arg === "--dry-run") {
|
|
120725
|
+
dryRun = true;
|
|
120726
|
+
} else if (arg === "--extract-timestamps") {
|
|
120727
|
+
extractTimestamps = true;
|
|
120619
120728
|
} else if (arg.startsWith("-")) {
|
|
120620
120729
|
console.error(`Unknown option: ${arg}`);
|
|
120621
120730
|
exitProcess(1);
|
|
@@ -120672,6 +120781,21 @@ debug("Mode:", mode);
|
|
|
120672
120781
|
if (lang)
|
|
120673
120782
|
debug("Language:", lang);
|
|
120674
120783
|
await Promise.race([versionCheck, new Promise((r5) => setTimeout(r5, 1000))]);
|
|
120784
|
+
if (extractTimestamps) {
|
|
120785
|
+
startTimer();
|
|
120786
|
+
info("Fetching transcript...");
|
|
120787
|
+
const result = await fetchTranscript(videoId, {
|
|
120788
|
+
videoDetails: true,
|
|
120789
|
+
lang
|
|
120790
|
+
});
|
|
120791
|
+
info("Transcript fetched");
|
|
120792
|
+
const chapters = extractChapters(result.videoDetails.description);
|
|
120793
|
+
const output = outputJson ? JSON.stringify(chapters, null, 2) + `
|
|
120794
|
+
` : formatChaptersAsText(chapters) + `
|
|
120795
|
+
`;
|
|
120796
|
+
await outputText(output);
|
|
120797
|
+
exitProcess(0);
|
|
120798
|
+
}
|
|
120675
120799
|
try {
|
|
120676
120800
|
if (mode === "info") {
|
|
120677
120801
|
const result = await fetchTranscript(videoId, { videoDetails: true, lang });
|
|
@@ -120683,8 +120807,9 @@ try {
|
|
|
120683
120807
|
exitProcess(0);
|
|
120684
120808
|
}
|
|
120685
120809
|
if (mode === "summarize") {
|
|
120686
|
-
|
|
120687
|
-
|
|
120810
|
+
const sumCmd = resolveSummarizeCmd(config);
|
|
120811
|
+
if (!sumCmd) {
|
|
120812
|
+
console.error("Error: no command configured for summarize. Set [ai].command or [summarize].command in config.");
|
|
120688
120813
|
exitProcess(1);
|
|
120689
120814
|
}
|
|
120690
120815
|
const cacheOpts2 = { lang, mode: "summarize", noDecode };
|
|
@@ -120713,13 +120838,24 @@ try {
|
|
|
120713
120838
|
await writeCache(dir2, "transcript.json", JSON.stringify(segments2));
|
|
120714
120839
|
debug("Cache written: transcript.json");
|
|
120715
120840
|
}
|
|
120716
|
-
const
|
|
120841
|
+
const prompt2 = resolveSummarizePrompt(config) ?? "";
|
|
120842
|
+
if (!prompt2) {
|
|
120843
|
+
console.error("Error: no prompt configured. Set a prompt in the [summarize] section of your config.");
|
|
120844
|
+
exitProcess(1);
|
|
120845
|
+
}
|
|
120717
120846
|
const transcriptText = toText(segments2, !noDecode);
|
|
120847
|
+
if (dryRun) {
|
|
120848
|
+
await outputText(`${prompt2}
|
|
120849
|
+
|
|
120850
|
+
${transcriptText}
|
|
120851
|
+
`);
|
|
120852
|
+
exitProcess(0);
|
|
120853
|
+
}
|
|
120718
120854
|
if (!summary) {
|
|
120719
120855
|
info(`Summarizing...`);
|
|
120720
120856
|
summary = await summarize({
|
|
120721
|
-
prompt,
|
|
120722
|
-
command:
|
|
120857
|
+
prompt: prompt2,
|
|
120858
|
+
command: sumCmd,
|
|
120723
120859
|
transcript: transcriptText,
|
|
120724
120860
|
cwd: dir2
|
|
120725
120861
|
});
|
|
@@ -120736,6 +120872,140 @@ try {
|
|
|
120736
120872
|
printLanguages(languages2);
|
|
120737
120873
|
exitProcess(0);
|
|
120738
120874
|
}
|
|
120875
|
+
if (format3 === "markdown") {
|
|
120876
|
+
const transcribeCmd = resolveTranscribeCmd(config);
|
|
120877
|
+
if (!transcribeCmd) {
|
|
120878
|
+
console.error("Error: no command configured for transcribe. Set [transcribe].command, [ai].command, or [summarize].command in config.");
|
|
120879
|
+
exitProcess(1);
|
|
120880
|
+
}
|
|
120881
|
+
const cacheOpts2 = { lang, mode: "transcribe", noDecode };
|
|
120882
|
+
const dir2 = cacheDir(videoId, cacheOpts2);
|
|
120883
|
+
let segments2 = null;
|
|
120884
|
+
let md = null;
|
|
120885
|
+
startTimer();
|
|
120886
|
+
let cachedInfo = null;
|
|
120887
|
+
if (!noCache) {
|
|
120888
|
+
const cachedSegments = await readCache(dir2, "transcript.json");
|
|
120889
|
+
const cachedMd = await readCache(dir2, "transcript.md");
|
|
120890
|
+
cachedInfo = await readCache(dir2, "info.json");
|
|
120891
|
+
if (cachedSegments && cachedMd) {
|
|
120892
|
+
info("Transcript cached");
|
|
120893
|
+
debug("Cache hit:", dir2);
|
|
120894
|
+
segments2 = JSON.parse(cachedSegments);
|
|
120895
|
+
md = cachedMd;
|
|
120896
|
+
} else {
|
|
120897
|
+
debug("Cache miss:", dir2);
|
|
120898
|
+
}
|
|
120899
|
+
} else {
|
|
120900
|
+
debug("Cache skipped (--no-cache)");
|
|
120901
|
+
}
|
|
120902
|
+
const prompt2 = resolveTranscribePrompt(config) ?? "";
|
|
120903
|
+
if (!prompt2) {
|
|
120904
|
+
console.error("Error: no prompt configured. Set a prompt in the [transcribe] or [summarize] section of your config.");
|
|
120905
|
+
exitProcess(1);
|
|
120906
|
+
}
|
|
120907
|
+
if (!segments2) {
|
|
120908
|
+
info("Fetching transcript...");
|
|
120909
|
+
const opts = lang ? { lang, videoDetails: true } : { videoDetails: true };
|
|
120910
|
+
const result = await fetchTranscript(videoId, opts);
|
|
120911
|
+
segments2 = result.segments;
|
|
120912
|
+
const infoJson = JSON.stringify({
|
|
120913
|
+
title: result.videoDetails.title,
|
|
120914
|
+
channel: result.videoDetails.author,
|
|
120915
|
+
description: result.videoDetails.description
|
|
120916
|
+
});
|
|
120917
|
+
cachedInfo = infoJson;
|
|
120918
|
+
const chapterValue = formatChaptersAsJson(extractChapters(result.videoDetails.description));
|
|
120919
|
+
const truncatedInfo = JSON.stringify({
|
|
120920
|
+
title: result.videoDetails.title,
|
|
120921
|
+
channel: result.videoDetails.author,
|
|
120922
|
+
description: result.videoDetails.description.slice(0, 1000)
|
|
120923
|
+
});
|
|
120924
|
+
const transcriptText = toText(segments2, !noDecode);
|
|
120925
|
+
const structuredContent = `INFO:
|
|
120926
|
+
${truncatedInfo}
|
|
120927
|
+
|
|
120928
|
+
TIMESTAMPS:
|
|
120929
|
+
${chapterValue}
|
|
120930
|
+
|
|
120931
|
+
TEXT:
|
|
120932
|
+
${transcriptText}`;
|
|
120933
|
+
if (dryRun) {
|
|
120934
|
+
await outputText(`${prompt2}
|
|
120935
|
+
|
|
120936
|
+
${structuredContent}
|
|
120937
|
+
`);
|
|
120938
|
+
exitProcess(0);
|
|
120939
|
+
}
|
|
120940
|
+
info(`Transcript: ${segments2.length} segments`);
|
|
120941
|
+
await writeCache(dir2, "transcript.json", JSON.stringify(segments2));
|
|
120942
|
+
await writeCache(dir2, "info.json", infoJson);
|
|
120943
|
+
await writeCache(dir2, "chapters.json", chapterValue);
|
|
120944
|
+
debug("Cache written: transcript.json, info.json, chapters.json");
|
|
120945
|
+
if (!md) {
|
|
120946
|
+
info(`Transcribing...`);
|
|
120947
|
+
md = await summarize({
|
|
120948
|
+
prompt: prompt2,
|
|
120949
|
+
command: transcribeCmd,
|
|
120950
|
+
transcript: structuredContent,
|
|
120951
|
+
cwd: dir2
|
|
120952
|
+
});
|
|
120953
|
+
info("Transcription ready");
|
|
120954
|
+
await writeCache(dir2, "transcript.md", md);
|
|
120955
|
+
debug("Cache written: transcript.md");
|
|
120956
|
+
}
|
|
120957
|
+
} else {
|
|
120958
|
+
let chapterValue;
|
|
120959
|
+
if (!cachedInfo) {
|
|
120960
|
+
debug("Cache missing info.json, re-fetching video details");
|
|
120961
|
+
const fallbackOpts = lang ? { lang, videoDetails: true } : { videoDetails: true };
|
|
120962
|
+
const fallbackResult = await fetchTranscript(videoId, fallbackOpts);
|
|
120963
|
+
cachedInfo = JSON.stringify({
|
|
120964
|
+
title: fallbackResult.videoDetails.title,
|
|
120965
|
+
channel: fallbackResult.videoDetails.author,
|
|
120966
|
+
description: fallbackResult.videoDetails.description
|
|
120967
|
+
});
|
|
120968
|
+
await writeCache(dir2, "info.json", cachedInfo);
|
|
120969
|
+
chapterValue = formatChaptersAsJson(extractChapters(fallbackResult.videoDetails.description));
|
|
120970
|
+
await writeCache(dir2, "chapters.json", chapterValue);
|
|
120971
|
+
debug("Cache written: info.json, chapters.json");
|
|
120972
|
+
} else {
|
|
120973
|
+
const cachedChapters = await readCache(dir2, "chapters.json");
|
|
120974
|
+
chapterValue = cachedChapters ?? "not available";
|
|
120975
|
+
}
|
|
120976
|
+
const transcriptText = toText(segments2, !noDecode);
|
|
120977
|
+
const cachedInfoObj = JSON.parse(cachedInfo);
|
|
120978
|
+
const truncatedInfo = JSON.stringify({
|
|
120979
|
+
title: cachedInfoObj.title,
|
|
120980
|
+
channel: cachedInfoObj.channel,
|
|
120981
|
+
description: cachedInfoObj.description.slice(0, 1000)
|
|
120982
|
+
});
|
|
120983
|
+
const structuredContent = `INFO:
|
|
120984
|
+
${truncatedInfo}
|
|
120985
|
+
|
|
120986
|
+
TIMESTAMPS:
|
|
120987
|
+
${chapterValue}
|
|
120988
|
+
|
|
120989
|
+
TEXT:
|
|
120990
|
+
${transcriptText}`;
|
|
120991
|
+
if (!md) {
|
|
120992
|
+
info(`Transcribing...`);
|
|
120993
|
+
md = await summarize({
|
|
120994
|
+
prompt: prompt2,
|
|
120995
|
+
command: transcribeCmd,
|
|
120996
|
+
transcript: structuredContent,
|
|
120997
|
+
cwd: dir2
|
|
120998
|
+
});
|
|
120999
|
+
info("Transcription ready");
|
|
121000
|
+
await writeCache(dir2, "transcript.md", md);
|
|
121001
|
+
debug("Cache written: transcript.md");
|
|
121002
|
+
}
|
|
121003
|
+
}
|
|
121004
|
+
const formatted = noFormat ? md : await formatMd(md);
|
|
121005
|
+
await outputText(formatted + `
|
|
121006
|
+
`);
|
|
121007
|
+
exitProcess(0);
|
|
121008
|
+
}
|
|
120739
121009
|
const decode = !noDecode;
|
|
120740
121010
|
const cacheOpts = { lang, timestamps, json: outputJson, noDecode };
|
|
120741
121011
|
const dir = cacheDir(videoId, cacheOpts);
|
|
@@ -120754,6 +121024,11 @@ try {
|
|
|
120754
121024
|
} else {
|
|
120755
121025
|
debug("Cache skipped (--no-cache)");
|
|
120756
121026
|
}
|
|
121027
|
+
const prompt = resolveTranscribePrompt(config) ?? "";
|
|
121028
|
+
if (!prompt) {
|
|
121029
|
+
console.error("Error: no prompt configured. Set a prompt in the [transcribe] or [summarize] section of your config.");
|
|
121030
|
+
exitProcess(1);
|
|
121031
|
+
}
|
|
120757
121032
|
if (!segments) {
|
|
120758
121033
|
info("Fetching transcript...");
|
|
120759
121034
|
if (showDetails && !outputJson) {
|