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.
Files changed (267) hide show
  1. package/README.md +87 -41
  2. package/dist/brain-schemas.d.ts +10 -0
  3. package/dist/brain-schemas.d.ts.map +1 -1
  4. package/dist/harvest-schemas.d.ts +6 -6
  5. package/dist/harvest-schemas.d.ts.map +1 -1
  6. package/dist/harvest-schemas.js +5 -0
  7. package/dist/harvest-schemas.js.map +1 -1
  8. package/dist/registry/brain-tools.d.ts.map +1 -1
  9. package/dist/registry/brain-tools.js +16 -0
  10. package/dist/registry/brain-tools.js.map +1 -1
  11. package/dist/registry/channel-tools.d.ts.map +1 -1
  12. package/dist/registry/channel-tools.js +16 -0
  13. package/dist/registry/channel-tools.js.map +1 -1
  14. package/dist/registry/index.d.ts.map +1 -1
  15. package/dist/registry/index.js +4 -0
  16. package/dist/registry/index.js.map +1 -1
  17. package/dist/registry/media-tools.d.ts.map +1 -1
  18. package/dist/registry/media-tools.js +31 -0
  19. package/dist/registry/media-tools.js.map +1 -1
  20. package/dist/registry/store-tools.d.ts +9 -0
  21. package/dist/registry/store-tools.d.ts.map +1 -0
  22. package/dist/registry/store-tools.js +59 -0
  23. package/dist/registry/store-tools.js.map +1 -0
  24. package/dist/registry/transcript-tools.d.ts +4 -1
  25. package/dist/registry/transcript-tools.d.ts.map +1 -1
  26. package/dist/registry/transcript-tools.js +20 -1
  27. package/dist/registry/transcript-tools.js.map +1 -1
  28. package/dist/registry/video-tools.d.ts.map +1 -1
  29. package/dist/registry/video-tools.js +48 -0
  30. package/dist/registry/video-tools.js.map +1 -1
  31. package/dist/registry/vision-tools.d.ts +7 -0
  32. package/dist/registry/vision-tools.d.ts.map +1 -0
  33. package/dist/registry/vision-tools.js +55 -0
  34. package/dist/registry/vision-tools.js.map +1 -0
  35. package/dist/schemas.d.ts +6 -0
  36. package/dist/schemas.d.ts.map +1 -1
  37. package/dist/schemas.js +2 -0
  38. package/dist/schemas.js.map +1 -1
  39. package/dist/thumbnail-schemas.d.ts +10 -0
  40. package/dist/thumbnail-schemas.d.ts.map +1 -1
  41. package/dist/tools/ask-brains.d.ts +66 -0
  42. package/dist/tools/ask-brains.d.ts.map +1 -0
  43. package/dist/tools/ask-brains.js +111 -0
  44. package/dist/tools/ask-brains.js.map +1 -0
  45. package/dist/tools/check-health.d.ts +16 -6
  46. package/dist/tools/check-health.d.ts.map +1 -1
  47. package/dist/tools/check-health.js +30 -1
  48. package/dist/tools/check-health.js.map +1 -1
  49. package/dist/tools/check-new-uploads.d.ts +55 -0
  50. package/dist/tools/check-new-uploads.d.ts.map +1 -0
  51. package/dist/tools/check-new-uploads.js +74 -0
  52. package/dist/tools/check-new-uploads.js.map +1 -0
  53. package/dist/tools/clips.d.ts +15 -0
  54. package/dist/tools/clips.d.ts.map +1 -1
  55. package/dist/tools/clips.js +5 -5
  56. package/dist/tools/clips.js.map +1 -1
  57. package/dist/tools/detect-scenes.d.ts +38 -0
  58. package/dist/tools/detect-scenes.d.ts.map +1 -0
  59. package/dist/tools/detect-scenes.js +66 -0
  60. package/dist/tools/detect-scenes.js.map +1 -0
  61. package/dist/tools/export-data.d.ts +85 -0
  62. package/dist/tools/export-data.d.ts.map +1 -0
  63. package/dist/tools/export-data.js +99 -0
  64. package/dist/tools/export-data.js.map +1 -0
  65. package/dist/tools/frames-subtitles.d.ts.map +1 -1
  66. package/dist/tools/frames-subtitles.js +16 -8
  67. package/dist/tools/frames-subtitles.js.map +1 -1
  68. package/dist/tools/frames.d.ts +73 -0
  69. package/dist/tools/frames.d.ts.map +1 -0
  70. package/dist/tools/frames.js +136 -0
  71. package/dist/tools/frames.js.map +1 -0
  72. package/dist/tools/get-channel-info.d.ts +2 -0
  73. package/dist/tools/get-channel-info.d.ts.map +1 -1
  74. package/dist/tools/get-channel-info.js +4 -0
  75. package/dist/tools/get-channel-info.js.map +1 -1
  76. package/dist/tools/get-comments.d.ts +5 -5
  77. package/dist/tools/get-coverage.d.ts +5 -5
  78. package/dist/tools/get-sponsor-segments.d.ts +49 -0
  79. package/dist/tools/get-sponsor-segments.d.ts.map +1 -0
  80. package/dist/tools/get-sponsor-segments.js +61 -0
  81. package/dist/tools/get-sponsor-segments.js.map +1 -0
  82. package/dist/tools/get-thumbnail.d.ts.map +1 -1
  83. package/dist/tools/get-thumbnail.js +2 -1
  84. package/dist/tools/get-thumbnail.js.map +1 -1
  85. package/dist/tools/get-transcript.d.ts +1 -0
  86. package/dist/tools/get-transcript.d.ts.map +1 -1
  87. package/dist/tools/get-transcript.js +11 -4
  88. package/dist/tools/get-transcript.js.map +1 -1
  89. package/dist/tools/get-video-details.d.ts +118 -0
  90. package/dist/tools/get-video-details.d.ts.map +1 -0
  91. package/dist/tools/get-video-details.js +147 -0
  92. package/dist/tools/get-video-details.js.map +1 -0
  93. package/dist/tools/gif-concat.d.ts +49 -0
  94. package/dist/tools/gif-concat.d.ts.map +1 -0
  95. package/dist/tools/gif-concat.js +96 -0
  96. package/dist/tools/gif-concat.js.map +1 -0
  97. package/dist/tools/harvest-channel.d.ts +5 -5
  98. package/dist/tools/harvest-comments.d.ts +5 -5
  99. package/dist/tools/harvest-details.d.ts +84 -0
  100. package/dist/tools/harvest-details.d.ts.map +1 -0
  101. package/dist/tools/harvest-details.js +71 -0
  102. package/dist/tools/harvest-details.js.map +1 -0
  103. package/dist/tools/list-captions.d.ts +17 -0
  104. package/dist/tools/list-captions.d.ts.map +1 -0
  105. package/dist/tools/list-captions.js +60 -0
  106. package/dist/tools/list-captions.js.map +1 -0
  107. package/dist/tools/list-channel-playlists.d.ts +30 -0
  108. package/dist/tools/list-channel-playlists.d.ts.map +1 -0
  109. package/dist/tools/list-channel-playlists.js +44 -0
  110. package/dist/tools/list-channel-playlists.js.map +1 -0
  111. package/dist/tools/query-comments.d.ts +5 -5
  112. package/dist/tools/query-videos.d.ts +5 -5
  113. package/dist/tools/search-channels.d.ts +6 -0
  114. package/dist/tools/search-channels.d.ts.map +1 -1
  115. package/dist/tools/transcribe-video.d.ts +28 -0
  116. package/dist/tools/transcribe-video.d.ts.map +1 -0
  117. package/dist/tools/transcribe-video.js +52 -0
  118. package/dist/tools/transcribe-video.js.map +1 -0
  119. package/dist/utils/brain-fusion.d.ts +24 -0
  120. package/dist/utils/brain-fusion.d.ts.map +1 -0
  121. package/dist/utils/brain-fusion.js +18 -0
  122. package/dist/utils/brain-fusion.js.map +1 -0
  123. package/dist/utils/channel-playlists.d.ts +21 -0
  124. package/dist/utils/channel-playlists.d.ts.map +1 -0
  125. package/dist/utils/channel-playlists.js +56 -0
  126. package/dist/utils/channel-playlists.js.map +1 -0
  127. package/dist/utils/clips.d.ts.map +1 -1
  128. package/dist/utils/clips.js +3 -17
  129. package/dist/utils/clips.js.map +1 -1
  130. package/dist/utils/concat.d.ts +19 -0
  131. package/dist/utils/concat.d.ts.map +1 -0
  132. package/dist/utils/concat.js +135 -0
  133. package/dist/utils/concat.js.map +1 -0
  134. package/dist/utils/contact-sheet.d.ts +28 -0
  135. package/dist/utils/contact-sheet.d.ts.map +1 -0
  136. package/dist/utils/contact-sheet.js +65 -0
  137. package/dist/utils/contact-sheet.js.map +1 -0
  138. package/dist/utils/detail-batch.d.ts +25 -0
  139. package/dist/utils/detail-batch.d.ts.map +1 -0
  140. package/dist/utils/detail-batch.js +58 -0
  141. package/dist/utils/detail-batch.js.map +1 -0
  142. package/dist/utils/errors.d.ts +1 -1
  143. package/dist/utils/errors.d.ts.map +1 -1
  144. package/dist/utils/errors.js +10 -0
  145. package/dist/utils/errors.js.map +1 -1
  146. package/dist/utils/export-data.d.ts +41 -0
  147. package/dist/utils/export-data.d.ts.map +1 -0
  148. package/dist/utils/export-data.js +114 -0
  149. package/dist/utils/export-data.js.map +1 -0
  150. package/dist/utils/export-format.d.ts +23 -0
  151. package/dist/utils/export-format.d.ts.map +1 -0
  152. package/dist/utils/export-format.js +59 -0
  153. package/dist/utils/export-format.js.map +1 -0
  154. package/dist/utils/export-load.d.ts +28 -0
  155. package/dist/utils/export-load.d.ts.map +1 -0
  156. package/dist/utils/export-load.js +70 -0
  157. package/dist/utils/export-load.js.map +1 -0
  158. package/dist/utils/export-rows.d.ts +35 -0
  159. package/dist/utils/export-rows.d.ts.map +1 -0
  160. package/dist/utils/export-rows.js +69 -0
  161. package/dist/utils/export-rows.js.map +1 -0
  162. package/dist/utils/ffmpeg.d.ts +26 -0
  163. package/dist/utils/ffmpeg.d.ts.map +1 -0
  164. package/dist/utils/ffmpeg.js +28 -0
  165. package/dist/utils/ffmpeg.js.map +1 -0
  166. package/dist/utils/format.d.ts +6 -0
  167. package/dist/utils/format.d.ts.map +1 -1
  168. package/dist/utils/format.js +4 -0
  169. package/dist/utils/format.js.map +1 -1
  170. package/dist/utils/frame-plan.d.ts +24 -0
  171. package/dist/utils/frame-plan.d.ts.map +1 -0
  172. package/dist/utils/frame-plan.js +58 -0
  173. package/dist/utils/frame-plan.js.map +1 -0
  174. package/dist/utils/frames.d.ts +32 -0
  175. package/dist/utils/frames.d.ts.map +1 -0
  176. package/dist/utils/frames.js +60 -0
  177. package/dist/utils/frames.js.map +1 -0
  178. package/dist/utils/gif.d.ts +31 -0
  179. package/dist/utils/gif.d.ts.map +1 -0
  180. package/dist/utils/gif.js +77 -0
  181. package/dist/utils/gif.js.map +1 -0
  182. package/dist/utils/harvest-catalog.d.ts +10 -0
  183. package/dist/utils/harvest-catalog.d.ts.map +1 -1
  184. package/dist/utils/harvest-catalog.js +14 -5
  185. package/dist/utils/harvest-catalog.js.map +1 -1
  186. package/dist/utils/harvest-details.d.ts +30 -0
  187. package/dist/utils/harvest-details.d.ts.map +1 -0
  188. package/dist/utils/harvest-details.js +98 -0
  189. package/dist/utils/harvest-details.js.map +1 -0
  190. package/dist/utils/local-transcription.d.ts +14 -0
  191. package/dist/utils/local-transcription.d.ts.map +1 -0
  192. package/dist/utils/local-transcription.js +85 -0
  193. package/dist/utils/local-transcription.js.map +1 -0
  194. package/dist/utils/moment.d.ts +24 -0
  195. package/dist/utils/moment.d.ts.map +1 -0
  196. package/dist/utils/moment.js +22 -0
  197. package/dist/utils/moment.js.map +1 -0
  198. package/dist/utils/most-replayed.d.ts +15 -0
  199. package/dist/utils/most-replayed.d.ts.map +1 -0
  200. package/dist/utils/most-replayed.js +25 -0
  201. package/dist/utils/most-replayed.js.map +1 -0
  202. package/dist/utils/scenes.d.ts +39 -0
  203. package/dist/utils/scenes.d.ts.map +1 -0
  204. package/dist/utils/scenes.js +83 -0
  205. package/dist/utils/scenes.js.map +1 -0
  206. package/dist/utils/seconds.d.ts +5 -0
  207. package/dist/utils/seconds.d.ts.map +1 -0
  208. package/dist/utils/seconds.js +7 -0
  209. package/dist/utils/seconds.js.map +1 -0
  210. package/dist/utils/sponsorblock.d.ts +24 -0
  211. package/dist/utils/sponsorblock.d.ts.map +1 -0
  212. package/dist/utils/sponsorblock.js +153 -0
  213. package/dist/utils/sponsorblock.js.map +1 -0
  214. package/dist/utils/transcript-cache.d.ts +5 -1
  215. package/dist/utils/transcript-cache.d.ts.map +1 -1
  216. package/dist/utils/transcript-cache.js +31 -50
  217. package/dist/utils/transcript-cache.js.map +1 -1
  218. package/dist/utils/transcript-files.d.ts +24 -0
  219. package/dist/utils/transcript-files.d.ts.map +1 -0
  220. package/dist/utils/transcript-files.js +61 -0
  221. package/dist/utils/transcript-files.js.map +1 -0
  222. package/dist/utils/transcript.d.ts +3 -0
  223. package/dist/utils/transcript.d.ts.map +1 -1
  224. package/dist/utils/transcript.js +2 -0
  225. package/dist/utils/transcript.js.map +1 -1
  226. package/dist/utils/upload-check.d.ts +38 -0
  227. package/dist/utils/upload-check.d.ts.map +1 -0
  228. package/dist/utils/upload-check.js +93 -0
  229. package/dist/utils/upload-check.js.map +1 -0
  230. package/dist/utils/validate.d.ts +2 -0
  231. package/dist/utils/validate.d.ts.map +1 -1
  232. package/dist/utils/validate.js +4 -0
  233. package/dist/utils/validate.js.map +1 -1
  234. package/dist/utils/video-detail-store.d.ts +47 -0
  235. package/dist/utils/video-detail-store.d.ts.map +1 -0
  236. package/dist/utils/video-detail-store.js +89 -0
  237. package/dist/utils/video-detail-store.js.map +1 -0
  238. package/dist/utils/video-stats-cache.js +1 -1
  239. package/dist/utils/video-stats-schema.d.ts +187 -0
  240. package/dist/utils/video-stats-schema.d.ts.map +1 -0
  241. package/dist/utils/video-stats-schema.js +50 -0
  242. package/dist/utils/video-stats-schema.js.map +1 -0
  243. package/dist/utils/video-stats-storage.d.ts +16 -0
  244. package/dist/utils/video-stats-storage.d.ts.map +1 -0
  245. package/dist/utils/video-stats-storage.js +37 -0
  246. package/dist/utils/video-stats-storage.js.map +1 -0
  247. package/dist/utils/video-stats.d.ts +23 -127
  248. package/dist/utils/video-stats.d.ts.map +1 -1
  249. package/dist/utils/video-stats.js +42 -102
  250. package/dist/utils/video-stats.js.map +1 -1
  251. package/dist/utils/video-stream.d.ts +21 -0
  252. package/dist/utils/video-stream.d.ts.map +1 -0
  253. package/dist/utils/video-stream.js +36 -0
  254. package/dist/utils/video-stream.js.map +1 -0
  255. package/dist/utils/whisper.d.ts +39 -0
  256. package/dist/utils/whisper.d.ts.map +1 -0
  257. package/dist/utils/whisper.js +137 -0
  258. package/dist/utils/whisper.js.map +1 -0
  259. package/dist/utils/youtube-channel.d.ts +4 -0
  260. package/dist/utils/youtube-channel.d.ts.map +1 -1
  261. package/dist/utils/youtube-channel.js +6 -0
  262. package/dist/utils/youtube-channel.js.map +1 -1
  263. package/dist/utils/youtube-schemas.d.ts +6 -0
  264. package/dist/utils/youtube-schemas.d.ts.map +1 -1
  265. package/dist/utils/youtube-schemas.js +2 -0
  266. package/dist/utils/youtube-schemas.js.map +1 -1
  267. 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): Promise<z.infer<typeof videoStatsRowSchema>>;
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;AAKxB;;;;;;;;;;;;;GAaG;AAEH,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,2EAA2E;AAC3E,eAAO,MAAM,aAAa,mHAQhB,CAAC;AAQX,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4B9B,CAAC;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;CACvB;AAED,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAErD,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcrF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,GAAG,UAAU,CA0EjF;AAED,gFAAgF;AAChF,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAS9C;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAExE"}
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
- * One full read of a video, serving every caller that needs part of it.
7
- *
8
- * `getVideoInfo` ran a narrow `--print` and `getVideoDetails` ran a full `-j`
9
- * and discarded all but three fields, so a single video cost two spawns for
10
- * data one already had. Requests to YouTube are the thing that gets a client
11
- * throttled, so halving them is the cheapest reliability win available.
12
- *
13
- * What is NOT kept matters as much as what is. Measured on a 637KB payload:
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
- ...(row.channel_id === null || row.channel_id === undefined
100
- ? {}
101
- : { channelId: row.channel_id }),
102
- ...(row.channel_follower_count === null || row.channel_follower_count === undefined
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
- ...(row.view_count === null || row.view_count === undefined
109
- ? {}
110
- : { viewCount: row.view_count }),
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
- ...(row.availability === null || row.availability === undefined
121
- ? {}
122
- : { availability: row.availability }),
123
- ...(row.live_status === null || row.live_status === undefined
124
- ? {}
125
- : { liveStatus: row.live_status }),
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
- ...(row.thumbnail === null || row.thumbnail === undefined
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 stdout = await runYtDlp(['-j', '--skip-download'], {
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":"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"}
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