youtube-knowledge-mcp 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -41
- package/dist/brain-schemas.d.ts +10 -0
- package/dist/brain-schemas.d.ts.map +1 -1
- package/dist/harvest-schemas.d.ts +6 -6
- package/dist/harvest-schemas.d.ts.map +1 -1
- package/dist/harvest-schemas.js +5 -0
- package/dist/harvest-schemas.js.map +1 -1
- package/dist/registry/brain-tools.d.ts.map +1 -1
- package/dist/registry/brain-tools.js +16 -0
- package/dist/registry/brain-tools.js.map +1 -1
- package/dist/registry/channel-tools.d.ts.map +1 -1
- package/dist/registry/channel-tools.js +16 -0
- package/dist/registry/channel-tools.js.map +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +4 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/media-tools.d.ts.map +1 -1
- package/dist/registry/media-tools.js +31 -0
- package/dist/registry/media-tools.js.map +1 -1
- package/dist/registry/store-tools.d.ts +9 -0
- package/dist/registry/store-tools.d.ts.map +1 -0
- package/dist/registry/store-tools.js +59 -0
- package/dist/registry/store-tools.js.map +1 -0
- package/dist/registry/transcript-tools.d.ts +4 -1
- package/dist/registry/transcript-tools.d.ts.map +1 -1
- package/dist/registry/transcript-tools.js +20 -1
- package/dist/registry/transcript-tools.js.map +1 -1
- package/dist/registry/video-tools.d.ts.map +1 -1
- package/dist/registry/video-tools.js +48 -0
- package/dist/registry/video-tools.js.map +1 -1
- package/dist/registry/vision-tools.d.ts +7 -0
- package/dist/registry/vision-tools.d.ts.map +1 -0
- package/dist/registry/vision-tools.js +55 -0
- package/dist/registry/vision-tools.js.map +1 -0
- package/dist/schemas.d.ts +6 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +2 -0
- package/dist/schemas.js.map +1 -1
- package/dist/thumbnail-schemas.d.ts +10 -0
- package/dist/thumbnail-schemas.d.ts.map +1 -1
- package/dist/tools/ask-brains.d.ts +66 -0
- package/dist/tools/ask-brains.d.ts.map +1 -0
- package/dist/tools/ask-brains.js +111 -0
- package/dist/tools/ask-brains.js.map +1 -0
- package/dist/tools/check-health.d.ts +16 -6
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +30 -1
- package/dist/tools/check-health.js.map +1 -1
- package/dist/tools/check-new-uploads.d.ts +55 -0
- package/dist/tools/check-new-uploads.d.ts.map +1 -0
- package/dist/tools/check-new-uploads.js +74 -0
- package/dist/tools/check-new-uploads.js.map +1 -0
- package/dist/tools/clips.d.ts +15 -0
- package/dist/tools/clips.d.ts.map +1 -1
- package/dist/tools/clips.js +5 -5
- package/dist/tools/clips.js.map +1 -1
- package/dist/tools/detect-scenes.d.ts +38 -0
- package/dist/tools/detect-scenes.d.ts.map +1 -0
- package/dist/tools/detect-scenes.js +66 -0
- package/dist/tools/detect-scenes.js.map +1 -0
- package/dist/tools/export-data.d.ts +85 -0
- package/dist/tools/export-data.d.ts.map +1 -0
- package/dist/tools/export-data.js +99 -0
- package/dist/tools/export-data.js.map +1 -0
- package/dist/tools/frames-subtitles.d.ts.map +1 -1
- package/dist/tools/frames-subtitles.js +16 -8
- package/dist/tools/frames-subtitles.js.map +1 -1
- package/dist/tools/frames.d.ts +73 -0
- package/dist/tools/frames.d.ts.map +1 -0
- package/dist/tools/frames.js +136 -0
- package/dist/tools/frames.js.map +1 -0
- package/dist/tools/get-channel-info.d.ts +2 -0
- package/dist/tools/get-channel-info.d.ts.map +1 -1
- package/dist/tools/get-channel-info.js +4 -0
- package/dist/tools/get-channel-info.js.map +1 -1
- package/dist/tools/get-comments.d.ts +5 -5
- package/dist/tools/get-coverage.d.ts +5 -5
- package/dist/tools/get-sponsor-segments.d.ts +49 -0
- package/dist/tools/get-sponsor-segments.d.ts.map +1 -0
- package/dist/tools/get-sponsor-segments.js +61 -0
- package/dist/tools/get-sponsor-segments.js.map +1 -0
- package/dist/tools/get-thumbnail.d.ts.map +1 -1
- package/dist/tools/get-thumbnail.js +2 -1
- package/dist/tools/get-thumbnail.js.map +1 -1
- package/dist/tools/get-transcript.d.ts +1 -0
- package/dist/tools/get-transcript.d.ts.map +1 -1
- package/dist/tools/get-transcript.js +11 -4
- package/dist/tools/get-transcript.js.map +1 -1
- package/dist/tools/get-video-details.d.ts +118 -0
- package/dist/tools/get-video-details.d.ts.map +1 -0
- package/dist/tools/get-video-details.js +147 -0
- package/dist/tools/get-video-details.js.map +1 -0
- package/dist/tools/gif-concat.d.ts +49 -0
- package/dist/tools/gif-concat.d.ts.map +1 -0
- package/dist/tools/gif-concat.js +96 -0
- package/dist/tools/gif-concat.js.map +1 -0
- package/dist/tools/harvest-channel.d.ts +5 -5
- package/dist/tools/harvest-comments.d.ts +5 -5
- package/dist/tools/harvest-details.d.ts +84 -0
- package/dist/tools/harvest-details.d.ts.map +1 -0
- package/dist/tools/harvest-details.js +71 -0
- package/dist/tools/harvest-details.js.map +1 -0
- package/dist/tools/list-captions.d.ts +17 -0
- package/dist/tools/list-captions.d.ts.map +1 -0
- package/dist/tools/list-captions.js +60 -0
- package/dist/tools/list-captions.js.map +1 -0
- package/dist/tools/list-channel-playlists.d.ts +30 -0
- package/dist/tools/list-channel-playlists.d.ts.map +1 -0
- package/dist/tools/list-channel-playlists.js +44 -0
- package/dist/tools/list-channel-playlists.js.map +1 -0
- package/dist/tools/query-comments.d.ts +5 -5
- package/dist/tools/query-videos.d.ts +5 -5
- package/dist/tools/search-channels.d.ts +6 -0
- package/dist/tools/search-channels.d.ts.map +1 -1
- package/dist/tools/transcribe-video.d.ts +28 -0
- package/dist/tools/transcribe-video.d.ts.map +1 -0
- package/dist/tools/transcribe-video.js +52 -0
- package/dist/tools/transcribe-video.js.map +1 -0
- package/dist/utils/brain-fusion.d.ts +24 -0
- package/dist/utils/brain-fusion.d.ts.map +1 -0
- package/dist/utils/brain-fusion.js +18 -0
- package/dist/utils/brain-fusion.js.map +1 -0
- package/dist/utils/channel-playlists.d.ts +21 -0
- package/dist/utils/channel-playlists.d.ts.map +1 -0
- package/dist/utils/channel-playlists.js +56 -0
- package/dist/utils/channel-playlists.js.map +1 -0
- package/dist/utils/clips.d.ts.map +1 -1
- package/dist/utils/clips.js +3 -17
- package/dist/utils/clips.js.map +1 -1
- package/dist/utils/concat.d.ts +19 -0
- package/dist/utils/concat.d.ts.map +1 -0
- package/dist/utils/concat.js +135 -0
- package/dist/utils/concat.js.map +1 -0
- package/dist/utils/contact-sheet.d.ts +28 -0
- package/dist/utils/contact-sheet.d.ts.map +1 -0
- package/dist/utils/contact-sheet.js +65 -0
- package/dist/utils/contact-sheet.js.map +1 -0
- package/dist/utils/detail-batch.d.ts +25 -0
- package/dist/utils/detail-batch.d.ts.map +1 -0
- package/dist/utils/detail-batch.js +58 -0
- package/dist/utils/detail-batch.js.map +1 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +10 -0
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/export-data.d.ts +41 -0
- package/dist/utils/export-data.d.ts.map +1 -0
- package/dist/utils/export-data.js +114 -0
- package/dist/utils/export-data.js.map +1 -0
- package/dist/utils/export-format.d.ts +23 -0
- package/dist/utils/export-format.d.ts.map +1 -0
- package/dist/utils/export-format.js +59 -0
- package/dist/utils/export-format.js.map +1 -0
- package/dist/utils/export-load.d.ts +28 -0
- package/dist/utils/export-load.d.ts.map +1 -0
- package/dist/utils/export-load.js +70 -0
- package/dist/utils/export-load.js.map +1 -0
- package/dist/utils/export-rows.d.ts +35 -0
- package/dist/utils/export-rows.d.ts.map +1 -0
- package/dist/utils/export-rows.js +69 -0
- package/dist/utils/export-rows.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +26 -0
- package/dist/utils/ffmpeg.d.ts.map +1 -0
- package/dist/utils/ffmpeg.js +28 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +4 -0
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/frame-plan.d.ts +24 -0
- package/dist/utils/frame-plan.d.ts.map +1 -0
- package/dist/utils/frame-plan.js +58 -0
- package/dist/utils/frame-plan.js.map +1 -0
- package/dist/utils/frames.d.ts +32 -0
- package/dist/utils/frames.d.ts.map +1 -0
- package/dist/utils/frames.js +60 -0
- package/dist/utils/frames.js.map +1 -0
- package/dist/utils/gif.d.ts +31 -0
- package/dist/utils/gif.d.ts.map +1 -0
- package/dist/utils/gif.js +77 -0
- package/dist/utils/gif.js.map +1 -0
- package/dist/utils/harvest-catalog.d.ts +10 -0
- package/dist/utils/harvest-catalog.d.ts.map +1 -1
- package/dist/utils/harvest-catalog.js +14 -5
- package/dist/utils/harvest-catalog.js.map +1 -1
- package/dist/utils/harvest-details.d.ts +30 -0
- package/dist/utils/harvest-details.d.ts.map +1 -0
- package/dist/utils/harvest-details.js +98 -0
- package/dist/utils/harvest-details.js.map +1 -0
- package/dist/utils/local-transcription.d.ts +14 -0
- package/dist/utils/local-transcription.d.ts.map +1 -0
- package/dist/utils/local-transcription.js +85 -0
- package/dist/utils/local-transcription.js.map +1 -0
- package/dist/utils/moment.d.ts +24 -0
- package/dist/utils/moment.d.ts.map +1 -0
- package/dist/utils/moment.js +22 -0
- package/dist/utils/moment.js.map +1 -0
- package/dist/utils/most-replayed.d.ts +15 -0
- package/dist/utils/most-replayed.d.ts.map +1 -0
- package/dist/utils/most-replayed.js +25 -0
- package/dist/utils/most-replayed.js.map +1 -0
- package/dist/utils/scenes.d.ts +39 -0
- package/dist/utils/scenes.d.ts.map +1 -0
- package/dist/utils/scenes.js +83 -0
- package/dist/utils/scenes.js.map +1 -0
- package/dist/utils/seconds.d.ts +5 -0
- package/dist/utils/seconds.d.ts.map +1 -0
- package/dist/utils/seconds.js +7 -0
- package/dist/utils/seconds.js.map +1 -0
- package/dist/utils/sponsorblock.d.ts +24 -0
- package/dist/utils/sponsorblock.d.ts.map +1 -0
- package/dist/utils/sponsorblock.js +153 -0
- package/dist/utils/sponsorblock.js.map +1 -0
- package/dist/utils/transcript-cache.d.ts +5 -1
- package/dist/utils/transcript-cache.d.ts.map +1 -1
- package/dist/utils/transcript-cache.js +31 -50
- package/dist/utils/transcript-cache.js.map +1 -1
- package/dist/utils/transcript-files.d.ts +24 -0
- package/dist/utils/transcript-files.d.ts.map +1 -0
- package/dist/utils/transcript-files.js +61 -0
- package/dist/utils/transcript-files.js.map +1 -0
- package/dist/utils/transcript.d.ts +3 -0
- package/dist/utils/transcript.d.ts.map +1 -1
- package/dist/utils/transcript.js +2 -0
- package/dist/utils/transcript.js.map +1 -1
- package/dist/utils/upload-check.d.ts +38 -0
- package/dist/utils/upload-check.d.ts.map +1 -0
- package/dist/utils/upload-check.js +93 -0
- package/dist/utils/upload-check.js.map +1 -0
- package/dist/utils/validate.d.ts +2 -0
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +4 -0
- package/dist/utils/validate.js.map +1 -1
- package/dist/utils/video-detail-store.d.ts +47 -0
- package/dist/utils/video-detail-store.d.ts.map +1 -0
- package/dist/utils/video-detail-store.js +89 -0
- package/dist/utils/video-detail-store.js.map +1 -0
- package/dist/utils/video-stats-cache.js +1 -1
- package/dist/utils/video-stats-schema.d.ts +187 -0
- package/dist/utils/video-stats-schema.d.ts.map +1 -0
- package/dist/utils/video-stats-schema.js +50 -0
- package/dist/utils/video-stats-schema.js.map +1 -0
- package/dist/utils/video-stats-storage.d.ts +16 -0
- package/dist/utils/video-stats-storage.d.ts.map +1 -0
- package/dist/utils/video-stats-storage.js +37 -0
- package/dist/utils/video-stats-storage.js.map +1 -0
- package/dist/utils/video-stats.d.ts +23 -127
- package/dist/utils/video-stats.d.ts.map +1 -1
- package/dist/utils/video-stats.js +42 -102
- package/dist/utils/video-stats.js.map +1 -1
- package/dist/utils/video-stream.d.ts +21 -0
- package/dist/utils/video-stream.d.ts.map +1 -0
- package/dist/utils/video-stream.js +36 -0
- package/dist/utils/video-stream.js.map +1 -0
- package/dist/utils/whisper.d.ts +39 -0
- package/dist/utils/whisper.d.ts.map +1 -0
- package/dist/utils/whisper.js +137 -0
- package/dist/utils/whisper.js.map +1 -0
- package/dist/utils/youtube-channel.d.ts +4 -0
- package/dist/utils/youtube-channel.d.ts.map +1 -1
- package/dist/utils/youtube-channel.js +6 -0
- package/dist/utils/youtube-channel.js.map +1 -1
- package/dist/utils/youtube-schemas.d.ts +6 -0
- package/dist/utils/youtube-schemas.d.ts.map +1 -1
- package/dist/utils/youtube-schemas.js +2 -0
- package/dist/utils/youtube-schemas.js.map +1 -1
- package/package.json +9 -3
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { videoStatsRowSchema, type MusicCredits, type VideoFormat } from './video-stats-schema.js';
|
|
3
|
+
export { videoStatsRowSchema } from './video-stats-schema.js';
|
|
4
|
+
export { CATALOG_DESCRIPTION_PREVIEW_CHARS, MAX_DETAIL_JSON_BYTES, STRIPPED_KEYS, stripForStorage, } from './video-stats-storage.js';
|
|
5
|
+
export type { MusicCredits, VideoFormat } from './video-stats-schema.js';
|
|
2
6
|
/**
|
|
3
7
|
* One full read of a video, serving every caller that needs part of it.
|
|
4
8
|
*
|
|
@@ -13,129 +17,6 @@ import { z } from 'zod';
|
|
|
13
17
|
* the record. Stripped, a video is 4,785 bytes. Storing the raw object would
|
|
14
18
|
* cost 6.4GB per 10,000 videos instead of 48MB.
|
|
15
19
|
*/
|
|
16
|
-
/** A stored snapshot must stay small enough that a catalogue is not mostly URLs. */
|
|
17
|
-
export declare const MAX_DETAIL_JSON_BYTES = 32768;
|
|
18
|
-
/** Keys dropped before storage: expiring URLs and bulk we can re-fetch. */
|
|
19
|
-
export declare const STRIPPED_KEYS: readonly ["formats", "requested_formats", "automatic_captions", "subtitles", "thumbnails", "comments", "heatmap"];
|
|
20
|
-
export declare const videoStatsRowSchema: z.ZodObject<{
|
|
21
|
-
id: z.ZodString;
|
|
22
|
-
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
23
|
-
channel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
24
|
-
channel_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
25
|
-
channel_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
26
|
-
channel_follower_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
27
|
-
upload_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
28
|
-
duration: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
29
|
-
view_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
30
|
-
like_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
31
|
-
comment_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
32
|
-
concurrent_view_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
33
|
-
categories: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString, "many">>>;
|
|
34
|
-
tags: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString, "many">>>;
|
|
35
|
-
age_limit: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
36
|
-
availability: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
|
-
live_status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
38
|
-
was_live: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
39
|
-
is_live: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
40
|
-
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
41
|
-
thumbnail: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
42
|
-
chapters: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
43
|
-
title: z.ZodString;
|
|
44
|
-
start_time: z.ZodNumber;
|
|
45
|
-
end_time: z.ZodNumber;
|
|
46
|
-
}, "strip", z.ZodTypeAny, {
|
|
47
|
-
title: string;
|
|
48
|
-
start_time: number;
|
|
49
|
-
end_time: number;
|
|
50
|
-
}, {
|
|
51
|
-
title: string;
|
|
52
|
-
start_time: number;
|
|
53
|
-
end_time: number;
|
|
54
|
-
}>, "many">>>;
|
|
55
|
-
heatmap: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
56
|
-
start_time: z.ZodNumber;
|
|
57
|
-
end_time: z.ZodNumber;
|
|
58
|
-
value: z.ZodNumber;
|
|
59
|
-
}, "strip", z.ZodTypeAny, {
|
|
60
|
-
value: number;
|
|
61
|
-
start_time: number;
|
|
62
|
-
end_time: number;
|
|
63
|
-
}, {
|
|
64
|
-
value: number;
|
|
65
|
-
start_time: number;
|
|
66
|
-
end_time: number;
|
|
67
|
-
}>, "many">>>;
|
|
68
|
-
automatic_captions: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
69
|
-
subtitles: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
70
|
-
}, "strip", z.ZodTypeAny, {
|
|
71
|
-
id: string;
|
|
72
|
-
title?: string | null | undefined;
|
|
73
|
-
channel?: string | null | undefined;
|
|
74
|
-
description?: string | null | undefined;
|
|
75
|
-
tags?: string[] | null | undefined;
|
|
76
|
-
duration?: number | null | undefined;
|
|
77
|
-
upload_date?: string | null | undefined;
|
|
78
|
-
thumbnail?: string | null | undefined;
|
|
79
|
-
view_count?: number | null | undefined;
|
|
80
|
-
like_count?: number | null | undefined;
|
|
81
|
-
comment_count?: number | null | undefined;
|
|
82
|
-
availability?: string | null | undefined;
|
|
83
|
-
live_status?: string | null | undefined;
|
|
84
|
-
chapters?: {
|
|
85
|
-
title: string;
|
|
86
|
-
start_time: number;
|
|
87
|
-
end_time: number;
|
|
88
|
-
}[] | null | undefined;
|
|
89
|
-
subtitles?: Record<string, unknown> | null | undefined;
|
|
90
|
-
automatic_captions?: Record<string, unknown> | null | undefined;
|
|
91
|
-
channel_id?: string | null | undefined;
|
|
92
|
-
channel_url?: string | null | undefined;
|
|
93
|
-
channel_follower_count?: number | null | undefined;
|
|
94
|
-
heatmap?: {
|
|
95
|
-
value: number;
|
|
96
|
-
start_time: number;
|
|
97
|
-
end_time: number;
|
|
98
|
-
}[] | null | undefined;
|
|
99
|
-
concurrent_view_count?: number | null | undefined;
|
|
100
|
-
categories?: string[] | null | undefined;
|
|
101
|
-
age_limit?: number | null | undefined;
|
|
102
|
-
was_live?: boolean | null | undefined;
|
|
103
|
-
is_live?: boolean | null | undefined;
|
|
104
|
-
}, {
|
|
105
|
-
id: string;
|
|
106
|
-
title?: string | null | undefined;
|
|
107
|
-
channel?: string | null | undefined;
|
|
108
|
-
description?: string | null | undefined;
|
|
109
|
-
tags?: string[] | null | undefined;
|
|
110
|
-
duration?: number | null | undefined;
|
|
111
|
-
upload_date?: string | null | undefined;
|
|
112
|
-
thumbnail?: string | null | undefined;
|
|
113
|
-
view_count?: number | null | undefined;
|
|
114
|
-
like_count?: number | null | undefined;
|
|
115
|
-
comment_count?: number | null | undefined;
|
|
116
|
-
availability?: string | null | undefined;
|
|
117
|
-
live_status?: string | null | undefined;
|
|
118
|
-
chapters?: {
|
|
119
|
-
title: string;
|
|
120
|
-
start_time: number;
|
|
121
|
-
end_time: number;
|
|
122
|
-
}[] | null | undefined;
|
|
123
|
-
subtitles?: Record<string, unknown> | null | undefined;
|
|
124
|
-
automatic_captions?: Record<string, unknown> | null | undefined;
|
|
125
|
-
channel_id?: string | null | undefined;
|
|
126
|
-
channel_url?: string | null | undefined;
|
|
127
|
-
channel_follower_count?: number | null | undefined;
|
|
128
|
-
heatmap?: {
|
|
129
|
-
value: number;
|
|
130
|
-
start_time: number;
|
|
131
|
-
end_time: number;
|
|
132
|
-
}[] | null | undefined;
|
|
133
|
-
concurrent_view_count?: number | null | undefined;
|
|
134
|
-
categories?: string[] | null | undefined;
|
|
135
|
-
age_limit?: number | null | undefined;
|
|
136
|
-
was_live?: boolean | null | undefined;
|
|
137
|
-
is_live?: boolean | null | undefined;
|
|
138
|
-
}>;
|
|
139
20
|
export interface VideoStats {
|
|
140
21
|
id: string;
|
|
141
22
|
title: string;
|
|
@@ -175,12 +56,27 @@ export interface VideoStats {
|
|
|
175
56
|
descriptionChars: number;
|
|
176
57
|
description: string;
|
|
177
58
|
thumbnailUrl?: string;
|
|
59
|
+
channelUrl?: string;
|
|
60
|
+
uploader?: string;
|
|
61
|
+
license?: string;
|
|
62
|
+
language?: string;
|
|
63
|
+
/** yt-dlp's own label, such as `video`, `short` or `livestream`. */
|
|
64
|
+
mediaType?: string;
|
|
65
|
+
playableInEmbed?: boolean;
|
|
66
|
+
/** Unix seconds a premiere or scheduled video goes live. */
|
|
67
|
+
releaseTimestamp?: number;
|
|
68
|
+
music?: MusicCredits;
|
|
69
|
+
video?: VideoFormat;
|
|
178
70
|
}
|
|
179
|
-
export declare const CATALOG_DESCRIPTION_PREVIEW_CHARS = 500;
|
|
180
|
-
/** Drops the bulk keys, so what is stored is the record rather than its attachments. */
|
|
181
|
-
export declare function stripForStorage(raw: Record<string, unknown>): Record<string, unknown>;
|
|
182
71
|
export declare function toVideoStats(row: z.infer<typeof videoStatsRowSchema>): VideoStats;
|
|
183
72
|
/** One `-j` read, serving info, details, chapters, captions and the heatmap. */
|
|
184
|
-
export declare function fetchVideoStatsRow(urlOrId: string
|
|
73
|
+
export declare function fetchVideoStatsRow(urlOrId: string, options?: {
|
|
74
|
+
/**
|
|
75
|
+
* Have yt-dlp return a video YouTube refuses to serve as a row, with `availability`
|
|
76
|
+
* filled in, instead of failing. Without it a private video fails with a message that
|
|
77
|
+
* asks to sign in, which reads as a session problem rather than as that one video.
|
|
78
|
+
*/
|
|
79
|
+
tolerateRefusal?: boolean;
|
|
80
|
+
}): Promise<z.infer<typeof videoStatsRowSchema>>;
|
|
185
81
|
export declare function getVideoStats(urlOrId: string): Promise<VideoStats>;
|
|
186
82
|
//# sourceMappingURL=video-stats.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-stats.d.ts","sourceRoot":"","sources":["../../src/utils/video-stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"video-stats.d.ts","sourceRoot":"","sources":["../../src/utils/video-stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,mBAAmB,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEnG,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EACL,iCAAiC,EACjC,qBAAqB,EACrB,aAAa,EACb,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClE,+EAA+E;IAC/E,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxE,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AASD,wBAAgB,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,GAAG,UAAU,CA+EjF;AAED,gFAAgF;AAChF,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IACP;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CACtB,GACL,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAY9C;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAExE"}
|
|
@@ -1,82 +1,15 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
1
|
import { TIMEOUTS, parseYtDlpJson, runYtDlp } from './ytdlp.js';
|
|
3
2
|
import { extractVideoId, formatDuration, watchUrl } from './youtube-url.js';
|
|
4
3
|
import { classifyPlayability } from './errors.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* `automatic_captions` is 460,965 bytes (72%) of URLs that expire, `formats`
|
|
15
|
-
* another 113,601 (18%), and `heatmap` 6,589 — larger than the whole rest of
|
|
16
|
-
* the record. Stripped, a video is 4,785 bytes. Storing the raw object would
|
|
17
|
-
* cost 6.4GB per 10,000 videos instead of 48MB.
|
|
18
|
-
*/
|
|
19
|
-
/** A stored snapshot must stay small enough that a catalogue is not mostly URLs. */
|
|
20
|
-
export const MAX_DETAIL_JSON_BYTES = 32_768;
|
|
21
|
-
/** Keys dropped before storage: expiring URLs and bulk we can re-fetch. */
|
|
22
|
-
export const STRIPPED_KEYS = [
|
|
23
|
-
'formats',
|
|
24
|
-
'requested_formats',
|
|
25
|
-
'automatic_captions',
|
|
26
|
-
'subtitles',
|
|
27
|
-
'thumbnails',
|
|
28
|
-
'comments',
|
|
29
|
-
'heatmap',
|
|
30
|
-
];
|
|
31
|
-
const heatmapEntrySchema = z.object({
|
|
32
|
-
start_time: z.number(),
|
|
33
|
-
end_time: z.number(),
|
|
34
|
-
value: z.number(),
|
|
35
|
-
});
|
|
36
|
-
export const videoStatsRowSchema = z.object({
|
|
37
|
-
id: z.string(),
|
|
38
|
-
title: z.string().nullish(),
|
|
39
|
-
channel: z.string().nullish(),
|
|
40
|
-
channel_id: z.string().nullish(),
|
|
41
|
-
channel_url: z.string().nullish(),
|
|
42
|
-
channel_follower_count: z.number().nullish(),
|
|
43
|
-
upload_date: z.string().nullish(),
|
|
44
|
-
duration: z.number().nullish(),
|
|
45
|
-
view_count: z.number().nullish(),
|
|
46
|
-
like_count: z.number().nullish(),
|
|
47
|
-
comment_count: z.number().nullish(),
|
|
48
|
-
concurrent_view_count: z.number().nullish(),
|
|
49
|
-
categories: z.array(z.string()).nullish(),
|
|
50
|
-
tags: z.array(z.string()).nullish(),
|
|
51
|
-
age_limit: z.number().nullish(),
|
|
52
|
-
availability: z.string().nullish(),
|
|
53
|
-
live_status: z.string().nullish(),
|
|
54
|
-
was_live: z.boolean().nullish(),
|
|
55
|
-
is_live: z.boolean().nullish(),
|
|
56
|
-
description: z.string().nullish(),
|
|
57
|
-
thumbnail: z.string().nullish(),
|
|
58
|
-
chapters: z
|
|
59
|
-
.array(z.object({ title: z.string(), start_time: z.number(), end_time: z.number() }))
|
|
60
|
-
.nullish(),
|
|
61
|
-
heatmap: z.array(heatmapEntrySchema).nullish(),
|
|
62
|
-
automatic_captions: z.record(z.string(), z.unknown()).nullish(),
|
|
63
|
-
subtitles: z.record(z.string(), z.unknown()).nullish(),
|
|
64
|
-
});
|
|
65
|
-
export const CATALOG_DESCRIPTION_PREVIEW_CHARS = 500;
|
|
66
|
-
/** Drops the bulk keys, so what is stored is the record rather than its attachments. */
|
|
67
|
-
export function stripForStorage(raw) {
|
|
68
|
-
const kept = {};
|
|
69
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
70
|
-
if (STRIPPED_KEYS.some((stripped) => stripped === key))
|
|
71
|
-
continue;
|
|
72
|
-
kept[key] = value;
|
|
73
|
-
}
|
|
74
|
-
const description = kept.description;
|
|
75
|
-
if (typeof description === 'string' && description.length > CATALOG_DESCRIPTION_PREVIEW_CHARS) {
|
|
76
|
-
kept.description = description.slice(0, CATALOG_DESCRIPTION_PREVIEW_CHARS);
|
|
77
|
-
kept.description_truncated = true;
|
|
78
|
-
}
|
|
79
|
-
return kept;
|
|
4
|
+
import { videoStatsRowSchema } from './video-stats-schema.js';
|
|
5
|
+
export { videoStatsRowSchema } from './video-stats-schema.js';
|
|
6
|
+
export { CATALOG_DESCRIPTION_PREVIEW_CHARS, MAX_DETAIL_JSON_BYTES, STRIPPED_KEYS, stripForStorage, } from './video-stats-storage.js';
|
|
7
|
+
/** `{ [key]: value }` when yt-dlp told us something, `{}` when it said null or nothing. */
|
|
8
|
+
function present(key, value) {
|
|
9
|
+
const field = {};
|
|
10
|
+
if (value !== null && value !== undefined)
|
|
11
|
+
field[key] = value;
|
|
12
|
+
return field;
|
|
80
13
|
}
|
|
81
14
|
export function toVideoStats(row) {
|
|
82
15
|
// classifyPlayability RETURNS the error rather than throwing it, so ignoring
|
|
@@ -91,38 +24,44 @@ export function toVideoStats(row) {
|
|
|
91
24
|
const duration = row.duration ?? 0;
|
|
92
25
|
const heatmap = row.heatmap ?? undefined;
|
|
93
26
|
const description = row.description ?? '';
|
|
27
|
+
const music = {
|
|
28
|
+
...present('track', row.track),
|
|
29
|
+
...present('artist', row.artist),
|
|
30
|
+
...present('album', row.album),
|
|
31
|
+
...present('releaseYear', row.release_year),
|
|
32
|
+
};
|
|
33
|
+
const format = {
|
|
34
|
+
...present('width', row.width),
|
|
35
|
+
...present('height', row.height),
|
|
36
|
+
...present('fps', row.fps),
|
|
37
|
+
};
|
|
94
38
|
return {
|
|
95
39
|
id: row.id,
|
|
96
40
|
title: row.title ?? 'Unknown',
|
|
97
41
|
url: watchUrl(row.id),
|
|
98
42
|
channel: row.channel ?? 'Unknown',
|
|
99
|
-
...(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
...(
|
|
103
|
-
? {}
|
|
104
|
-
: { channelFollowerCount: row.channel_follower_count }),
|
|
43
|
+
...present('channelId', row.channel_id),
|
|
44
|
+
...present('channelFollowerCount', row.channel_follower_count),
|
|
45
|
+
...present('channelUrl', row.channel_url),
|
|
46
|
+
...present('uploader', row.uploader),
|
|
105
47
|
uploadDate: row.upload_date ?? '',
|
|
106
48
|
durationSeconds: duration,
|
|
107
49
|
durationFormatted: formatDuration(Math.floor(duration)),
|
|
108
|
-
...(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
...(row.like_count === null || row.like_count === undefined
|
|
112
|
-
? {}
|
|
113
|
-
: { likeCount: row.like_count }),
|
|
114
|
-
...(row.comment_count === null || row.comment_count === undefined
|
|
115
|
-
? {}
|
|
116
|
-
: { commentCount: row.comment_count }),
|
|
50
|
+
...present('viewCount', row.view_count),
|
|
51
|
+
...present('likeCount', row.like_count),
|
|
52
|
+
...present('commentCount', row.comment_count),
|
|
117
53
|
categories: row.categories ?? [],
|
|
118
54
|
tags: row.tags ?? [],
|
|
119
55
|
ageLimit: row.age_limit ?? 0,
|
|
120
|
-
...(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
...(
|
|
124
|
-
|
|
125
|
-
|
|
56
|
+
...present('availability', row.availability),
|
|
57
|
+
...present('liveStatus', row.live_status),
|
|
58
|
+
...present('license', row.license),
|
|
59
|
+
...present('language', row.language),
|
|
60
|
+
...present('mediaType', row.media_type),
|
|
61
|
+
...present('playableInEmbed', row.playable_in_embed),
|
|
62
|
+
...present('releaseTimestamp', row.release_timestamp),
|
|
63
|
+
...(Object.keys(music).length === 0 ? {} : { music }),
|
|
64
|
+
...(Object.keys(format).length === 0 ? {} : { video: format }),
|
|
126
65
|
wasLive: row.was_live ?? false,
|
|
127
66
|
chapters: (row.chapters ?? []).map((chapter) => ({
|
|
128
67
|
title: chapter.title,
|
|
@@ -147,15 +86,16 @@ export function toVideoStats(row) {
|
|
|
147
86
|
}),
|
|
148
87
|
descriptionChars: description.length,
|
|
149
88
|
description,
|
|
150
|
-
...(
|
|
151
|
-
? {}
|
|
152
|
-
: { thumbnailUrl: row.thumbnail }),
|
|
89
|
+
...present('thumbnailUrl', row.thumbnail),
|
|
153
90
|
};
|
|
154
91
|
}
|
|
155
92
|
/** One `-j` read, serving info, details, chapters, captions and the heatmap. */
|
|
156
|
-
export async function fetchVideoStatsRow(urlOrId) {
|
|
93
|
+
export async function fetchVideoStatsRow(urlOrId, options = {}) {
|
|
157
94
|
const videoId = extractVideoId(urlOrId);
|
|
158
|
-
const
|
|
95
|
+
const args = ['-j', '--skip-download'];
|
|
96
|
+
if (options.tolerateRefusal === true)
|
|
97
|
+
args.push('--ignore-no-formats-error');
|
|
98
|
+
const stdout = await runYtDlp(args, {
|
|
159
99
|
label: 'get_video_stats',
|
|
160
100
|
timeoutMs: TIMEOUTS.metadata,
|
|
161
101
|
target: watchUrl(videoId),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-stats.js","sourceRoot":"","sources":["../../src/utils/video-stats.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"video-stats.js","sourceRoot":"","sources":["../../src/utils/video-stats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handing a video's stream to ffmpeg without downloading the file.
|
|
3
|
+
*
|
|
4
|
+
* Every tool that looks at pixels (a still, a contact sheet, scene cuts) needs the
|
|
5
|
+
* same two guarantees, so they live in one place: the media URL really is an https
|
|
6
|
+
* URL, and ffmpeg is told it may open nothing else.
|
|
7
|
+
*/
|
|
8
|
+
/** Up to 1080p: enough to read small text in a still. */
|
|
9
|
+
export declare const DETAIL_STREAM_SELECTOR = "bestvideo[height<=1080]/best";
|
|
10
|
+
/** Up to 720p: plenty for frames shrunk to fit a model's context. */
|
|
11
|
+
export declare const PREVIEW_STREAM_SELECTOR = "bestvideo[height<=720]/best[height<=720]/best";
|
|
12
|
+
/** The lightest stream that still shows a cut: scene detection reads every frame. */
|
|
13
|
+
export declare const SCAN_STREAM_SELECTOR = "bestvideo[height<=360]/worst";
|
|
14
|
+
/**
|
|
15
|
+
* ffmpeg accepts file:, pipe:, concat: and more as inputs. The only thing yt-dlp
|
|
16
|
+
* should have resolved is an https media URL, and this allowlist makes ffmpeg refuse
|
|
17
|
+
* anything else even if it had. A per-input option, so it must precede `-i`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const FFMPEG_NETWORK_INPUT_ARGS: string[];
|
|
20
|
+
export declare function resolveStreamUrl(videoId: string, selector: string, label: string): Promise<string>;
|
|
21
|
+
//# sourceMappingURL=video-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-stream.d.ts","sourceRoot":"","sources":["../../src/utils/video-stream.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AAEH,yDAAyD;AACzD,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AACrE,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,kDAAkD,CAAC;AACvF,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,iCAAiC,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,UAAkD,CAAC;AAEzF,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAiBjB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { YouTubeError } from './errors.js';
|
|
2
|
+
import { runYtDlp } from './ytdlp.js';
|
|
3
|
+
/**
|
|
4
|
+
* Handing a video's stream to ffmpeg without downloading the file.
|
|
5
|
+
*
|
|
6
|
+
* Every tool that looks at pixels (a still, a contact sheet, scene cuts) needs the
|
|
7
|
+
* same two guarantees, so they live in one place: the media URL really is an https
|
|
8
|
+
* URL, and ffmpeg is told it may open nothing else.
|
|
9
|
+
*/
|
|
10
|
+
/** Up to 1080p: enough to read small text in a still. */
|
|
11
|
+
export const DETAIL_STREAM_SELECTOR = 'bestvideo[height<=1080]/best';
|
|
12
|
+
/** Up to 720p: plenty for frames shrunk to fit a model's context. */
|
|
13
|
+
export const PREVIEW_STREAM_SELECTOR = 'bestvideo[height<=720]/best[height<=720]/best';
|
|
14
|
+
/** The lightest stream that still shows a cut: scene detection reads every frame. */
|
|
15
|
+
export const SCAN_STREAM_SELECTOR = 'bestvideo[height<=360]/worst';
|
|
16
|
+
/**
|
|
17
|
+
* ffmpeg accepts file:, pipe:, concat: and more as inputs. The only thing yt-dlp
|
|
18
|
+
* should have resolved is an https media URL, and this allowlist makes ffmpeg refuse
|
|
19
|
+
* anything else even if it had. A per-input option, so it must precede `-i`.
|
|
20
|
+
*/
|
|
21
|
+
export const FFMPEG_NETWORK_INPUT_ARGS = ['-protocol_whitelist', 'https,tls,tcp,crypto'];
|
|
22
|
+
export async function resolveStreamUrl(videoId, selector, label) {
|
|
23
|
+
const mediaUrl = (await runYtDlp(['-f', selector, '--get-url', '--no-playlist'], {
|
|
24
|
+
label: `${label} (resolve)`,
|
|
25
|
+
target: `https://www.youtube.com/watch?v=${videoId}`,
|
|
26
|
+
}))
|
|
27
|
+
.trim()
|
|
28
|
+
.split('\n')[0];
|
|
29
|
+
if (!mediaUrl?.startsWith('https://')) {
|
|
30
|
+
throw new YouTubeError('YTDLP_FAILED', 'Could not resolve a video stream for this video.', {
|
|
31
|
+
nextStep: 'Check the video is playable, or call check_health.',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return mediaUrl;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=video-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-stream.js","sourceRoot":"","sources":["../../src/utils/video-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC;;;;;;GAMG;AAEH,yDAAyD;AACzD,MAAM,CAAC,MAAM,sBAAsB,GAAG,8BAA8B,CAAC;AACrE,qEAAqE;AACrE,MAAM,CAAC,MAAM,uBAAuB,GAAG,+CAA+C,CAAC;AACvF,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,8BAA8B,CAAC;AAEnE;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,qBAAqB,EAAE,sBAAsB,CAAC,CAAC;AAEzF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAe,EACf,QAAgB,EAChB,KAAa;IAEb,MAAM,QAAQ,GAAG,CACf,MAAM,QAAQ,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC,EAAE;QAC7D,KAAK,EAAE,GAAG,KAAK,YAAY;QAC3B,MAAM,EAAE,mCAAmC,OAAO,EAAE;KACrD,CAAC,CACH;SACE,IAAI,EAAE;SACN,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAElB,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,kDAAkD,EAAE;YACzF,QAAQ,EAAE,oDAAoD;SAC/D,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TranscriptSegment } from './transcript.js';
|
|
2
|
+
/**
|
|
3
|
+
* Transcribing audio on this machine, with whisper.cpp.
|
|
4
|
+
*
|
|
5
|
+
* For a video with no captions this is the only way to get words out of it. It is
|
|
6
|
+
* strictly optional, like ffmpeg: nothing here runs unless a person asks for a local
|
|
7
|
+
* transcription, and no model is downloaded for them. The model is a file they point
|
|
8
|
+
* `YOUTUBE_MCP_WHISPER_MODEL` at, because which one to run is a trade between speed,
|
|
9
|
+
* disk and accuracy that this server cannot make for them.
|
|
10
|
+
*/
|
|
11
|
+
/** The executable's name since whisper.cpp 1.7; earlier releases called it `main`. */
|
|
12
|
+
export declare const WHISPER_BINARY = "whisper-cli";
|
|
13
|
+
export interface WhisperStatus {
|
|
14
|
+
installed: boolean;
|
|
15
|
+
version?: string;
|
|
16
|
+
/** The path the person configured, whether or not a file is there. */
|
|
17
|
+
modelPath?: string;
|
|
18
|
+
modelFound: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function probeWhisper(): Promise<WhisperStatus>;
|
|
21
|
+
/** The model to run, or the typed reason there is nothing to run. */
|
|
22
|
+
export declare function requireWhisper(operation: string): Promise<string>;
|
|
23
|
+
/** whisper.cpp's JSON as segments in seconds; the silence marker is dropped. */
|
|
24
|
+
export declare function parseWhisperJson(raw: string): TranscriptSegment[];
|
|
25
|
+
/**
|
|
26
|
+
* The language as whisper.cpp names it: the primary subtag, lower-case.
|
|
27
|
+
*
|
|
28
|
+
* Callers pass BCP-47 tags such as `pt-BR` or `zh-Hans`, and whisper.cpp knows only `pt`
|
|
29
|
+
* and `zh` and exits on anything else (measured against whisper.cpp 1.9.5).
|
|
30
|
+
*/
|
|
31
|
+
export declare function whisperLanguage(tag: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Transcribe one audio file.
|
|
34
|
+
*
|
|
35
|
+
* The language is always explicit: left to auto-detect, a small model took English
|
|
36
|
+
* speech for French, and a transcript in the wrong language is worse than none.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runWhisper(audioPath: string, language: string, modelPath: string, outputBase: string): Promise<TranscriptSegment[]>;
|
|
39
|
+
//# sourceMappingURL=whisper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whisper.d.ts","sourceRoot":"","sources":["../../src/utils/whisper.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;;;;;;;GAQG;AAEH,sFAAsF;AACtF,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAM5C,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB;AAuBD,wBAAsB,YAAY,IAAI,OAAO,CAAC,aAAa,CAAC,CAY3D;AAED,qEAAqE;AACrE,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCvE;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,EAAE,CA0BjE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA8B9B"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { readFile, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { currentContext } from './context.js';
|
|
5
|
+
import { envString } from './env.js';
|
|
6
|
+
import { YouTubeError } from './errors.js';
|
|
7
|
+
import { MILLISECONDS_PER_SECOND } from './seconds.js';
|
|
8
|
+
/**
|
|
9
|
+
* Transcribing audio on this machine, with whisper.cpp.
|
|
10
|
+
*
|
|
11
|
+
* For a video with no captions this is the only way to get words out of it. It is
|
|
12
|
+
* strictly optional, like ffmpeg: nothing here runs unless a person asks for a local
|
|
13
|
+
* transcription, and no model is downloaded for them. The model is a file they point
|
|
14
|
+
* `YOUTUBE_MCP_WHISPER_MODEL` at, because which one to run is a trade between speed,
|
|
15
|
+
* disk and accuracy that this server cannot make for them.
|
|
16
|
+
*/
|
|
17
|
+
/** The executable's name since whisper.cpp 1.7; earlier releases called it `main`. */
|
|
18
|
+
export const WHISPER_BINARY = 'whisper-cli';
|
|
19
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
20
|
+
/** whisper.cpp writes this for a stretch with no speech; it is not part of what was said. */
|
|
21
|
+
const SILENCE_MARKER = '[BLANK_AUDIO]';
|
|
22
|
+
const whisperOutputSchema = z.object({
|
|
23
|
+
transcription: z.array(z.object({
|
|
24
|
+
offsets: z.object({ from: z.number(), to: z.number() }),
|
|
25
|
+
text: z.string(),
|
|
26
|
+
})),
|
|
27
|
+
});
|
|
28
|
+
function configuredModel() {
|
|
29
|
+
return envString('YOUTUBE_MCP_WHISPER_MODEL');
|
|
30
|
+
}
|
|
31
|
+
async function isFile(path) {
|
|
32
|
+
try {
|
|
33
|
+
return (await stat(path)).isFile();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function probeWhisper() {
|
|
40
|
+
const modelPath = configuredModel();
|
|
41
|
+
const modelFound = modelPath !== undefined && (await isFile(modelPath));
|
|
42
|
+
const model = modelPath === undefined ? {} : { modelPath };
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execa(WHISPER_BINARY, ['--version'], { timeout: PROBE_TIMEOUT_MS });
|
|
45
|
+
const version = /version:\s*(\S+)/.exec(stdout)?.[1] ?? 'unknown';
|
|
46
|
+
return { installed: true, version, ...model, modelFound };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { installed: false, ...model, modelFound };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** The model to run, or the typed reason there is nothing to run. */
|
|
53
|
+
export async function requireWhisper(operation) {
|
|
54
|
+
const status = await probeWhisper();
|
|
55
|
+
if (!status.installed) {
|
|
56
|
+
throw new YouTubeError('WHISPER_MISSING', `${operation} requires whisper.cpp, which is not installed.`, {
|
|
57
|
+
nextStep: 'Install whisper.cpp (macOS: `brew install whisper.cpp`; elsewhere build it from https://github.com/ggml-org/whisper.cpp) so that `whisper-cli` is on PATH, then call check_health.',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (status.modelPath === undefined) {
|
|
61
|
+
throw new YouTubeError('WHISPER_MISSING', `${operation} needs a whisper model, and none is configured.`, {
|
|
62
|
+
nextStep: 'Download a ggml model file (for example ggml-base.bin from https://huggingface.co/ggerganov/whisper.cpp) and set YOUTUBE_MCP_WHISPER_MODEL to its path.',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!status.modelFound) {
|
|
66
|
+
throw new YouTubeError('WHISPER_MISSING', `The whisper model at ${status.modelPath} is not a readable file.`, { nextStep: 'Fix the path in YOUTUBE_MCP_WHISPER_MODEL, then call check_health.' });
|
|
67
|
+
}
|
|
68
|
+
return status.modelPath;
|
|
69
|
+
}
|
|
70
|
+
/** whisper.cpp's JSON as segments in seconds; the silence marker is dropped. */
|
|
71
|
+
export function parseWhisperJson(raw) {
|
|
72
|
+
let json;
|
|
73
|
+
try {
|
|
74
|
+
json = JSON.parse(raw);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new YouTubeError('MALFORMED_RESPONSE', 'whisper.cpp wrote output that is not JSON.', {
|
|
78
|
+
cause: error,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const parsed = whisperOutputSchema.safeParse(json);
|
|
82
|
+
if (!parsed.success) {
|
|
83
|
+
throw new YouTubeError('MALFORMED_RESPONSE', 'whisper.cpp wrote JSON without a transcription.', { cause: parsed.error });
|
|
84
|
+
}
|
|
85
|
+
return parsed.data.transcription
|
|
86
|
+
.map(({ offsets, text }) => ({
|
|
87
|
+
start: offsets.from / MILLISECONDS_PER_SECOND,
|
|
88
|
+
end: offsets.to / MILLISECONDS_PER_SECOND,
|
|
89
|
+
text: text.trim(),
|
|
90
|
+
}))
|
|
91
|
+
.filter((segment) => segment.text !== '' && segment.text !== SILENCE_MARKER);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The language as whisper.cpp names it: the primary subtag, lower-case.
|
|
95
|
+
*
|
|
96
|
+
* Callers pass BCP-47 tags such as `pt-BR` or `zh-Hans`, and whisper.cpp knows only `pt`
|
|
97
|
+
* and `zh` and exits on anything else (measured against whisper.cpp 1.9.5).
|
|
98
|
+
*/
|
|
99
|
+
export function whisperLanguage(tag) {
|
|
100
|
+
return (tag.split('-')[0] ?? tag).toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Transcribe one audio file.
|
|
104
|
+
*
|
|
105
|
+
* The language is always explicit: left to auto-detect, a small model took English
|
|
106
|
+
* speech for French, and a transcript in the wrong language is worse than none.
|
|
107
|
+
*/
|
|
108
|
+
export async function runWhisper(audioPath, language, modelPath, outputBase) {
|
|
109
|
+
const outputPath = `${outputBase}.json`;
|
|
110
|
+
try {
|
|
111
|
+
await execa(WHISPER_BINARY, [
|
|
112
|
+
'-m',
|
|
113
|
+
modelPath,
|
|
114
|
+
'-f',
|
|
115
|
+
audioPath,
|
|
116
|
+
'-l',
|
|
117
|
+
whisperLanguage(language),
|
|
118
|
+
'-oj',
|
|
119
|
+
'-of',
|
|
120
|
+
outputBase,
|
|
121
|
+
'-np',
|
|
122
|
+
], { cancelSignal: currentContext().signal });
|
|
123
|
+
return parseWhisperJson(await readFile(outputPath, 'utf-8'));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error instanceof YouTubeError)
|
|
127
|
+
throw error;
|
|
128
|
+
throw new YouTubeError('YTDLP_FAILED', 'whisper.cpp could not transcribe this audio.', {
|
|
129
|
+
nextStep: 'Check that the model file is a valid ggml model, or call check_health.',
|
|
130
|
+
cause: error,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await rm(outputPath, { force: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=whisper.js.map
|