atris 3.56.0 → 3.56.1
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/atris/skills/engines/SKILL.md +11 -3
- package/atris/skills/x-search/SKILL.md +20 -1
- package/atris/skills/youtube/SKILL.md +51 -43
- package/bin/atris.js +119 -87
- package/commands/autopilot-front.js +29 -13
- package/commands/brainstorm.js +77 -476
- package/commands/business.js +13 -1
- package/commands/fleet-report.js +2 -2
- package/commands/human-missions.js +26 -2
- package/commands/init.js +2 -35
- package/commands/integrations.js +100 -0
- package/commands/land.js +53 -23
- package/commands/later.js +52 -0
- package/commands/log.js +79 -30
- package/commands/mission.js +117 -22
- package/commands/next.js +153 -63
- package/commands/now.js +80 -38
- package/commands/recap.js +66 -4
- package/commands/run-front.js +20 -5
- package/commands/spaceship.js +52 -12
- package/commands/status.js +30 -7
- package/commands/task.js +56 -19
- package/commands/terminal.js +5 -5
- package/commands/workflow.js +18 -5
- package/commands/x-search.js +123 -13
- package/commands/youtube.js +639 -36
- package/lib/account-bound.js +7 -0
- package/lib/apply-gate.js +94 -0
- package/lib/context-gatherer.js +55 -8
- package/lib/engine-ask.js +16 -6
- package/lib/first-minute.js +552 -26
- package/lib/known-commands.js +1 -1
- package/lib/runner-command.js +5 -3
- package/lib/scratch-root.js +44 -0
- package/package.json +1 -1
- package/utils/auth.js +9 -0
package/commands/youtube.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { apiRequestJson } = require('../utils/api');
|
|
2
2
|
const { ensureBilledCommandAuth } = require('./auth');
|
|
3
|
+
const applyGate = require('../lib/apply-gate');
|
|
3
4
|
const { spawnSync } = require('child_process');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const os = require('os');
|
|
@@ -28,7 +29,9 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
28
29
|
output('');
|
|
29
30
|
output(`Usage: ${commandName} search "<query>" [--limit N] [--json]`);
|
|
30
31
|
output(` ${commandName} search --paid "<query>" [--limit N] [--json]`);
|
|
31
|
-
output(` ${commandName} notes <youtube-url> [youtube-url-or-playlist...] [engine]`);
|
|
32
|
+
output(` ${commandName} notes <youtube-url> [youtube-url-or-playlist...] [engine] [--save]`);
|
|
33
|
+
output(` ${commandName} teach <youtube-url> [--section N] [--save]`);
|
|
34
|
+
output(` ${commandName} unsave <url-or-id>`);
|
|
32
35
|
output(` ${commandName} process <youtube-url> [options]`);
|
|
33
36
|
output(` ${commandName} digest [--days N]`);
|
|
34
37
|
output(` ${commandName} watch add <channel-url-or-@handle>`);
|
|
@@ -39,8 +42,9 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
39
42
|
output('');
|
|
40
43
|
output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links');
|
|
41
44
|
output('search --paid = 5 credits, watch permalinks + titles from Atris');
|
|
42
|
-
output('notes = free local notes
|
|
43
|
-
output('
|
|
45
|
+
output('notes = free local notes to stdout; ephemeral unless --save');
|
|
46
|
+
output('teach = one chapter from local captions; ephemeral unless --save');
|
|
47
|
+
output('process = 5 credits cloud knowledge (needs a filled Apply)');
|
|
44
48
|
output('digest = one decision page from this week\'s video briefs');
|
|
45
49
|
output('watch = subscribed channels turn into briefs without a human');
|
|
46
50
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
@@ -49,6 +53,9 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
49
53
|
output('Options:');
|
|
50
54
|
output(' --limit <n> Max search results (default: 5)');
|
|
51
55
|
output(' --paid Bill 5 credits for watch permalinks (search only)');
|
|
56
|
+
output(' --save File brief, journal line, and apply stub (notes and teach)');
|
|
57
|
+
output(' --section <n> Chapter to teach, 1-based (teach only, default: 1)');
|
|
58
|
+
output(' --unsave Delete filed brief and apply stub (no paid calls)');
|
|
52
59
|
output(' --query, -q <text> Focus question for the analysis');
|
|
53
60
|
output(' --agent <id> Agent id to store knowledge against');
|
|
54
61
|
output(' --store Save as agent knowledge (requires --agent)');
|
|
@@ -64,6 +71,11 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
64
71
|
output(` ${commandName} search "MCP agents" --limit 10`);
|
|
65
72
|
output(` ${commandName} search --paid "MCP agents 2026"`);
|
|
66
73
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
74
|
+
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID --save`);
|
|
75
|
+
output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID"`);
|
|
76
|
+
output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID" --section 2`);
|
|
77
|
+
output(` ${commandName} notes --unsave VIDEO_ID`);
|
|
78
|
+
output(` ${commandName} unsave VIDEO_ID`);
|
|
67
79
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID https://youtu.be/OTHER_ID`);
|
|
68
80
|
output(` ${commandName} notes https://www.youtube.com/playlist?list=PLAYLIST_ID`);
|
|
69
81
|
output(` ${commandName} https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
@@ -300,14 +312,14 @@ function fetchCaptionText(urlString, redirects = 0) {
|
|
|
300
312
|
});
|
|
301
313
|
}
|
|
302
314
|
|
|
303
|
-
function
|
|
315
|
+
function parseCaptionCues(raw) {
|
|
304
316
|
const trimmed = String(raw || '').trimStart();
|
|
305
|
-
if (!trimmed) return
|
|
317
|
+
if (!trimmed) return [];
|
|
306
318
|
|
|
307
319
|
if (trimmed.startsWith('{')) {
|
|
308
320
|
try {
|
|
309
321
|
const payload = JSON.parse(trimmed);
|
|
310
|
-
const
|
|
322
|
+
const cues = [];
|
|
311
323
|
for (const event of payload.events || []) {
|
|
312
324
|
const text = (event.segs || [])
|
|
313
325
|
.map((piece) => piece.utf8 || '')
|
|
@@ -315,18 +327,21 @@ function parseCaptionText(raw) {
|
|
|
315
327
|
.replace(/\s+/g, ' ')
|
|
316
328
|
.trim();
|
|
317
329
|
if (!text) continue;
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
330
|
+
const startMs = Number(event.tStartMs);
|
|
331
|
+
const cue = { startMs: Number.isFinite(startMs) ? startMs : 0, text };
|
|
332
|
+
if (cues.length && cues[cues.length - 1].text === cue.text && cues[cues.length - 1].startMs === cue.startMs) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
cues.push(cue);
|
|
321
336
|
}
|
|
322
|
-
return
|
|
337
|
+
return cues;
|
|
323
338
|
} catch {
|
|
324
|
-
return
|
|
339
|
+
return [];
|
|
325
340
|
}
|
|
326
341
|
}
|
|
327
342
|
|
|
328
343
|
if (/^WEBVTT/i.test(trimmed) || trimmed.includes('-->')) {
|
|
329
|
-
const
|
|
344
|
+
const cues = [];
|
|
330
345
|
for (const block of String(raw).split(/\r?\n\r?\n+/)) {
|
|
331
346
|
const lines = block.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
332
347
|
const timeLine = lines.find((line) => line.includes('-->'));
|
|
@@ -339,7 +354,28 @@ function parseCaptionText(raw) {
|
|
|
339
354
|
.filter((line, index, all) => index === 0 || line !== all[index - 1])
|
|
340
355
|
.join(' ')
|
|
341
356
|
.trim();
|
|
342
|
-
|
|
357
|
+
if (!text) continue;
|
|
358
|
+
const cue = { startMs: startMs == null ? 0 : startMs, text };
|
|
359
|
+
if (cues.length && cues[cues.length - 1].text === cue.text && cues[cues.length - 1].startMs === cue.startMs) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
cues.push(cue);
|
|
363
|
+
}
|
|
364
|
+
return cues;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function parseCaptionText(raw) {
|
|
371
|
+
const trimmed = String(raw || '').trimStart();
|
|
372
|
+
if (!trimmed) return '';
|
|
373
|
+
|
|
374
|
+
const cues = parseCaptionCues(raw);
|
|
375
|
+
if (cues.length) {
|
|
376
|
+
const segments = [];
|
|
377
|
+
for (const cue of cues) {
|
|
378
|
+
const captionLine = timestampedCaptionLine(cue.text, cue.startMs);
|
|
343
379
|
if (!captionLine || segments[segments.length - 1] === captionLine) continue;
|
|
344
380
|
segments.push(captionLine);
|
|
345
381
|
}
|
|
@@ -393,6 +429,19 @@ async function extractLocalTranscript(youtubeUrl, deps = {}) {
|
|
|
393
429
|
}
|
|
394
430
|
|
|
395
431
|
async function processYoutube(options, deps = {}) {
|
|
432
|
+
const applyStatus = (deps.ensureProcessApply || ensureProcessApply)({
|
|
433
|
+
cwd: deps.cwd || process.cwd(),
|
|
434
|
+
url: options.youtubeUrl,
|
|
435
|
+
now: deps.now,
|
|
436
|
+
output: deps.output,
|
|
437
|
+
});
|
|
438
|
+
if (applyStatus !== 0) {
|
|
439
|
+
const err = new Error(PROCESS_APPLY_MESSAGE);
|
|
440
|
+
err.exitCode = applyStatus;
|
|
441
|
+
err.applyRequired = true;
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
|
|
396
445
|
const apiFn = deps.apiRequestJson || apiRequestJson;
|
|
397
446
|
const ensureBilled = deps.ensureBilledCommandAuth || ensureBilledCommandAuth;
|
|
398
447
|
let auth = await ensureBilled('youtube', deps);
|
|
@@ -490,6 +539,14 @@ function videoIdFromUrl(url) {
|
|
|
490
539
|
return short ? short[1] : null;
|
|
491
540
|
}
|
|
492
541
|
|
|
542
|
+
function videoIdFromArg(arg) {
|
|
543
|
+
const fromUrl = videoIdFromUrl(arg);
|
|
544
|
+
if (fromUrl) return fromUrl;
|
|
545
|
+
const text = String(arg || '').trim();
|
|
546
|
+
if (/^[A-Za-z0-9_-]{6,}$/.test(text)) return text;
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
493
550
|
function looksLikeYoutubeUrl(arg) {
|
|
494
551
|
const text = String(arg || '').trim();
|
|
495
552
|
if (!text || text.startsWith('-')) return false;
|
|
@@ -505,17 +562,24 @@ function parseNotesArgs(argv = []) {
|
|
|
505
562
|
const urls = [];
|
|
506
563
|
let engine = null;
|
|
507
564
|
let help = false;
|
|
565
|
+
let save = false;
|
|
566
|
+
let unsave = false;
|
|
508
567
|
for (const raw of argv) {
|
|
509
568
|
const arg = String(raw);
|
|
510
|
-
if (arg === '--help' || arg === '-h' || arg === 'help')
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
569
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') help = true;
|
|
570
|
+
else if (arg === '--save') save = true;
|
|
571
|
+
else if (arg === '--unsave') unsave = true;
|
|
572
|
+
}
|
|
573
|
+
for (const raw of argv) {
|
|
574
|
+
const arg = String(raw);
|
|
575
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') continue;
|
|
576
|
+
if (arg === '--save' || arg === '--unsave') continue;
|
|
514
577
|
if (arg.startsWith('-')) continue;
|
|
515
578
|
if (looksLikeYoutubeUrl(arg)) urls.push(arg);
|
|
579
|
+
else if (unsave && videoIdFromArg(arg)) urls.push(arg);
|
|
516
580
|
else engine = arg;
|
|
517
581
|
}
|
|
518
|
-
return { urls, engine, help };
|
|
582
|
+
return { urls, engine, help, save, unsave };
|
|
519
583
|
}
|
|
520
584
|
|
|
521
585
|
function dateStamp(now) {
|
|
@@ -572,6 +636,94 @@ function fileBriefFromNotes({ cwd, url, workDir, now } = {}) {
|
|
|
572
636
|
}
|
|
573
637
|
}
|
|
574
638
|
|
|
639
|
+
const APPLY_NEXT_MESSAGE =
|
|
640
|
+
'next: write one apply (change + receipt) before process.';
|
|
641
|
+
const PROCESS_APPLY_MESSAGE =
|
|
642
|
+
'write one apply (change + receipt) before process.';
|
|
643
|
+
|
|
644
|
+
function applySidecarRel(id) {
|
|
645
|
+
return applyGate.applySidecarRel('youtube', id);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function ensureNotesApply({ cwd, url, now, output } = {}) {
|
|
649
|
+
const id = videoIdFromUrl(url);
|
|
650
|
+
return applyGate.ensureApply({
|
|
651
|
+
cwd,
|
|
652
|
+
source: url,
|
|
653
|
+
rel: id ? applySidecarRel(id) : null,
|
|
654
|
+
now,
|
|
655
|
+
output,
|
|
656
|
+
incompleteMessage: APPLY_NEXT_MESSAGE,
|
|
657
|
+
required: false,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function youtubeBriefRel(id) {
|
|
662
|
+
return `atris/wiki/briefs/youtube-${id}.md`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function unsaveYoutubeNotes(target, deps = {}) {
|
|
666
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
667
|
+
const cwd = deps.cwd || process.cwd();
|
|
668
|
+
const id = videoIdFromArg(target);
|
|
669
|
+
if (!id) {
|
|
670
|
+
output('usage: atris youtube unsave <url-or-id>');
|
|
671
|
+
return 2;
|
|
672
|
+
}
|
|
673
|
+
const briefRel = youtubeBriefRel(id);
|
|
674
|
+
const applyRel = applySidecarRel(id);
|
|
675
|
+
const removed = [];
|
|
676
|
+
for (const rel of [briefRel, applyRel]) {
|
|
677
|
+
const abs = path.join(cwd, rel);
|
|
678
|
+
try {
|
|
679
|
+
if (fs.existsSync(abs)) {
|
|
680
|
+
fs.unlinkSync(abs);
|
|
681
|
+
removed.push(rel);
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
// already gone or unreadable: do not error
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (!removed.length) {
|
|
688
|
+
output(`already gone: ${briefRel} and ${applyRel}`);
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
output(`removed ${removed.join(' and ')}`);
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function runYoutubeUnsave(args = [], deps = {}) {
|
|
696
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
697
|
+
const parsed = parseNotesArgs(['--unsave', ...args]);
|
|
698
|
+
if (parsed.help) {
|
|
699
|
+
showYoutubeHelp(output, deps.commandName || 'atris youtube');
|
|
700
|
+
return 0;
|
|
701
|
+
}
|
|
702
|
+
if (!parsed.urls.length) {
|
|
703
|
+
output('usage: atris youtube unsave <url-or-id>');
|
|
704
|
+
return 2;
|
|
705
|
+
}
|
|
706
|
+
let code = 0;
|
|
707
|
+
for (const target of parsed.urls) {
|
|
708
|
+
const status = unsaveYoutubeNotes(target, deps);
|
|
709
|
+
if (status !== 0) code = status;
|
|
710
|
+
}
|
|
711
|
+
return code;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function ensureProcessApply({ cwd, url, now, output } = {}) {
|
|
715
|
+
const id = videoIdFromUrl(url);
|
|
716
|
+
return applyGate.ensureApply({
|
|
717
|
+
cwd,
|
|
718
|
+
source: url,
|
|
719
|
+
rel: id ? applySidecarRel(id) : null,
|
|
720
|
+
now,
|
|
721
|
+
output,
|
|
722
|
+
incompleteMessage: PROCESS_APPLY_MESSAGE,
|
|
723
|
+
required: true,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
575
727
|
const DIGEST_ENGINE_TIMEOUT_MS = 240000;
|
|
576
728
|
const DEFAULT_DIGEST_DAYS = 7;
|
|
577
729
|
|
|
@@ -1187,7 +1339,7 @@ function runOneNotesItem(item, engine, deps = {}) {
|
|
|
1187
1339
|
} catch {
|
|
1188
1340
|
status = 1;
|
|
1189
1341
|
}
|
|
1190
|
-
const brief = status === 0 ? fileNotesBrief(item.url, deps) : null;
|
|
1342
|
+
const brief = status === 0 && deps.save ? fileNotesBrief(item.url, deps) : null;
|
|
1191
1343
|
const seconds = Math.max(0, Math.round((readNowMs(deps) - started) / 1000));
|
|
1192
1344
|
const ok = status === 0;
|
|
1193
1345
|
output(`${label} ${seconds}s ${ok ? (brief || 'ok') : 'FAILED'}`);
|
|
@@ -1203,7 +1355,8 @@ function formatNotesSummary(rows = []) {
|
|
|
1203
1355
|
return lines.join('\n');
|
|
1204
1356
|
}
|
|
1205
1357
|
|
|
1206
|
-
function runYoutubeNotesBatch({ urls, engine } = {}, deps = {}) {
|
|
1358
|
+
function runYoutubeNotesBatch({ urls, engine, save } = {}, deps = {}) {
|
|
1359
|
+
deps = { ...deps, save: save === true || deps.save === true };
|
|
1207
1360
|
const output = deps.output || ((line = '') => console.error(line));
|
|
1208
1361
|
const items = expandNotesTargets(urls || [], deps);
|
|
1209
1362
|
const rows = [];
|
|
@@ -1226,8 +1379,16 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1226
1379
|
return 1;
|
|
1227
1380
|
}
|
|
1228
1381
|
const status = readRunnerStatus(result);
|
|
1229
|
-
if (status
|
|
1230
|
-
return
|
|
1382
|
+
if (status !== 0) return status;
|
|
1383
|
+
if (!deps.save) return 0;
|
|
1384
|
+
fileNotesBrief(url, deps);
|
|
1385
|
+
const ensureApply = deps.ensureApply || ensureNotesApply;
|
|
1386
|
+
return ensureApply({
|
|
1387
|
+
cwd: deps.cwd || process.cwd(),
|
|
1388
|
+
url,
|
|
1389
|
+
now: deps.now,
|
|
1390
|
+
output: deps.output,
|
|
1391
|
+
});
|
|
1231
1392
|
}
|
|
1232
1393
|
|
|
1233
1394
|
function runYoutubeNotes(args = [], deps = {}) {
|
|
@@ -1237,15 +1398,19 @@ function runYoutubeNotes(args = [], deps = {}) {
|
|
|
1237
1398
|
showYoutubeHelp(output, deps.commandName || 'atris youtube');
|
|
1238
1399
|
return 0;
|
|
1239
1400
|
}
|
|
1401
|
+
if (parsed.unsave) {
|
|
1402
|
+
return runYoutubeUnsave(args, deps);
|
|
1403
|
+
}
|
|
1240
1404
|
if (!parsed.urls.length) {
|
|
1241
1405
|
output(YTNOTES_USAGE);
|
|
1242
1406
|
output(YTNOTES_HINT);
|
|
1243
1407
|
return 2;
|
|
1244
1408
|
}
|
|
1409
|
+
const nextDeps = { ...deps, save: parsed.save };
|
|
1245
1410
|
if (parsed.urls.length === 1 && !isPlaylistUrl(parsed.urls[0])) {
|
|
1246
|
-
return runSingleYoutubeNotes(parsed.urls[0], parsed.engine,
|
|
1411
|
+
return runSingleYoutubeNotes(parsed.urls[0], parsed.engine, nextDeps);
|
|
1247
1412
|
}
|
|
1248
|
-
return runYoutubeNotesBatch(parsed,
|
|
1413
|
+
return runYoutubeNotesBatch(parsed, nextDeps);
|
|
1249
1414
|
}
|
|
1250
1415
|
|
|
1251
1416
|
const DEFAULT_SEARCH_LIMIT = 5;
|
|
@@ -1261,6 +1426,8 @@ const LOCAL_SEARCH_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
|
1261
1426
|
const LOCAL_SEARCH_CACHE_FILE = 'youtube-search-cache.json';
|
|
1262
1427
|
const LOCAL_SEARCH_CACHE_NOTE =
|
|
1263
1428
|
'cached because youtube rate-limited local search.';
|
|
1429
|
+
const PAID_SEARCH_FRESH_CACHE_REFUSE =
|
|
1430
|
+
'free cache still has results for this query. drop --paid or wait until the cache expires.';
|
|
1264
1431
|
|
|
1265
1432
|
function showYoutubeSearchHelp(output = console.log, commandName = 'atris youtube') {
|
|
1266
1433
|
output('');
|
|
@@ -1489,24 +1656,50 @@ function paidSearchVideos(data) {
|
|
|
1489
1656
|
return rows;
|
|
1490
1657
|
}
|
|
1491
1658
|
|
|
1492
|
-
function
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1659
|
+
function paidSearchCredits(data) {
|
|
1660
|
+
if (!data || typeof data !== 'object') {
|
|
1661
|
+
return { used: undefined, remaining: undefined, refunded: undefined };
|
|
1662
|
+
}
|
|
1663
|
+
const payload = unwrapSearchPayload(data) || {};
|
|
1664
|
+
const used = data.credits_used !== undefined ? data.credits_used : payload.credits_used;
|
|
1665
|
+
const remaining = data.credits_remaining !== undefined
|
|
1496
1666
|
? data.credits_remaining
|
|
1497
1667
|
: payload.credits_remaining;
|
|
1498
|
-
|
|
1499
|
-
|
|
1668
|
+
let refunded = data.credits_refunded !== undefined
|
|
1669
|
+
? data.credits_refunded
|
|
1670
|
+
: payload.credits_refunded;
|
|
1671
|
+
if (refunded === undefined && (data.refunded === true || payload.refunded === true)) {
|
|
1672
|
+
refunded = true;
|
|
1673
|
+
}
|
|
1674
|
+
return { used, remaining, refunded };
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function creditsWereRefunded(credits) {
|
|
1678
|
+
if (!credits) return false;
|
|
1679
|
+
if (credits.used === 0) return true;
|
|
1680
|
+
if (credits.refunded === true) return true;
|
|
1681
|
+
return typeof credits.refunded === 'number' && credits.refunded > 0;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function formatCreditsLines(credits) {
|
|
1685
|
+
const lines = [];
|
|
1686
|
+
if (credits.used !== undefined || credits.remaining !== undefined) {
|
|
1687
|
+
lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
|
|
1688
|
+
}
|
|
1689
|
+
if (creditsWereRefunded(credits)) {
|
|
1690
|
+
lines.push('credits refunded');
|
|
1691
|
+
}
|
|
1692
|
+
return lines;
|
|
1500
1693
|
}
|
|
1501
1694
|
|
|
1502
1695
|
function formatPaidSearchResults(data) {
|
|
1503
1696
|
const lines = paidSearchVideos(data).map((row) => (
|
|
1504
1697
|
row.title ? `${row.title} | ${row.url}` : row.url
|
|
1505
1698
|
));
|
|
1506
|
-
const
|
|
1507
|
-
if (
|
|
1699
|
+
const creditLines = formatCreditsLines(paidSearchCredits(data));
|
|
1700
|
+
if (creditLines.length) {
|
|
1508
1701
|
if (lines.length) lines.push('');
|
|
1509
|
-
lines.push(
|
|
1702
|
+
lines.push(...creditLines);
|
|
1510
1703
|
}
|
|
1511
1704
|
return lines.join('\n');
|
|
1512
1705
|
}
|
|
@@ -1519,7 +1712,13 @@ function youtubeSearchFailureError(result) {
|
|
|
1519
1712
|
: result.status === 502
|
|
1520
1713
|
? ' YouTube search is unavailable; retry in a few seconds.'
|
|
1521
1714
|
: '';
|
|
1522
|
-
|
|
1715
|
+
const credits = paidSearchCredits(result.data);
|
|
1716
|
+
const refundHint = result.status === 502 && creditsWereRefunded(credits)
|
|
1717
|
+
? ' credits refunded.'
|
|
1718
|
+
: '';
|
|
1719
|
+
const lines = [`YouTube search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
|
|
1720
|
+
lines.push(...formatCreditsLines(credits));
|
|
1721
|
+
return new Error(lines.join('\n'));
|
|
1523
1722
|
}
|
|
1524
1723
|
|
|
1525
1724
|
async function requestPaidYoutubeSearch(options, deps = {}) {
|
|
@@ -1556,6 +1755,10 @@ async function requestPaidYoutubeSearch(options, deps = {}) {
|
|
|
1556
1755
|
|
|
1557
1756
|
async function runPaidYoutubeSearch(options, deps = {}) {
|
|
1558
1757
|
const output = deps.output || ((line = '') => console.log(line));
|
|
1758
|
+
if (readFreshLocalSearchCache(options.query, deps)) {
|
|
1759
|
+
output(PAID_SEARCH_FRESH_CACHE_REFUSE);
|
|
1760
|
+
return 2;
|
|
1761
|
+
}
|
|
1559
1762
|
const data = await requestPaidYoutubeSearch(options, deps);
|
|
1560
1763
|
if (options.json) {
|
|
1561
1764
|
output(JSON.stringify(data, null, 2));
|
|
@@ -1733,6 +1936,384 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
1733
1936
|
return 0;
|
|
1734
1937
|
}
|
|
1735
1938
|
|
|
1939
|
+
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save]';
|
|
1940
|
+
const TEACH_PAID_REFUSE = 'teach is free local captions. drop --paid.';
|
|
1941
|
+
const TEACH_APPLY_NEXT_MESSAGE = APPLY_NEXT_MESSAGE;
|
|
1942
|
+
const MECHANISM_STOP = new Set([
|
|
1943
|
+
'the', 'this', 'that', 'and', 'but', 'for', 'with', 'from', 'you', 'we', 'they',
|
|
1944
|
+
'what', 'when', 'how', 'why', 'there', 'here', 'then', 'just', 'also', 'very',
|
|
1945
|
+
'really', 'about', 'into', 'over', 'after', 'before', 'because', 'while', 'where',
|
|
1946
|
+
'which', 'their', 'your', 'our', 'its', 'not', 'all', 'any', 'some', 'more', 'most',
|
|
1947
|
+
'other', 'first', 'second', 'next', 'last', 'new', 'old', 'good', 'bad', 'big',
|
|
1948
|
+
'small', 'long', 'short', 'video', 'chapter', 'section', 'youtube', 'transcript',
|
|
1949
|
+
'yeah', 'okay', 'ok', 'so', 'well', 'like', 'right', 'now', 'one', 'two',
|
|
1950
|
+
'who', 'every', 'holy', 'want', 'operating',
|
|
1951
|
+
]);
|
|
1952
|
+
const NUMBER_UNITS = 'percent|million|billion|thousand|people|hours?|minutes?|seconds?|years?|months?|weeks?|days?|cycles?';
|
|
1953
|
+
const NUMBER_CLAIM_VERBS = 'install|ship|hire|raise|cut|save|cost|last|weigh|span|spend';
|
|
1954
|
+
const MECHANISM_HEADS = 'window|model|principle|pattern|loop|cycle|method|rule|doctrine|framework|heuristic';
|
|
1955
|
+
const TEACH_SWEAR_RE = /\b(fuck(?:ing)?|shit|damn|ass|bitch|crap)\b/i;
|
|
1956
|
+
const GENERIC_MECHANISM_LEFT = new Set([
|
|
1957
|
+
...MECHANISM_STOP,
|
|
1958
|
+
'a', 'an', 'in', 'of', 'to', 'my', 'i', 'im', "i'm",
|
|
1959
|
+
]);
|
|
1960
|
+
|
|
1961
|
+
function parseTeachArgs(argv = []) {
|
|
1962
|
+
const args = [...argv];
|
|
1963
|
+
const options = {
|
|
1964
|
+
help: false,
|
|
1965
|
+
save: false,
|
|
1966
|
+
url: null,
|
|
1967
|
+
section: 1,
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
if (args.length === 0 || ['help', '--help', '-h'].includes(args[0])) {
|
|
1971
|
+
options.help = true;
|
|
1972
|
+
return options;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1976
|
+
const arg = String(args[i]);
|
|
1977
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
1978
|
+
options.help = true;
|
|
1979
|
+
} else if (arg === '--save') {
|
|
1980
|
+
options.save = true;
|
|
1981
|
+
} else if (arg === '--paid') {
|
|
1982
|
+
throw new Error(TEACH_PAID_REFUSE);
|
|
1983
|
+
} else if (arg === '--section') {
|
|
1984
|
+
const raw = args[i + 1];
|
|
1985
|
+
const value = Number.parseInt(raw, 10);
|
|
1986
|
+
if (raw == null || String(raw).startsWith('--') || !Number.isInteger(value) || value < 1) {
|
|
1987
|
+
throw new Error('--section must be a positive integer');
|
|
1988
|
+
}
|
|
1989
|
+
options.section = value;
|
|
1990
|
+
i += 1;
|
|
1991
|
+
} else if (arg.startsWith('--section=')) {
|
|
1992
|
+
const value = Number.parseInt(arg.slice('--section='.length), 10);
|
|
1993
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1994
|
+
throw new Error('--section must be a positive integer');
|
|
1995
|
+
}
|
|
1996
|
+
options.section = value;
|
|
1997
|
+
} else if (arg.startsWith('-')) {
|
|
1998
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1999
|
+
} else if (!options.url && looksLikeYoutubeUrl(arg)) {
|
|
2000
|
+
options.url = arg;
|
|
2001
|
+
} else {
|
|
2002
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (options.help) return options;
|
|
2007
|
+
if (!options.url) throw new Error('Missing YouTube URL. Run "atris youtube teach --help".');
|
|
2008
|
+
return options;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function chapterStartSeconds(chapter) {
|
|
2012
|
+
if (!chapter) return NaN;
|
|
2013
|
+
if (chapter.startSeconds != null) return Number(chapter.startSeconds);
|
|
2014
|
+
return Number(chapter.start_time);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
function chapterEndSeconds(chapter) {
|
|
2018
|
+
if (!chapter) return NaN;
|
|
2019
|
+
if (chapter.endSeconds != null) return Number(chapter.endSeconds);
|
|
2020
|
+
return Number(chapter.end_time);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function normalizeChapters(rawChapters, durationSeconds) {
|
|
2024
|
+
const duration = Number(durationSeconds) || 0;
|
|
2025
|
+
const list = Array.isArray(rawChapters) ? rawChapters.filter(Boolean) : [];
|
|
2026
|
+
if (!list.length) {
|
|
2027
|
+
return [{
|
|
2028
|
+
index: 1,
|
|
2029
|
+
title: 'full video',
|
|
2030
|
+
startSeconds: 0,
|
|
2031
|
+
endSeconds: duration || Infinity,
|
|
2032
|
+
}];
|
|
2033
|
+
}
|
|
2034
|
+
return list.map((chapter, index) => {
|
|
2035
|
+
const start = chapterStartSeconds(chapter);
|
|
2036
|
+
const startSeconds = Number.isFinite(start) ? start : 0;
|
|
2037
|
+
const nextStart = chapterStartSeconds(list[index + 1]);
|
|
2038
|
+
const explicitEnd = chapterEndSeconds(chapter);
|
|
2039
|
+
const endSeconds = Number.isFinite(explicitEnd)
|
|
2040
|
+
? explicitEnd
|
|
2041
|
+
: (Number.isFinite(nextStart) ? nextStart : (duration || Infinity));
|
|
2042
|
+
const title = String(chapter.title || `section ${index + 1}`).trim() || `section ${index + 1}`;
|
|
2043
|
+
return { index: index + 1, title, startSeconds, endSeconds };
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function sliceCuesForChapter(cues = [], chapter) {
|
|
2048
|
+
if (!chapter) return [];
|
|
2049
|
+
const startMs = Number(chapter.startSeconds) * 1000;
|
|
2050
|
+
const endMs = Number(chapter.endSeconds) * 1000;
|
|
2051
|
+
return (cues || []).filter((cue) => {
|
|
2052
|
+
const at = Number(cue.startMs);
|
|
2053
|
+
if (!Number.isFinite(at)) return false;
|
|
2054
|
+
if (!Number.isFinite(startMs)) return true;
|
|
2055
|
+
if (at < startMs) return false;
|
|
2056
|
+
if (Number.isFinite(endMs) && at >= endMs) return false;
|
|
2057
|
+
return true;
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function teachCaptionWords(text) {
|
|
2062
|
+
return String(text || '')
|
|
2063
|
+
.replace(/\[\d{1,2}:\d{2}(?::\d{2})?\]/g, ' ')
|
|
2064
|
+
.replace(/\b\d{1,2}:\d{2}(?::\d{2})?(?:\.\d+)?\b/g, ' ')
|
|
2065
|
+
.replace(/[“”]/g, '"')
|
|
2066
|
+
.split(/\s+/)
|
|
2067
|
+
.map((word) => word.replace(/^[^A-Za-z0-9$%]+|[^A-Za-z0-9%]+$/g, ''))
|
|
2068
|
+
.filter(Boolean);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function extractTeachNumbers(text) {
|
|
2072
|
+
// Keep a number only with its unit or a nearby claim. Bare 20/60 are crumbs.
|
|
2073
|
+
const words = teachCaptionWords(text);
|
|
2074
|
+
const found = [];
|
|
2075
|
+
const seen = new Set();
|
|
2076
|
+
const numberRe = /^(?:\$)?(\d[\d,]*(?:\.\d+)?)(%?)$/;
|
|
2077
|
+
const unitRe = new RegExp(`^(?:${NUMBER_UNITS})$`, 'i');
|
|
2078
|
+
const verbRe = new RegExp(`\\b(?:${NUMBER_CLAIM_VERBS})\\b`, 'i');
|
|
2079
|
+
|
|
2080
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
2081
|
+
const match = words[i].match(numberRe);
|
|
2082
|
+
if (!match) continue;
|
|
2083
|
+
const nearby = words.slice(i + 1, i + 3);
|
|
2084
|
+
const unit = nearby.find((word) => unitRe.test(word));
|
|
2085
|
+
const hasPercent = match[2] === '%' || /%/.test(words[i]);
|
|
2086
|
+
if (!unit && !hasPercent) continue;
|
|
2087
|
+
|
|
2088
|
+
const windowText = words.slice(Math.max(0, i - 6), Math.min(words.length, i + 7)).join(' ');
|
|
2089
|
+
const verbMatch = windowText.match(verbRe);
|
|
2090
|
+
const number = match[1];
|
|
2091
|
+
let claim = hasPercent ? `${number}%` : `${number} ${String(unit).toLowerCase()}`;
|
|
2092
|
+
if (verbMatch) claim += ` to ${verbMatch[0].toLowerCase()}`;
|
|
2093
|
+
const key = claim.toLowerCase();
|
|
2094
|
+
if (seen.has(key)) continue;
|
|
2095
|
+
seen.add(key);
|
|
2096
|
+
found.push(claim);
|
|
2097
|
+
}
|
|
2098
|
+
return found.slice(0, 8);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function extractTeachMechanisms(text) {
|
|
2102
|
+
// Named only: "Overton window", "omakase model". Drop quotes, swears, crumbs.
|
|
2103
|
+
const found = [];
|
|
2104
|
+
const seen = new Set();
|
|
2105
|
+
const add = (value) => {
|
|
2106
|
+
const token = String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
2107
|
+
if (!token || TEACH_SWEAR_RE.test(token)) return;
|
|
2108
|
+
if (seen.has(token) || MECHANISM_STOP.has(token) || GENERIC_MECHANISM_LEFT.has(token)) return;
|
|
2109
|
+
if (token.length < 4 && !/\d/.test(token)) return;
|
|
2110
|
+
seen.add(token);
|
|
2111
|
+
found.push(token);
|
|
2112
|
+
};
|
|
2113
|
+
|
|
2114
|
+
const raw = String(text || '');
|
|
2115
|
+
const namedRe = new RegExp(`\\b([A-Za-z][A-Za-z0-9+-]{2,})\\s+(${MECHANISM_HEADS})\\b`, 'gi');
|
|
2116
|
+
for (const match of raw.matchAll(namedRe)) {
|
|
2117
|
+
const left = String(match[1] || '').toLowerCase();
|
|
2118
|
+
if (GENERIC_MECHANISM_LEFT.has(left) || MECHANISM_STOP.has(left)) continue;
|
|
2119
|
+
add(`${match[1]} ${match[2]}`);
|
|
2120
|
+
}
|
|
2121
|
+
for (const match of raw.matchAll(/\b(\d+[A-Za-z][A-Za-z0-9]+)\b/g)) add(match[1]);
|
|
2122
|
+
return found.slice(0, 8);
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function oneTeachCheck(mechanisms, numbers, title) {
|
|
2126
|
+
if (mechanisms[0]) {
|
|
2127
|
+
const name = mechanisms[0];
|
|
2128
|
+
const named = new RegExp(`\\b(?:${MECHANISM_HEADS})\\b`, 'i').test(name);
|
|
2129
|
+
if (named && !/^(the|a|an)\s/i.test(name)) return `what is the ${name}?`;
|
|
2130
|
+
return `what is ${name}?`;
|
|
2131
|
+
}
|
|
2132
|
+
if (numbers[0]) return `what does ${numbers[0]} measure in this chapter?`;
|
|
2133
|
+
return `what is the point of ${title || 'this chapter'}?`;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
function quoteYoutubeUrl(url) {
|
|
2137
|
+
return `"${String(url || '').replace(/"/g, '')}"`;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
function formatTeachLesson({ url, section, chapters, chapter, cues, title } = {}) {
|
|
2141
|
+
const total = Array.isArray(chapters) && chapters.length ? chapters.length : 1;
|
|
2142
|
+
const heading = String(chapter?.title || 'full video').trim().toLowerCase();
|
|
2143
|
+
const videoTitle = String(title || '').trim().toLowerCase();
|
|
2144
|
+
const body = (cues || []).map((cue) => cue.text).join(' ');
|
|
2145
|
+
const numbers = extractTeachNumbers(body);
|
|
2146
|
+
const mechanisms = extractTeachMechanisms(body);
|
|
2147
|
+
const lines = [
|
|
2148
|
+
`section ${section}/${total} ${heading}`,
|
|
2149
|
+
];
|
|
2150
|
+
if (videoTitle) lines.push(videoTitle);
|
|
2151
|
+
lines.push('');
|
|
2152
|
+
lines.push('numbers');
|
|
2153
|
+
if (numbers.length) lines.push(...numbers);
|
|
2154
|
+
else lines.push('none');
|
|
2155
|
+
lines.push('');
|
|
2156
|
+
lines.push('mechanisms');
|
|
2157
|
+
if (mechanisms.length) lines.push(...mechanisms);
|
|
2158
|
+
else lines.push('none');
|
|
2159
|
+
lines.push('');
|
|
2160
|
+
lines.push('check');
|
|
2161
|
+
lines.push(oneTeachCheck(mechanisms, numbers, heading));
|
|
2162
|
+
if (section < total) {
|
|
2163
|
+
lines.push('');
|
|
2164
|
+
lines.push(`next: atris youtube teach ${quoteYoutubeUrl(url)} --section ${section + 1}`);
|
|
2165
|
+
} else {
|
|
2166
|
+
lines.push('');
|
|
2167
|
+
lines.push('next: last section');
|
|
2168
|
+
}
|
|
2169
|
+
return lines.join('\n');
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function teachBriefRel(id, section) {
|
|
2173
|
+
return `atris/wiki/briefs/youtube-${id}-s${section}.md`;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
function fileTeachBrief({ cwd, url, section, lesson, now } = {}) {
|
|
2177
|
+
try {
|
|
2178
|
+
const id = videoIdFromUrl(url);
|
|
2179
|
+
if (!id || !cwd) return null;
|
|
2180
|
+
const wikiDir = path.join(cwd, 'atris', 'wiki');
|
|
2181
|
+
if (!fs.existsSync(wikiDir)) return null;
|
|
2182
|
+
const rel = teachBriefRel(id, section);
|
|
2183
|
+
const briefsDir = path.join(cwd, 'atris', 'wiki', 'briefs');
|
|
2184
|
+
fs.mkdirSync(briefsDir, { recursive: true });
|
|
2185
|
+
const date = dateStamp(now);
|
|
2186
|
+
const header = [
|
|
2187
|
+
String(lesson || '').split('\n')[0] || `teach section ${section}`,
|
|
2188
|
+
'',
|
|
2189
|
+
`date: ${date}`,
|
|
2190
|
+
`source: ${url}`,
|
|
2191
|
+
`section: ${section}`,
|
|
2192
|
+
'rail: atris youtube teach, one chapter from local captions',
|
|
2193
|
+
].join('\n');
|
|
2194
|
+
fs.writeFileSync(path.join(cwd, rel), `${header}\n\n${lesson}\n`);
|
|
2195
|
+
|
|
2196
|
+
const journalPath = path.join(cwd, 'atris', 'logs', date.slice(0, 4), `${date}.md`);
|
|
2197
|
+
fs.mkdirSync(path.dirname(journalPath), { recursive: true });
|
|
2198
|
+
let existing = '';
|
|
2199
|
+
if (fs.existsSync(journalPath)) existing = fs.readFileSync(journalPath, 'utf8');
|
|
2200
|
+
const line = `- [claimable] taught: section ${section} -> ${rel}`;
|
|
2201
|
+
if (!existing.includes(line)) {
|
|
2202
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
2203
|
+
fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
|
|
2204
|
+
}
|
|
2205
|
+
return rel;
|
|
2206
|
+
} catch {
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
function ensureTeachApply({ cwd, url, section, now, output } = {}) {
|
|
2212
|
+
const id = videoIdFromUrl(url);
|
|
2213
|
+
return applyGate.ensureApply({
|
|
2214
|
+
cwd,
|
|
2215
|
+
source: url,
|
|
2216
|
+
rel: id ? applySidecarRel(`${id}-s${section}`) : null,
|
|
2217
|
+
now,
|
|
2218
|
+
output,
|
|
2219
|
+
incompleteMessage: TEACH_APPLY_NEXT_MESSAGE,
|
|
2220
|
+
required: false,
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
async function extractTeachSource(youtubeUrl, deps = {}) {
|
|
2225
|
+
if (typeof deps.extractTeachSource === 'function') {
|
|
2226
|
+
return deps.extractTeachSource(youtubeUrl, deps);
|
|
2227
|
+
}
|
|
2228
|
+
const runner = deps.spawnSync || spawnSync;
|
|
2229
|
+
const result = runner('yt-dlp', ['-J', '--skip-download', '--no-warnings', youtubeUrl], {
|
|
2230
|
+
encoding: 'utf8',
|
|
2231
|
+
timeout: 20000,
|
|
2232
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2233
|
+
});
|
|
2234
|
+
if (result.error || result.status !== 0 || !result.stdout) return null;
|
|
2235
|
+
|
|
2236
|
+
let info;
|
|
2237
|
+
try {
|
|
2238
|
+
info = JSON.parse(result.stdout);
|
|
2239
|
+
} catch {
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const selected = chooseCaptionTrack(info);
|
|
2244
|
+
if (!selected?.track?.url) return null;
|
|
2245
|
+
const rawCaption = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
2246
|
+
const cues = parseCaptionCues(rawCaption);
|
|
2247
|
+
if (!cues.length) return null;
|
|
2248
|
+
|
|
2249
|
+
return {
|
|
2250
|
+
id: info.id || videoIdFromUrl(youtubeUrl),
|
|
2251
|
+
title: info.title || '',
|
|
2252
|
+
url: youtubeUrl,
|
|
2253
|
+
durationSeconds: Number(info.duration || 0) || undefined,
|
|
2254
|
+
language: selected.language || 'unknown',
|
|
2255
|
+
chapters: normalizeChapters(info.chapters, info.duration),
|
|
2256
|
+
cues,
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
async function runYoutubeTeach(args = [], deps = {}) {
|
|
2261
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
2262
|
+
let parsed;
|
|
2263
|
+
try {
|
|
2264
|
+
parsed = parseTeachArgs(args);
|
|
2265
|
+
} catch (err) {
|
|
2266
|
+
output(err.message || YTTEACH_USAGE);
|
|
2267
|
+
return 2;
|
|
2268
|
+
}
|
|
2269
|
+
if (parsed.help) {
|
|
2270
|
+
showYoutubeHelp(output, deps.commandName || 'atris youtube');
|
|
2271
|
+
return 0;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url, deps);
|
|
2275
|
+
if (!source || !Array.isArray(source.cues) || !source.cues.length) {
|
|
2276
|
+
output('no english captions for this url. teach stays local and will not call process.');
|
|
2277
|
+
return 2;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
const chapters = normalizeChapters(source.chapters, source.durationSeconds);
|
|
2281
|
+
if (parsed.section > chapters.length) {
|
|
2282
|
+
output(`section ${parsed.section} is past ${chapters.length} chapters. try --section ${chapters.length}`);
|
|
2283
|
+
return 2;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
const chapter = chapters[parsed.section - 1];
|
|
2287
|
+
const cues = sliceCuesForChapter(source.cues, chapter);
|
|
2288
|
+
const lesson = formatTeachLesson({
|
|
2289
|
+
url: parsed.url,
|
|
2290
|
+
section: parsed.section,
|
|
2291
|
+
chapters,
|
|
2292
|
+
chapter,
|
|
2293
|
+
cues,
|
|
2294
|
+
title: source.title,
|
|
2295
|
+
});
|
|
2296
|
+
output(lesson);
|
|
2297
|
+
|
|
2298
|
+
if (!parsed.save) return 0;
|
|
2299
|
+
|
|
2300
|
+
fileTeachBrief({
|
|
2301
|
+
cwd: deps.cwd || process.cwd(),
|
|
2302
|
+
url: parsed.url,
|
|
2303
|
+
section: parsed.section,
|
|
2304
|
+
lesson,
|
|
2305
|
+
now: deps.now,
|
|
2306
|
+
});
|
|
2307
|
+
const ensureApply = deps.ensureApply || ensureTeachApply;
|
|
2308
|
+
return ensureApply({
|
|
2309
|
+
cwd: deps.cwd || process.cwd(),
|
|
2310
|
+
url: parsed.url,
|
|
2311
|
+
section: parsed.section,
|
|
2312
|
+
now: deps.now,
|
|
2313
|
+
output,
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
|
|
1736
2317
|
async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
1737
2318
|
const output = deps.output || ((line = '') => console.log(line));
|
|
1738
2319
|
if (argv[0] === 'search') {
|
|
@@ -1742,11 +2323,21 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
1742
2323
|
}
|
|
1743
2324
|
return code;
|
|
1744
2325
|
}
|
|
2326
|
+
if (argv[0] === 'unsave') {
|
|
2327
|
+
const code = runYoutubeUnsave(argv.slice(1), deps);
|
|
2328
|
+
if (!deps.output && !deps.spawnSync && !deps.runner && !deps.expander) process.exit(code);
|
|
2329
|
+
return code;
|
|
2330
|
+
}
|
|
1745
2331
|
if (argv[0] === 'notes') {
|
|
1746
2332
|
const code = runYoutubeNotes(argv.slice(1), deps);
|
|
1747
2333
|
if (!deps.output && !deps.spawnSync && !deps.runner && !deps.expander) process.exit(code);
|
|
1748
2334
|
return code;
|
|
1749
2335
|
}
|
|
2336
|
+
if (argv[0] === 'teach') {
|
|
2337
|
+
const code = await runYoutubeTeach(argv.slice(1), { ...deps, output });
|
|
2338
|
+
if (!deps.output && !deps.extractTeachSource && !deps.spawnSync && !deps.runner) process.exit(code);
|
|
2339
|
+
return code;
|
|
2340
|
+
}
|
|
1750
2341
|
if (argv[0] === 'digest') {
|
|
1751
2342
|
const code = runYoutubeDigest(argv.slice(1), { ...deps, output });
|
|
1752
2343
|
if (!deps.output && !deps.runner) process.exit(code);
|
|
@@ -1767,8 +2358,8 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
1767
2358
|
const data = await processYoutube(options, deps);
|
|
1768
2359
|
output(options.json ? JSON.stringify(data, null, 2) : formatYoutubeResult(data));
|
|
1769
2360
|
} catch (err) {
|
|
1770
|
-
output(err.message);
|
|
1771
|
-
status = 1;
|
|
2361
|
+
if (!err.applyRequired) output(err.message);
|
|
2362
|
+
status = Number.isInteger(err.exitCode) ? err.exitCode : 1;
|
|
1772
2363
|
}
|
|
1773
2364
|
if (!deps.output && !deps.apiRequestJson && !deps.ensureValidCredentials && !deps.ensureBilledCommandAuth && !deps.extractLocalTranscript) {
|
|
1774
2365
|
process.exit(status);
|
|
@@ -1786,6 +2377,10 @@ module.exports = {
|
|
|
1786
2377
|
shouldRetryWithLocalTranscript,
|
|
1787
2378
|
formatYoutubeResult,
|
|
1788
2379
|
fileBriefFromNotes,
|
|
2380
|
+
ensureNotesApply,
|
|
2381
|
+
unsaveYoutubeNotes,
|
|
2382
|
+
APPLY_NEXT_MESSAGE,
|
|
2383
|
+
PROCESS_APPLY_MESSAGE,
|
|
1789
2384
|
isPlaylistUrl,
|
|
1790
2385
|
parseNotesArgs,
|
|
1791
2386
|
expandNotesTargets,
|
|
@@ -1801,5 +2396,13 @@ module.exports = {
|
|
|
1801
2396
|
parseSearchArgs,
|
|
1802
2397
|
parseSearchStdout,
|
|
1803
2398
|
formatSearchResults,
|
|
2399
|
+
parseTeachArgs,
|
|
2400
|
+
parseCaptionCues,
|
|
2401
|
+
normalizeChapters,
|
|
2402
|
+
sliceCuesForChapter,
|
|
2403
|
+
formatTeachLesson,
|
|
2404
|
+
extractTeachNumbers,
|
|
2405
|
+
extractTeachMechanisms,
|
|
2406
|
+
extractTeachSource,
|
|
1804
2407
|
youtubeCommand,
|
|
1805
2408
|
};
|