@tacone/prosey 0.6.0 → 0.7.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/bin/prosey CHANGED
@@ -101600,46 +101600,6 @@ var lt = (r) => {
101600
101600
  };
101601
101601
 
101602
101602
  // src/config.ts
101603
- var FALLBACK_CONFIG_TOML = `# Default prosey configuration
101604
- # Created automatically on first run. Edit as needed.
101605
-
101606
- # Pager command for transcript and summary output.
101607
- # Defaults to "auto": bat -lmd --style plain → glow -p → mdcat -l -p → less
101608
- # Set to a custom command (e.g. "less -R") to override.
101609
- # Can also be set via the PROSEY_PAGER env var (takes precedence).
101610
- pager = "auto"
101611
-
101612
- # Show hints for missing tools (e.g. markdown highlighter).
101613
- # Can also be set via PROSEY_HINTS env var (yes, no, 1, 0, true, false).
101614
- hints = true
101615
-
101616
- [ai]
101617
- # Default command for AI operations (summarize, transcribe).
101618
- # Can be overridden per-section via the command key below.
101619
- command = "opencode run"
101620
-
101621
- [summarize]
101622
- # Prompt sent to the command via stdin.
101623
- # Customize this to change how transcripts are summarized.
101624
- prompt = """
101625
- Write a comprehensive summary of the following transcription.
101626
- """
101627
-
101628
- # Command override for summarize. Uncomment to use a different command
101629
- # than the one specified in [ai].
101630
- # command = "opencode run"
101631
-
101632
- [transcribe]
101633
- # Prompt sent to the command via stdin.
101634
- # Customize this to change how transcripts are formatted as markdown.
101635
- prompt = """
101636
- Convert this transcript to clean, readable markdown.
101637
- """
101638
-
101639
- # Command override for transcribe. Uncomment to use a different command
101640
- # than the one specified in [ai].
101641
- # command = "opencode run"
101642
- `;
101643
101603
  async function readDefaultConfig() {
101644
101604
  const paths = [
101645
101605
  join(dirname(fileURLToPath(import.meta.url)), "default-config.toml"),
@@ -101649,7 +101609,7 @@ async function readDefaultConfig() {
101649
101609
  if (existsSync(p2))
101650
101610
  return readFile(p2, "utf8");
101651
101611
  }
101652
- return FALLBACK_CONFIG_TOML;
101612
+ throw new Error("default-config.toml not found");
101653
101613
  }
101654
101614
  function configDir() {
101655
101615
  const env = process.env.XDG_CONFIG_HOME;
@@ -103188,14 +103148,23 @@ var LOGO_DATA_URI = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(LOGO_
103188
103148
  function escapeHtml(text) {
103189
103149
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
103190
103150
  }
103191
- async function generateHtml(markdown, title) {
103151
+ function watchMetaHtml(duration, wordCount, videoId) {
103152
+ const h2 = Math.floor(duration / 3600);
103153
+ const m3 = Math.round(duration % 3600 / 60);
103154
+ const durStr = h2 > 0 ? `${h2} h ${m3} min` : `${m3} min`;
103155
+ const readTime = Math.ceil(wordCount / 200);
103156
+ const url = `https://youtube.com/watch?v=${videoId}`;
103157
+ const separator = '<span style="opacity: 0.4">&nbsp;&nbsp;|&nbsp;&nbsp;</span>';
103158
+ return `<a href="${url}" class="watch-link">${durStr} watch</a> ${separator} ${readTime} min read`;
103159
+ }
103160
+ async function generateHtml(markdown, title, watchMeta) {
103192
103161
  const [css, body] = await Promise.all([getPicoCss(), g2.parse(markdown)]);
103193
103162
  return `<!DOCTYPE html>
103194
103163
  <html lang="en">
103195
103164
  <head>
103196
103165
  <meta charset="UTF-8">
103197
103166
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
103198
- <title>${escapeHtml(title ?? "Prosey")}</title>
103167
+ <title>${title ? escapeHtml(title) + " - Prosey" : "Prosey"}</title>
103199
103168
  <script>(function(){var m=localStorage.getItem('prosey-theme'),t=m||'auto';if(t==='auto')t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';document.documentElement.setAttribute('data-theme',t);if(!m)localStorage.setItem('prosey-theme','auto')})();</script>
103200
103169
  <link rel="preconnect" href="https://fonts.googleapis.com">
103201
103170
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -103249,14 +103218,17 @@ blockquote:last-child { margin-bottom: 0; }
103249
103218
  #theme-btn:hover, #theme-btn:focus, #theme-btn:active { opacity: 1 !important; }
103250
103219
  img[alt="Prosey"] { filter: grayscale(100%); }
103251
103220
  img[alt="Prosey"]:hover, img[alt="Prosey"]:active, img[alt="Prosey"]:focus { filter: grayscale(0%); }
103221
+ .watch-link { color: inherit; text-decoration: none; }
103222
+ .watch-link:hover { text-decoration: underline; }
103252
103223
  </style>
103253
103224
  </head>
103254
103225
  <body>
103255
- <div style="display:flex;justify-content:space-between;align-items:flex-start;padding:1rem 1rem 0">
103256
- <img src="${LOGO_DATA_URI}" alt="Prosey" title="Prosey" style="vertical-align:top">
103257
- <button id="theme-btn" type="button" style="background:none;border:none;cursor:pointer;padding:0;line-height:1;opacity:.5;filter:grayscale(100%);transition:all 0.3s">\uD83D\uDCA1</button>
103226
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem 0">
103227
+ <img src="${LOGO_DATA_URI}" alt="Prosey" title="Prosey" style="vertical-align:top;flex-shrink:0">
103228
+ <div style="flex:1;text-align:center;font-size:.85rem;color:var(--pico-muted-color)">${watchMeta ? watchMetaHtml(watchMeta.duration, watchMeta.wordCount, watchMeta.videoId) : ""}</div>
103229
+ <button id="theme-btn" type="button" style="background:none;border:none;cursor:pointer;padding:0;line-height:1;opacity:.5;filter:grayscale(100%);transition:all 0.3s;flex-shrink:0">\uD83D\uDCA1</button>
103258
103230
  </div>
103259
- <main style="max-width:720px;margin:0 auto;padding:1rem">
103231
+ <main style="max-width:50em;margin:0 auto;padding:1rem">
103260
103232
  ${body}
103261
103233
  </main>
103262
103234
  <script>
@@ -103291,7 +103263,7 @@ function openInBrowser(htmlPath) {
103291
103263
  // package.json
103292
103264
  var package_default = {
103293
103265
  name: "@tacone/prosey",
103294
- version: "0.6.0",
103266
+ version: "0.7.0",
103295
103267
  description: "Download YouTube video transcripts from the CLI",
103296
103268
  module: "src/index.ts",
103297
103269
  type: "module",
@@ -122307,18 +122279,23 @@ try {
122307
122279
  exitProcess(1);
122308
122280
  }
122309
122281
  let structuredContent;
122282
+ let videoTitle;
122283
+ let videoDuration = 0;
122310
122284
  if (!segments2) {
122311
122285
  info("Fetching transcript...");
122312
122286
  const opts = lang ? { lang, videoDetails: true } : { videoDetails: true };
122313
122287
  const result = await fetchTranscript(videoId, opts);
122314
122288
  segments2 = result.segments;
122289
+ videoDuration = result.videoDetails.lengthSeconds;
122315
122290
  const infoJson = JSON.stringify({
122316
122291
  title: result.videoDetails.title,
122317
122292
  channel: result.videoDetails.author,
122318
- description: result.videoDetails.description
122293
+ description: result.videoDetails.description,
122294
+ duration: videoDuration
122319
122295
  });
122320
122296
  cachedInfo = infoJson;
122321
122297
  const chapterValue = formatChaptersAsJson(extractChapters(result.videoDetails.description));
122298
+ videoTitle = result.videoDetails.title;
122322
122299
  const truncatedInfo = JSON.stringify({
122323
122300
  title: result.videoDetails.title,
122324
122301
  channel: result.videoDetails.author,
@@ -122348,10 +122325,12 @@ ${structuredContent}
122348
122325
  debug("Cache missing info.json, re-fetching video details");
122349
122326
  const fallbackOpts = lang ? { lang, videoDetails: true } : { videoDetails: true };
122350
122327
  const fallbackResult = await fetchTranscript(videoId, fallbackOpts);
122328
+ videoDuration = fallbackResult.videoDetails.lengthSeconds;
122351
122329
  cachedInfo = JSON.stringify({
122352
122330
  title: fallbackResult.videoDetails.title,
122353
122331
  channel: fallbackResult.videoDetails.author,
122354
- description: fallbackResult.videoDetails.description
122332
+ description: fallbackResult.videoDetails.description,
122333
+ duration: videoDuration
122355
122334
  });
122356
122335
  await writeCache(dir2, "info.json", cachedInfo);
122357
122336
  chapterValue = formatChaptersAsJson(extractChapters(fallbackResult.videoDetails.description));
@@ -122363,6 +122342,8 @@ ${structuredContent}
122363
122342
  }
122364
122343
  const transcriptText = toText(segments2, !noDecode);
122365
122344
  const cachedInfoObj = JSON.parse(cachedInfo);
122345
+ videoTitle = cachedInfoObj.title;
122346
+ videoDuration = cachedInfoObj.duration ?? 0;
122366
122347
  const truncatedInfo = JSON.stringify({
122367
122348
  title: cachedInfoObj.title,
122368
122349
  channel: cachedInfoObj.channel,
@@ -122395,7 +122376,12 @@ ${structuredContent}
122395
122376
  }
122396
122377
  const formatted = noFormat ? summary : await formatMd(summary);
122397
122378
  if (format3 === "html") {
122398
- const htmlContent = await generateHtml(formatted);
122379
+ const wordCount = summary.split(/\s+/).filter(Boolean).length;
122380
+ const htmlContent = await generateHtml(formatted, videoTitle, {
122381
+ videoId,
122382
+ duration: videoDuration,
122383
+ wordCount
122384
+ });
122399
122385
  const htmlPath = join5(dir2, "summary.html");
122400
122386
  await writeFile3(htmlPath, htmlContent, "utf8");
122401
122387
  debug("HTML written:", htmlPath);
@@ -122426,6 +122412,8 @@ ${structuredContent}
122426
122412
  const dir2 = cacheDir(videoId, cacheOpts2);
122427
122413
  let segments2 = null;
122428
122414
  let md = null;
122415
+ let videoTitle;
122416
+ let videoDuration = 0;
122429
122417
  startTimer();
122430
122418
  let cachedInfo = null;
122431
122419
  if (!noCache) {
@@ -122453,12 +122441,15 @@ ${structuredContent}
122453
122441
  const opts = lang ? { lang, videoDetails: true } : { videoDetails: true };
122454
122442
  const result = await fetchTranscript(videoId, opts);
122455
122443
  segments2 = result.segments;
122444
+ videoDuration = result.videoDetails.lengthSeconds;
122456
122445
  const infoJson = JSON.stringify({
122457
122446
  title: result.videoDetails.title,
122458
122447
  channel: result.videoDetails.author,
122459
- description: result.videoDetails.description
122448
+ description: result.videoDetails.description,
122449
+ duration: videoDuration
122460
122450
  });
122461
122451
  cachedInfo = infoJson;
122452
+ videoTitle = result.videoDetails.title;
122462
122453
  const chapterValue = formatChaptersAsJson(extractChapters(result.videoDetails.description));
122463
122454
  const truncatedInfo = JSON.stringify({
122464
122455
  title: result.videoDetails.title,
@@ -122504,10 +122495,12 @@ ${structuredContent}
122504
122495
  debug("Cache missing info.json, re-fetching video details");
122505
122496
  const fallbackOpts = lang ? { lang, videoDetails: true } : { videoDetails: true };
122506
122497
  const fallbackResult = await fetchTranscript(videoId, fallbackOpts);
122498
+ videoDuration = fallbackResult.videoDetails.lengthSeconds;
122507
122499
  cachedInfo = JSON.stringify({
122508
122500
  title: fallbackResult.videoDetails.title,
122509
122501
  channel: fallbackResult.videoDetails.author,
122510
- description: fallbackResult.videoDetails.description
122502
+ description: fallbackResult.videoDetails.description,
122503
+ duration: videoDuration
122511
122504
  });
122512
122505
  await writeCache(dir2, "info.json", cachedInfo);
122513
122506
  chapterValue = formatChaptersAsJson(extractChapters(fallbackResult.videoDetails.description));
@@ -122519,6 +122512,8 @@ ${structuredContent}
122519
122512
  }
122520
122513
  const transcriptText = toText(segments2, !noDecode);
122521
122514
  const cachedInfoObj = JSON.parse(cachedInfo);
122515
+ videoTitle = cachedInfoObj.title;
122516
+ videoDuration = cachedInfoObj.duration ?? 0;
122522
122517
  const truncatedInfo = JSON.stringify({
122523
122518
  title: cachedInfoObj.title,
122524
122519
  channel: cachedInfoObj.channel,
@@ -122547,7 +122542,12 @@ ${transcriptText}`;
122547
122542
  }
122548
122543
  const formatted = noFormat ? md : await formatMd(md);
122549
122544
  if (format3 === "html") {
122550
- const htmlContent = await generateHtml(formatted);
122545
+ const wordCount = md.split(/\s+/).filter(Boolean).length;
122546
+ const htmlContent = await generateHtml(formatted, videoTitle, {
122547
+ videoId,
122548
+ duration: videoDuration,
122549
+ wordCount
122550
+ });
122551
122551
  const htmlPath = join5(dir2, "transcript.html");
122552
122552
  await writeFile3(htmlPath, htmlContent, "utf8");
122553
122553
  debug("HTML written:", htmlPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacone/prosey",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Download YouTube video transcripts from the CLI",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/config.ts CHANGED
@@ -21,47 +21,6 @@ export interface ProseyConfig {
21
21
  };
22
22
  }
23
23
 
24
- const FALLBACK_CONFIG_TOML = `# Default prosey configuration
25
- # Created automatically on first run. Edit as needed.
26
-
27
- # Pager command for transcript and summary output.
28
- # Defaults to "auto": bat -lmd --style plain → glow -p → mdcat -l -p → less
29
- # Set to a custom command (e.g. "less -R") to override.
30
- # Can also be set via the PROSEY_PAGER env var (takes precedence).
31
- pager = "auto"
32
-
33
- # Show hints for missing tools (e.g. markdown highlighter).
34
- # Can also be set via PROSEY_HINTS env var (yes, no, 1, 0, true, false).
35
- hints = true
36
-
37
- [ai]
38
- # Default command for AI operations (summarize, transcribe).
39
- # Can be overridden per-section via the command key below.
40
- command = "opencode run"
41
-
42
- [summarize]
43
- # Prompt sent to the command via stdin.
44
- # Customize this to change how transcripts are summarized.
45
- prompt = """
46
- Write a comprehensive summary of the following transcription.
47
- """
48
-
49
- # Command override for summarize. Uncomment to use a different command
50
- # than the one specified in [ai].
51
- # command = "opencode run"
52
-
53
- [transcribe]
54
- # Prompt sent to the command via stdin.
55
- # Customize this to change how transcripts are formatted as markdown.
56
- prompt = """
57
- Convert this transcript to clean, readable markdown.
58
- """
59
-
60
- # Command override for transcribe. Uncomment to use a different command
61
- # than the one specified in [ai].
62
- # command = "opencode run"
63
- `;
64
-
65
24
  async function readDefaultConfig(): Promise<string> {
66
25
  const paths = [
67
26
  join(dirname(fileURLToPath(import.meta.url)), "default-config.toml"),
@@ -70,7 +29,7 @@ async function readDefaultConfig(): Promise<string> {
70
29
  for (const p of paths) {
71
30
  if (existsSync(p)) return readFile(p, "utf8");
72
31
  }
73
- return FALLBACK_CONFIG_TOML;
32
+ throw new Error("default-config.toml not found");
74
33
  }
75
34
 
76
35
  function configDir(): string {
@@ -37,6 +37,7 @@ Guidelines:
37
37
  - use the TIMESTAMPS only to understand the structure, don't output time markers
38
38
  - content length: aim to be the same range as the original text or more.
39
39
  - section titles should be h2 headers
40
+ - paragraph length soft-limit. 250 characters
40
41
 
41
42
  Content considerations:
42
43
 
package/src/format.ts CHANGED
@@ -24,6 +24,13 @@ export function formatDuration(seconds: number): string {
24
24
  return `${m}:${String(s).padStart(2, "0")}`;
25
25
  }
26
26
 
27
+ export function formatReadableDuration(seconds: number): string {
28
+ const h = Math.floor(seconds / 3600);
29
+ const m = Math.round((seconds % 3600) / 60);
30
+ if (h > 0) return `${h}h ${m}min`;
31
+ return `${m}min`;
32
+ }
33
+
27
34
  export function decodeEntities(text: string): string {
28
35
  return text
29
36
  .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
package/src/html.test.ts CHANGED
@@ -5,7 +5,7 @@ describe("generateHtml", () => {
5
5
  test("wraps markdown in HTML with PicoCSS", async () => {
6
6
  const html = await generateHtml("# Hello\n\nWorld", "Test");
7
7
  expect(html).toStartWith("<!DOCTYPE html>");
8
- expect(html).toContain("<title>Test</title>");
8
+ expect(html).toContain("<title>Test - Prosey</title>");
9
9
  expect(html).toContain("<style>");
10
10
  expect(html).toContain("--pico-");
11
11
  expect(html).toContain("<h1>Hello</h1>");
package/src/html.ts CHANGED
@@ -34,7 +34,21 @@ function escapeHtml(text: string): string {
34
34
  .replace(/"/g, "&quot;");
35
35
  }
36
36
 
37
- export async function generateHtml(markdown: string, title?: string): Promise<string> {
37
+ function watchMetaHtml(duration: number, wordCount: number, videoId: string): string {
38
+ const h = Math.floor(duration / 3600);
39
+ const m = Math.round((duration % 3600) / 60);
40
+ const durStr = h > 0 ? `${h} h ${m} min` : `${m} min`;
41
+ const readTime = Math.ceil(wordCount / 200);
42
+ const url = `https://youtube.com/watch?v=${videoId}`;
43
+ const separator = '<span style="opacity: 0.4">&nbsp;&nbsp;|&nbsp;&nbsp;</span>';
44
+ return `<a href="${url}" class="watch-link">${durStr} watch</a> ${separator} ${readTime} min read`;
45
+ }
46
+
47
+ export async function generateHtml(
48
+ markdown: string,
49
+ title?: string,
50
+ watchMeta?: { videoId: string; duration: number; wordCount: number },
51
+ ): Promise<string> {
38
52
  const [css, body] = await Promise.all([getPicoCss(), marked.parse(markdown)]);
39
53
 
40
54
  return `<!DOCTYPE html>
@@ -42,7 +56,7 @@ export async function generateHtml(markdown: string, title?: string): Promise<st
42
56
  <head>
43
57
  <meta charset="UTF-8">
44
58
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
45
- <title>${escapeHtml(title ?? "Prosey")}</title>
59
+ <title>${title ? escapeHtml(title) + " - Prosey" : "Prosey"}</title>
46
60
  <script>(function(){var m=localStorage.getItem('prosey-theme'),t=m||'auto';if(t==='auto')t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';document.documentElement.setAttribute('data-theme',t);if(!m)localStorage.setItem('prosey-theme','auto')})();</script>
47
61
  <link rel="preconnect" href="https://fonts.googleapis.com">
48
62
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -96,14 +110,17 @@ blockquote:last-child { margin-bottom: 0; }
96
110
  #theme-btn:hover, #theme-btn:focus, #theme-btn:active { opacity: 1 !important; }
97
111
  img[alt="Prosey"] { filter: grayscale(100%); }
98
112
  img[alt="Prosey"]:hover, img[alt="Prosey"]:active, img[alt="Prosey"]:focus { filter: grayscale(0%); }
113
+ .watch-link { color: inherit; text-decoration: none; }
114
+ .watch-link:hover { text-decoration: underline; }
99
115
  </style>
100
116
  </head>
101
117
  <body>
102
- <div style="display:flex;justify-content:space-between;align-items:flex-start;padding:1rem 1rem 0">
103
- <img src="${LOGO_DATA_URI}" alt="Prosey" title="Prosey" style="vertical-align:top">
104
- <button id="theme-btn" type="button" style="background:none;border:none;cursor:pointer;padding:0;line-height:1;opacity:.5;filter:grayscale(100%);transition:all 0.3s">💡</button>
118
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem 0">
119
+ <img src="${LOGO_DATA_URI}" alt="Prosey" title="Prosey" style="vertical-align:top;flex-shrink:0">
120
+ <div style="flex:1;text-align:center;font-size:.85rem;color:var(--pico-muted-color)">${watchMeta ? watchMetaHtml(watchMeta.duration, watchMeta.wordCount, watchMeta.videoId) : ""}</div>
121
+ <button id="theme-btn" type="button" style="background:none;border:none;cursor:pointer;padding:0;line-height:1;opacity:.5;filter:grayscale(100%);transition:all 0.3s;flex-shrink:0">💡</button>
105
122
  </div>
106
- <main style="max-width:720px;margin:0 auto;padding:1rem">
123
+ <main style="max-width:50em;margin:0 auto;padding:1rem">
107
124
  ${body}
108
125
  </main>
109
126
  <script>
package/src/index.ts CHANGED
@@ -472,6 +472,8 @@ try {
472
472
  }
473
473
 
474
474
  let structuredContent: string;
475
+ let videoTitle: string | undefined;
476
+ let videoDuration = 0;
475
477
 
476
478
  if (!segments) {
477
479
  info("Fetching transcript...");
@@ -481,13 +483,16 @@ try {
481
483
  segments: TranscriptSegment[];
482
484
  };
483
485
  segments = result.segments;
486
+ videoDuration = result.videoDetails.lengthSeconds;
484
487
  const infoJson = JSON.stringify({
485
488
  title: result.videoDetails.title,
486
489
  channel: result.videoDetails.author,
487
490
  description: result.videoDetails.description,
491
+ duration: videoDuration,
488
492
  });
489
493
  cachedInfo = infoJson;
490
494
  const chapterValue = formatChaptersAsJson(extractChapters(result.videoDetails.description));
495
+ videoTitle = result.videoDetails.title;
491
496
  const truncatedInfo = JSON.stringify({
492
497
  title: result.videoDetails.title,
493
498
  channel: result.videoDetails.author,
@@ -517,10 +522,12 @@ try {
517
522
  videoDetails: VideoDetails;
518
523
  segments: TranscriptSegment[];
519
524
  };
525
+ videoDuration = fallbackResult.videoDetails.lengthSeconds;
520
526
  cachedInfo = JSON.stringify({
521
527
  title: fallbackResult.videoDetails.title,
522
528
  channel: fallbackResult.videoDetails.author,
523
529
  description: fallbackResult.videoDetails.description,
530
+ duration: videoDuration,
524
531
  });
525
532
  await writeCache(dir, "info.json", cachedInfo);
526
533
  chapterValue = formatChaptersAsJson(
@@ -534,6 +541,8 @@ try {
534
541
  }
535
542
  const transcriptText = toText(segments, !noDecode);
536
543
  const cachedInfoObj = JSON.parse(cachedInfo);
544
+ videoTitle = cachedInfoObj.title;
545
+ videoDuration = cachedInfoObj.duration ?? 0;
537
546
  const truncatedInfo = JSON.stringify({
538
547
  title: cachedInfoObj.title,
539
548
  channel: cachedInfoObj.channel,
@@ -562,7 +571,12 @@ try {
562
571
 
563
572
  const formatted = noFormat ? summary : await formatMd(summary);
564
573
  if (format === "html") {
565
- const htmlContent = await generateHtml(formatted);
574
+ const wordCount = summary!.split(/\s+/).filter(Boolean).length;
575
+ const htmlContent = await generateHtml(formatted, videoTitle, {
576
+ videoId,
577
+ duration: videoDuration,
578
+ wordCount,
579
+ });
566
580
  const htmlPath = join(dir, "summary.html");
567
581
  await writeFile(htmlPath, htmlContent, "utf8");
568
582
  debug("HTML written:", htmlPath);
@@ -596,6 +610,8 @@ try {
596
610
  const dir = cacheDir(videoId, cacheOpts);
597
611
  let segments: TranscriptSegment[] | null = null;
598
612
  let md: string | null = null;
613
+ let videoTitle: string | undefined;
614
+ let videoDuration = 0;
599
615
 
600
616
  startTimer();
601
617
 
@@ -633,12 +649,15 @@ try {
633
649
  segments: TranscriptSegment[];
634
650
  };
635
651
  segments = result.segments;
652
+ videoDuration = result.videoDetails.lengthSeconds;
636
653
  const infoJson = JSON.stringify({
637
654
  title: result.videoDetails.title,
638
655
  channel: result.videoDetails.author,
639
656
  description: result.videoDetails.description,
657
+ duration: videoDuration,
640
658
  });
641
659
  cachedInfo = infoJson;
660
+ videoTitle = result.videoDetails.title;
642
661
  const chapterValue = formatChaptersAsJson(extractChapters(result.videoDetails.description));
643
662
  const truncatedInfo = JSON.stringify({
644
663
  title: result.videoDetails.title,
@@ -682,10 +701,12 @@ try {
682
701
  videoDetails: VideoDetails;
683
702
  segments: TranscriptSegment[];
684
703
  };
704
+ videoDuration = fallbackResult.videoDetails.lengthSeconds;
685
705
  cachedInfo = JSON.stringify({
686
706
  title: fallbackResult.videoDetails.title,
687
707
  channel: fallbackResult.videoDetails.author,
688
708
  description: fallbackResult.videoDetails.description,
709
+ duration: videoDuration,
689
710
  });
690
711
  await writeCache(dir, "info.json", cachedInfo);
691
712
  chapterValue = formatChaptersAsJson(
@@ -699,6 +720,8 @@ try {
699
720
  }
700
721
  const transcriptText = toText(segments, !noDecode);
701
722
  const cachedInfoObj = JSON.parse(cachedInfo);
723
+ videoTitle = cachedInfoObj.title;
724
+ videoDuration = cachedInfoObj.duration ?? 0;
702
725
  const truncatedInfo = JSON.stringify({
703
726
  title: cachedInfoObj.title,
704
727
  channel: cachedInfoObj.channel,
@@ -722,7 +745,12 @@ try {
722
745
 
723
746
  const formatted = noFormat ? md : await formatMd(md);
724
747
  if (format === "html") {
725
- const htmlContent = await generateHtml(formatted);
748
+ const wordCount = md!.split(/\s+/).filter(Boolean).length;
749
+ const htmlContent = await generateHtml(formatted, videoTitle, {
750
+ videoId,
751
+ duration: videoDuration,
752
+ wordCount,
753
+ });
726
754
  const htmlPath = join(dir, "transcript.html");
727
755
  await writeFile(htmlPath, htmlContent, "utf8");
728
756
  debug("HTML written:", htmlPath);