@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 +54 -54
- package/package.json +1 -1
- package/src/config.ts +1 -42
- package/src/default-config.toml +1 -0
- package/src/format.ts +7 -0
- package/src/html.test.ts +1 -1
- package/src/html.ts +23 -6
- package/src/index.ts +30 -2
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
103190
103150
|
}
|
|
103191
|
-
|
|
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"> | </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
|
|
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;
|
|
103256
|
-
<img src="${LOGO_DATA_URI}" alt="Prosey" title="Prosey" style="vertical-align:top">
|
|
103257
|
-
<
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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
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
|
-
|
|
32
|
+
throw new Error("default-config.toml not found");
|
|
74
33
|
}
|
|
75
34
|
|
|
76
35
|
function configDir(): string {
|
package/src/default-config.toml
CHANGED
|
@@ -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, """);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
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"> | </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
|
|
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;
|
|
103
|
-
<img src="${LOGO_DATA_URI}" alt="Prosey" title="Prosey" style="vertical-align:top">
|
|
104
|
-
<
|
|
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:
|
|
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
|
|
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
|
|
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);
|