@tacone/prosey 0.2.4 → 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 +52 -26
- package/bin/prosey +363 -51
- 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 +27 -18
- package/src/default-config.toml +41 -19
- 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 +275 -26
- package/src/pager.test.ts +1 -1
- package/src/pager.ts +2 -2
- package/src/summarize.test.ts +74 -3
- package/src/summarize.ts +18 -4
- package/src/version-check.ts +32 -0
package/bin/prosey
CHANGED
|
@@ -94210,9 +94210,9 @@ function detectPager(cfgPager) {
|
|
|
94210
94210
|
if (cfgPager !== undefined && cfgPager !== "" && cfgPager !== "auto")
|
|
94211
94211
|
return cfgPager;
|
|
94212
94212
|
if (hasCommand("bat"))
|
|
94213
|
-
return "bat -lmd";
|
|
94213
|
+
return "bat -lmd --style plain";
|
|
94214
94214
|
if (hasCommand("glow"))
|
|
94215
|
-
return "glow";
|
|
94215
|
+
return "glow -p";
|
|
94216
94216
|
if (hasCommand("mdcat"))
|
|
94217
94217
|
return "mdcat -l -p";
|
|
94218
94218
|
if (hasCommand("less"))
|
|
@@ -101603,7 +101603,7 @@ var FALLBACK_CONFIG_TOML = `# Default prosey configuration
|
|
|
101603
101603
|
# Created automatically on first run. Edit as needed.
|
|
101604
101604
|
|
|
101605
101605
|
# Pager command for transcript and summary output.
|
|
101606
|
-
# Defaults to "auto": bat -lmd → glow → mdcat -l -p → less
|
|
101606
|
+
# Defaults to "auto": bat -lmd --style plain → glow -p → mdcat -l -p → less
|
|
101607
101607
|
# Set to a custom command (e.g. "less -R") to override.
|
|
101608
101608
|
# Can also be set via the PROSEY_PAGER env var (takes precedence).
|
|
101609
101609
|
pager = "auto"
|
|
@@ -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",
|
|
@@ -101822,6 +101893,31 @@ var package_default = {
|
|
|
101822
101893
|
}
|
|
101823
101894
|
};
|
|
101824
101895
|
|
|
101896
|
+
// src/version-check.ts
|
|
101897
|
+
var TIMEOUT_MS = 3000;
|
|
101898
|
+
var registryUrl = `https://registry.npmjs.org/${package_default.name}/latest`;
|
|
101899
|
+
async function checkVersion() {
|
|
101900
|
+
try {
|
|
101901
|
+
const controller = new AbortController;
|
|
101902
|
+
const timer2 = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
101903
|
+
const res = await fetch(registryUrl, { signal: controller.signal });
|
|
101904
|
+
clearTimeout(timer2);
|
|
101905
|
+
if (!res.ok) {
|
|
101906
|
+
debug("Version check failed: HTTP", res.status);
|
|
101907
|
+
return null;
|
|
101908
|
+
}
|
|
101909
|
+
const data = await res.json();
|
|
101910
|
+
if (!data.version) {
|
|
101911
|
+
debug("Version check: no version field in response");
|
|
101912
|
+
return null;
|
|
101913
|
+
}
|
|
101914
|
+
return data.version;
|
|
101915
|
+
} catch (err) {
|
|
101916
|
+
debug("Version check error:", err instanceof Error ? err.message : String(err));
|
|
101917
|
+
return null;
|
|
101918
|
+
}
|
|
101919
|
+
}
|
|
101920
|
+
|
|
101825
101921
|
// node_modules/prettier/index.mjs
|
|
101826
101922
|
import { createRequire as __prettierCreateRequire } from "module";
|
|
101827
101923
|
import { fileURLToPath as __prettierFileUrlToPath } from "url";
|
|
@@ -120360,22 +120456,40 @@ var debugApis = {
|
|
|
120360
120456
|
};
|
|
120361
120457
|
|
|
120362
120458
|
// src/index.ts
|
|
120459
|
+
process.stdout.on("error", (err) => {
|
|
120460
|
+
if (err.code === "EPIPE")
|
|
120461
|
+
process.exit(0);
|
|
120462
|
+
});
|
|
120363
120463
|
var NAME2 = "prosey";
|
|
120364
120464
|
var VERSION2 = package_default.version;
|
|
120465
|
+
var latestVersion = null;
|
|
120466
|
+
var versionCheck = checkVersion().then((v11) => {
|
|
120467
|
+
latestVersion = v11;
|
|
120468
|
+
});
|
|
120469
|
+
function exitProcess(code) {
|
|
120470
|
+
if (useHints && code === 0 && latestVersion && latestVersion !== VERSION2) {
|
|
120471
|
+
hint(`\uD83D\uDCE6 New version available: ${latestVersion} — use npm/pnpm/bun -g i ${package_default.name} to upgrade`);
|
|
120472
|
+
}
|
|
120473
|
+
process.exit(code);
|
|
120474
|
+
}
|
|
120365
120475
|
function help() {
|
|
120366
120476
|
return `${NAME2} v${VERSION2}
|
|
120367
120477
|
|
|
120368
120478
|
Usage: ${NAME2} [options] <video-url-or-id>
|
|
120479
|
+
${NAME2} read [options] <video-url-or-id>
|
|
120369
120480
|
${NAME2} info [options] <video-url-or-id>
|
|
120370
120481
|
${NAME2} summarize [options] <video-url-or-id>
|
|
120371
120482
|
${NAME2} config
|
|
120483
|
+
${NAME2} help
|
|
120372
120484
|
|
|
120373
120485
|
Download a YouTube video transcript or show video details.
|
|
120374
120486
|
|
|
120375
120487
|
Commands:
|
|
120488
|
+
summarize Pipe transcript to the AI command (default command)
|
|
120489
|
+
read Download and print a richly formatted transcript
|
|
120376
120490
|
info Show video metadata (title, channel, duration, etc.)
|
|
120377
|
-
summarize Pipe transcript to the command configured in [summarize]
|
|
120378
120491
|
config Open config file in $EDITOR
|
|
120492
|
+
help Show this help message
|
|
120379
120493
|
|
|
120380
120494
|
Arguments:
|
|
120381
120495
|
video-url-or-id YouTube URL (full or short) or bare video ID
|
|
@@ -120385,14 +120499,18 @@ Options:
|
|
|
120385
120499
|
-t, --timestamps Include timestamps [MM:SS] in output.
|
|
120386
120500
|
--list List available transcript languages and exit.
|
|
120387
120501
|
-o, --output <path> Write output to file instead of stdout.
|
|
120388
|
-
--
|
|
120389
|
-
--
|
|
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.
|
|
120390
120506
|
--details Prepend video details to transcript (default, text only).
|
|
120391
120507
|
--no-details Suppress video details, transcript only.
|
|
120392
120508
|
--no-decode-entities Preserve HTML entities (decoded by default).
|
|
120393
120509
|
--reset-config Reset config file to defaults and exit.
|
|
120394
120510
|
--no-cache Skip cache and overwrite cache files.
|
|
120395
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.
|
|
120396
120514
|
--no-pager Disable pager for stdout output.
|
|
120397
120515
|
--pager Use pager for stdout output (default).
|
|
120398
120516
|
--no-hints Disable hints.
|
|
@@ -120502,22 +120620,22 @@ async function outputText(text) {
|
|
|
120502
120620
|
}
|
|
120503
120621
|
var pagerCmd = null;
|
|
120504
120622
|
var args = process.argv.slice(2);
|
|
120505
|
-
if (args.length === 0 || args.includes("--help")) {
|
|
120623
|
+
if (args.length === 0 || args.includes("--help") || args.includes("help")) {
|
|
120506
120624
|
console.log(help());
|
|
120507
|
-
|
|
120625
|
+
exitProcess(0);
|
|
120508
120626
|
}
|
|
120509
120627
|
if (args.includes("--version")) {
|
|
120510
120628
|
console.log(VERSION2);
|
|
120511
|
-
|
|
120629
|
+
exitProcess(0);
|
|
120512
120630
|
}
|
|
120513
120631
|
if (args.includes("--reset-config")) {
|
|
120514
120632
|
const path15 = await resetConfig();
|
|
120515
120633
|
console.log(`Config reset to defaults: ${path15}`);
|
|
120516
|
-
|
|
120634
|
+
exitProcess(0);
|
|
120517
120635
|
}
|
|
120518
120636
|
var config = await loadConfig().catch(() => ({}));
|
|
120519
|
-
var mode = "
|
|
120520
|
-
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");
|
|
120521
120639
|
if (subcmdIndex !== -1) {
|
|
120522
120640
|
mode = args[subcmdIndex];
|
|
120523
120641
|
args.splice(subcmdIndex, 1);
|
|
@@ -120528,12 +120646,15 @@ var timestamps = false;
|
|
|
120528
120646
|
var listOnly = false;
|
|
120529
120647
|
var outputPath;
|
|
120530
120648
|
var outputJson = false;
|
|
120649
|
+
var format3 = "markdown";
|
|
120531
120650
|
var noDecode = false;
|
|
120532
120651
|
var showDetails = true;
|
|
120533
120652
|
var noCache = false;
|
|
120534
120653
|
var noFormat = false;
|
|
120535
120654
|
var usePager = true;
|
|
120536
120655
|
var useHints = true;
|
|
120656
|
+
var dryRun = false;
|
|
120657
|
+
var extractTimestamps = false;
|
|
120537
120658
|
var logLevel = "normal";
|
|
120538
120659
|
for (let i = 0;i < args.length; i++) {
|
|
120539
120660
|
const arg = args[i];
|
|
@@ -120543,7 +120664,7 @@ for (let i = 0;i < args.length; i++) {
|
|
|
120543
120664
|
lang = args[++i] ?? undefined;
|
|
120544
120665
|
if (!lang) {
|
|
120545
120666
|
console.error("Error: --lang requires a language code");
|
|
120546
|
-
|
|
120667
|
+
exitProcess(1);
|
|
120547
120668
|
}
|
|
120548
120669
|
} else if (arg === "--timestamps" || arg === "-t") {
|
|
120549
120670
|
timestamps = true;
|
|
@@ -120553,12 +120674,31 @@ for (let i = 0;i < args.length; i++) {
|
|
|
120553
120674
|
outputPath = args[++i] ?? undefined;
|
|
120554
120675
|
if (!outputPath) {
|
|
120555
120676
|
console.error("Error: -o/--output requires a file path");
|
|
120556
|
-
|
|
120677
|
+
exitProcess(1);
|
|
120557
120678
|
}
|
|
120558
120679
|
} else if (arg === "--json") {
|
|
120559
120680
|
outputJson = true;
|
|
120681
|
+
format3 = "json";
|
|
120560
120682
|
} else if (arg === "--text") {
|
|
120561
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
|
+
}
|
|
120562
120702
|
} else if (arg === "--details") {
|
|
120563
120703
|
showDetails = true;
|
|
120564
120704
|
} else if (arg === "--no-details") {
|
|
@@ -120581,9 +120721,13 @@ for (let i = 0;i < args.length; i++) {
|
|
|
120581
120721
|
logLevel = "verbose";
|
|
120582
120722
|
} else if (arg === "--no-decode-entities") {
|
|
120583
120723
|
noDecode = true;
|
|
120724
|
+
} else if (arg === "--dry-run") {
|
|
120725
|
+
dryRun = true;
|
|
120726
|
+
} else if (arg === "--extract-timestamps") {
|
|
120727
|
+
extractTimestamps = true;
|
|
120584
120728
|
} else if (arg.startsWith("-")) {
|
|
120585
120729
|
console.error(`Unknown option: ${arg}`);
|
|
120586
|
-
|
|
120730
|
+
exitProcess(1);
|
|
120587
120731
|
} else {
|
|
120588
120732
|
videoId = arg;
|
|
120589
120733
|
}
|
|
@@ -120600,17 +120744,17 @@ if (mode === "config") {
|
|
|
120600
120744
|
} else {
|
|
120601
120745
|
console.log(`Config file: ${path15}`);
|
|
120602
120746
|
}
|
|
120603
|
-
|
|
120747
|
+
exitProcess(0);
|
|
120604
120748
|
}
|
|
120605
120749
|
if (!videoId) {
|
|
120606
120750
|
console.error("Error: missing video URL or ID");
|
|
120607
120751
|
console.log(help());
|
|
120608
|
-
|
|
120752
|
+
exitProcess(1);
|
|
120609
120753
|
}
|
|
120610
120754
|
var extracted = extractVideoId(videoId);
|
|
120611
120755
|
if (!extracted) {
|
|
120612
120756
|
console.error("Error: invalid YouTube video URL or ID");
|
|
120613
|
-
|
|
120757
|
+
exitProcess(65);
|
|
120614
120758
|
}
|
|
120615
120759
|
videoId = extracted;
|
|
120616
120760
|
setLevel(logLevel);
|
|
@@ -120626,7 +120770,7 @@ debug("Pager:", pagerCmd ?? "none");
|
|
|
120626
120770
|
}
|
|
120627
120771
|
}
|
|
120628
120772
|
if (useHints) {
|
|
120629
|
-
const hasMarkdownPager = pagerCmd === "bat -lmd" || pagerCmd === "glow" || pagerCmd === "mdcat -l -p";
|
|
120773
|
+
const hasMarkdownPager = pagerCmd === "bat -lmd --style plain" || pagerCmd === "glow -p" || pagerCmd === "mdcat -l -p";
|
|
120630
120774
|
if (!hasMarkdownPager) {
|
|
120631
120775
|
hint("Tip: install a markdown highlighter for better output (e.g. bat, glow, mdcat)");
|
|
120632
120776
|
}
|
|
@@ -120636,6 +120780,22 @@ debug("Video ID:", videoId);
|
|
|
120636
120780
|
debug("Mode:", mode);
|
|
120637
120781
|
if (lang)
|
|
120638
120782
|
debug("Language:", lang);
|
|
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
|
+
}
|
|
120639
120799
|
try {
|
|
120640
120800
|
if (mode === "info") {
|
|
120641
120801
|
const result = await fetchTranscript(videoId, { videoDetails: true, lang });
|
|
@@ -120644,12 +120804,13 @@ try {
|
|
|
120644
120804
|
} else {
|
|
120645
120805
|
printVideoInfo(result.videoDetails);
|
|
120646
120806
|
}
|
|
120647
|
-
|
|
120807
|
+
exitProcess(0);
|
|
120648
120808
|
}
|
|
120649
120809
|
if (mode === "summarize") {
|
|
120650
|
-
|
|
120651
|
-
|
|
120652
|
-
|
|
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.");
|
|
120813
|
+
exitProcess(1);
|
|
120653
120814
|
}
|
|
120654
120815
|
const cacheOpts2 = { lang, mode: "summarize", noDecode };
|
|
120655
120816
|
const dir2 = cacheDir(videoId, cacheOpts2);
|
|
@@ -120677,13 +120838,24 @@ try {
|
|
|
120677
120838
|
await writeCache(dir2, "transcript.json", JSON.stringify(segments2));
|
|
120678
120839
|
debug("Cache written: transcript.json");
|
|
120679
120840
|
}
|
|
120680
|
-
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
|
+
}
|
|
120681
120846
|
const transcriptText = toText(segments2, !noDecode);
|
|
120847
|
+
if (dryRun) {
|
|
120848
|
+
await outputText(`${prompt2}
|
|
120849
|
+
|
|
120850
|
+
${transcriptText}
|
|
120851
|
+
`);
|
|
120852
|
+
exitProcess(0);
|
|
120853
|
+
}
|
|
120682
120854
|
if (!summary) {
|
|
120683
120855
|
info(`Summarizing...`);
|
|
120684
120856
|
summary = await summarize({
|
|
120685
|
-
prompt,
|
|
120686
|
-
command:
|
|
120857
|
+
prompt: prompt2,
|
|
120858
|
+
command: sumCmd,
|
|
120687
120859
|
transcript: transcriptText,
|
|
120688
120860
|
cwd: dir2
|
|
120689
120861
|
});
|
|
@@ -120694,11 +120866,145 @@ try {
|
|
|
120694
120866
|
const formatted = noFormat ? summary : await formatMd(summary);
|
|
120695
120867
|
await outputText(formatted + `
|
|
120696
120868
|
`);
|
|
120697
|
-
|
|
120869
|
+
exitProcess(0);
|
|
120698
120870
|
} else if (listOnly) {
|
|
120699
120871
|
const languages2 = await listLanguages(videoId);
|
|
120700
120872
|
printLanguages(languages2);
|
|
120701
|
-
|
|
120873
|
+
exitProcess(0);
|
|
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);
|
|
120702
121008
|
}
|
|
120703
121009
|
const decode = !noDecode;
|
|
120704
121010
|
const cacheOpts = { lang, timestamps, json: outputJson, noDecode };
|
|
@@ -120718,6 +121024,11 @@ try {
|
|
|
120718
121024
|
} else {
|
|
120719
121025
|
debug("Cache skipped (--no-cache)");
|
|
120720
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
|
+
}
|
|
120721
121032
|
if (!segments) {
|
|
120722
121033
|
info("Fetching transcript...");
|
|
120723
121034
|
if (showDetails && !outputJson) {
|
|
@@ -120752,8 +121063,9 @@ try {
|
|
|
120752
121063
|
`;
|
|
120753
121064
|
await outputText(output);
|
|
120754
121065
|
}
|
|
121066
|
+
exitProcess(0);
|
|
120755
121067
|
} catch (err) {
|
|
120756
121068
|
const message = err instanceof Error ? err.message : String(err);
|
|
120757
121069
|
console.error(`Error: ${message}`);
|
|
120758
|
-
|
|
121070
|
+
exitProcess(1);
|
|
120759
121071
|
}
|