@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 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** downloads YouTube transcripts to be printed or saved to a file.
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
- You can read, skim, search, copy, and manipulate the text using the tools you love the most.
20
+ Prosey can also output or store the native json format verbatim.
6
21
 
7
22
  ## Features
8
23
 
9
- | Feature | Description |
10
- | ------------------------------------ | ----------------------------------------------------------------------------------------------- |
11
- | **📋&nbsp;Transcript&nbsp;download** | Fetch YouTube transcripts as plain text or JSON |
12
- | **🤖&nbsp;AI&nbsp;summarization** | Pipe transcripts to any AI agent (opencode, Claude, Copilot, Codex) |
13
- | **🎨&nbsp;Smart&nbsp;pager** | Automatic syntax highlighting if bat, glow or mdcat are installed (or configure your own pager) |
14
- | **🛠️&nbsp;Customization** | Use `prosey config` to access and edit your configuration |
15
- | **💾&nbsp;Disk&nbsp;caching** | Transcripts and summaries cached for instant repeat |
16
- | **📄&nbsp;Supported&nbsp;formats** | Plain text and JSON |
24
+ | Feature | Description |
25
+ | ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
26
+ | **💡&nbsp;&nbsp;Summarize** | Fetch and summarize a video transcription |
27
+ | **📖&nbsp;&nbsp;Read** | Fetch and display the transcript formatted in Markdown |
28
+ | **📋&nbsp;&nbsp;Transcript&nbsp;download** | Fetch YouTube transcripts as plain text or JSON verbatim |
29
+ | **🤖&nbsp;&nbsp;AI&nbsp;summarization** | Pipe transcripts to any AI agent (opencode, Claude, Copilot, Codex) |
30
+ | **🎨&nbsp;&nbsp;Smart&nbsp;pager** | Automatic syntax highlighting if bat, glow or mdcat are installed (or configure your own pager) |
31
+ | **🛠️&nbsp;&nbsp;Customization** | Use `prosey config` to access and edit your configuration / customize the prompts |
32
+ | **💾&nbsp;&nbsp;Disk&nbsp;caching** | Transcripts and summaries are cached on disk for instant re-read |
33
+ | **📄&nbsp;&nbsp;Supported&nbsp;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
- Pass a full YouTube URL or a bare video ID. The transcript is printed to
73
- stdout by default, with video details prepended.
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
- # Basic plain text with details
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. `&#39;`). 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 to execute with the prompt and transcript piped via stdin.
101623
- # The transcript is appended to the prompt automatically.
101624
- #
101625
- # Available options:
101626
- #
101627
- # opencode run — full access (default)
101628
- # opencode run --permissions read — read-only (view files, no edits)
101629
- #
101630
- # claude -p "" --print — full access (--print for clean output)
101631
- # claude --permission-mode plan -p "" --print — read-only (plan/read only)
101632
- #
101633
- # copilot -sp "" — full access (-s = silent, -p = prompt)
101634
- # copilot -sp "" --deny-all-tools read-only (no shell/write access)
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
- function executeCommand(command, input, cwd) {
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 executeCommand(command, fullPrompt, cwd);
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.2.6",
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
- --json Output as JSON (suppresses details).
120424
- --text Output as plain text (default).
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 = "transcript";
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
- if (!config.summarize?.command) {
120687
- console.error("Error: [summarize] section with a command is required in config");
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 prompt = config.summarize.prompt ?? "";
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: config.summarize.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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacone/prosey",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Download YouTube video transcripts from the CLI",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",