youtube-knowledge-mcp 2.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -33
- package/dist/brain-schemas.d.ts +502 -0
- package/dist/brain-schemas.d.ts.map +1 -0
- package/dist/brain-schemas.js +109 -0
- package/dist/brain-schemas.js.map +1 -0
- package/dist/http/app.d.ts +16 -0
- package/dist/http/app.d.ts.map +1 -0
- package/dist/http/app.js +176 -0
- package/dist/http/app.js.map +1 -0
- package/dist/http/auth.d.ts +38 -0
- package/dist/http/auth.d.ts.map +1 -0
- package/dist/http/auth.js +79 -0
- package/dist/http/auth.js.map +1 -0
- package/dist/http/config.d.ts +41 -0
- package/dist/http/config.d.ts.map +1 -0
- package/dist/http/config.js +65 -0
- package/dist/http/config.js.map +1 -0
- package/dist/http/rate-limiter.d.ts +24 -0
- package/dist/http/rate-limiter.d.ts.map +1 -0
- package/dist/http/rate-limiter.js +55 -0
- package/dist/http/rate-limiter.js.map +1 -0
- package/dist/http/sessions.d.ts +34 -0
- package/dist/http/sessions.d.ts.map +1 -0
- package/dist/http/sessions.js +62 -0
- package/dist/http/sessions.js.map +1 -0
- package/dist/http.d.ts +6 -48
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +35 -252
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -431
- package/dist/index.js.map +1 -1
- package/dist/prompts-brain.d.ts +4 -0
- package/dist/prompts-brain.d.ts.map +1 -0
- package/dist/prompts-brain.js +48 -0
- package/dist/prompts-brain.js.map +1 -0
- package/dist/prompts-research.d.ts +4 -0
- package/dist/prompts-research.d.ts.map +1 -0
- package/dist/prompts-research.js +74 -0
- package/dist/prompts-research.js.map +1 -0
- package/dist/prompts-shared.d.ts +13 -0
- package/dist/prompts-shared.d.ts.map +1 -0
- package/dist/prompts-shared.js +7 -0
- package/dist/prompts-shared.js.map +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +5 -58
- package/dist/prompts.js.map +1 -1
- package/dist/registry/brain-tools.d.ts +4 -0
- package/dist/registry/brain-tools.d.ts.map +1 -0
- package/dist/registry/brain-tools.js +104 -0
- package/dist/registry/brain-tools.js.map +1 -0
- package/dist/registry/channel-tools.d.ts +4 -0
- package/dist/registry/channel-tools.d.ts.map +1 -0
- package/dist/registry/channel-tools.js +53 -0
- package/dist/registry/channel-tools.js.map +1 -0
- package/dist/registry/health-tools.d.ts +4 -0
- package/dist/registry/health-tools.d.ts.map +1 -0
- package/dist/registry/health-tools.js +21 -0
- package/dist/registry/health-tools.js.map +1 -0
- package/dist/registry/index.d.ts +8 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +27 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/library-tools.d.ts +4 -0
- package/dist/registry/library-tools.d.ts.map +1 -0
- package/dist/registry/library-tools.js +116 -0
- package/dist/registry/library-tools.js.map +1 -0
- package/dist/registry/media-tools.d.ts +4 -0
- package/dist/registry/media-tools.d.ts.map +1 -0
- package/dist/registry/media-tools.js +100 -0
- package/dist/registry/media-tools.js.map +1 -0
- package/dist/registry/thumbnail-tools.d.ts +7 -0
- package/dist/registry/thumbnail-tools.d.ts.map +1 -0
- package/dist/registry/thumbnail-tools.js +72 -0
- package/dist/registry/thumbnail-tools.js.map +1 -0
- package/dist/registry/transcript-tools.d.ts +4 -0
- package/dist/registry/transcript-tools.d.ts.map +1 -0
- package/dist/registry/transcript-tools.js +68 -0
- package/dist/registry/transcript-tools.js.map +1 -0
- package/dist/registry/types.d.ts +41 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +17 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/registry/video-tools.d.ts +4 -0
- package/dist/registry/video-tools.d.ts.map +1 -0
- package/dist/registry/video-tools.js +101 -0
- package/dist/registry/video-tools.js.map +1 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +75 -6
- package/dist/resources.js.map +1 -1
- package/dist/schemas.d.ts +74 -54
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +30 -4
- package/dist/schemas.js.map +1 -1
- package/dist/thumbnail-resource.d.ts +8 -0
- package/dist/thumbnail-resource.d.ts.map +1 -0
- package/dist/thumbnail-resource.js +38 -0
- package/dist/thumbnail-resource.js.map +1 -0
- package/dist/thumbnail-schemas.d.ts +512 -0
- package/dist/thumbnail-schemas.d.ts.map +1 -0
- package/dist/thumbnail-schemas.js +95 -0
- package/dist/thumbnail-schemas.js.map +1 -0
- package/dist/tools/ask-brain.d.ts +62 -0
- package/dist/tools/ask-brain.d.ts.map +1 -0
- package/dist/tools/ask-brain.js +70 -0
- package/dist/tools/ask-brain.js.map +1 -0
- package/dist/tools/batch.d.ts +10 -10
- package/dist/tools/batch.js +3 -3
- package/dist/tools/batch.js.map +1 -1
- package/dist/tools/brain-info.d.ts +180 -0
- package/dist/tools/brain-info.d.ts.map +1 -0
- package/dist/tools/brain-info.js +108 -0
- package/dist/tools/brain-info.js.map +1 -0
- package/dist/tools/build-brain.d.ts +120 -0
- package/dist/tools/build-brain.d.ts.map +1 -0
- package/dist/tools/build-brain.js +123 -0
- package/dist/tools/build-brain.js.map +1 -0
- package/dist/tools/check-health.d.ts +2 -0
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +9 -0
- package/dist/tools/check-health.js.map +1 -1
- package/dist/tools/clips.d.ts +13 -45
- package/dist/tools/clips.d.ts.map +1 -1
- package/dist/tools/clips.js +52 -128
- package/dist/tools/clips.js.map +1 -1
- package/dist/tools/fetch-channel-thumbnails.d.ts +179 -0
- package/dist/tools/fetch-channel-thumbnails.d.ts.map +1 -0
- package/dist/tools/fetch-channel-thumbnails.js +123 -0
- package/dist/tools/fetch-channel-thumbnails.js.map +1 -0
- package/dist/tools/fetch-videos.d.ts +11 -8
- package/dist/tools/fetch-videos.d.ts.map +1 -1
- package/dist/tools/fetch-videos.js +3 -0
- package/dist/tools/fetch-videos.js.map +1 -1
- package/dist/tools/frames-subtitles.d.ts +44 -0
- package/dist/tools/frames-subtitles.d.ts.map +1 -0
- package/dist/tools/frames-subtitles.js +104 -0
- package/dist/tools/frames-subtitles.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 +8 -1
- package/dist/tools/get-channel-info.js.map +1 -1
- package/dist/tools/get-chapters.d.ts +2 -2
- package/dist/tools/get-comments.d.ts +2 -2
- package/dist/tools/get-playlist-info.d.ts.map +1 -1
- package/dist/tools/get-playlist-info.js +1 -0
- package/dist/tools/get-playlist-info.js.map +1 -1
- package/dist/tools/get-thumbnail.d.ts +38 -0
- package/dist/tools/get-thumbnail.d.ts.map +1 -0
- package/dist/tools/get-thumbnail.js +152 -0
- package/dist/tools/get-thumbnail.js.map +1 -0
- package/dist/tools/get-transcript.d.ts +2 -2
- package/dist/tools/get-transcript.d.ts.map +1 -1
- package/dist/tools/get-transcript.js +2 -14
- package/dist/tools/get-transcript.js.map +1 -1
- package/dist/tools/library.d.ts +9 -7
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +23 -7
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/list-channel-thumbnails.d.ts +235 -0
- package/dist/tools/list-channel-thumbnails.d.ts.map +1 -0
- package/dist/tools/list-channel-thumbnails.js +79 -0
- package/dist/tools/list-channel-thumbnails.js.map +1 -0
- package/dist/tools/list-library.d.ts +4 -4
- package/dist/tools/manage-brain.d.ts +39 -0
- package/dist/tools/manage-brain.d.ts.map +1 -0
- package/dist/tools/manage-brain.js +53 -0
- package/dist/tools/manage-brain.js.map +1 -0
- package/dist/tools/save-to-library.d.ts.map +1 -1
- package/dist/tools/save-to-library.js +5 -1
- package/dist/tools/save-to-library.js.map +1 -1
- package/dist/tools/search-channels.d.ts +10 -4
- package/dist/tools/search-channels.d.ts.map +1 -1
- package/dist/tools/search-transcript.d.ts +2 -2
- package/dist/tools/search-transcript.d.ts.map +1 -1
- package/dist/tools/search-transcript.js +6 -13
- package/dist/tools/search-transcript.js.map +1 -1
- package/dist/tools/search-videos.d.ts +11 -8
- package/dist/tools/search-videos.d.ts.map +1 -1
- package/dist/utils/batches.d.ts +15 -0
- package/dist/utils/batches.d.ts.map +1 -0
- package/dist/utils/batches.js +31 -0
- package/dist/utils/batches.js.map +1 -0
- package/dist/utils/brain-build.d.ts +40 -0
- package/dist/utils/brain-build.d.ts.map +1 -0
- package/dist/utils/brain-build.js +200 -0
- package/dist/utils/brain-build.js.map +1 -0
- package/dist/utils/brain-chunks.d.ts +33 -0
- package/dist/utils/brain-chunks.d.ts.map +1 -0
- package/dist/utils/brain-chunks.js +87 -0
- package/dist/utils/brain-chunks.js.map +1 -0
- package/dist/utils/brain-index.d.ts +27 -0
- package/dist/utils/brain-index.d.ts.map +1 -0
- package/dist/utils/brain-index.js +101 -0
- package/dist/utils/brain-index.js.map +1 -0
- package/dist/utils/brain-ingest.d.ts +57 -0
- package/dist/utils/brain-ingest.d.ts.map +1 -0
- package/dist/utils/brain-ingest.js +127 -0
- package/dist/utils/brain-ingest.js.map +1 -0
- package/dist/utils/brain-lock.d.ts +4 -0
- package/dist/utils/brain-lock.d.ts.map +1 -0
- package/dist/utils/brain-lock.js +17 -0
- package/dist/utils/brain-lock.js.map +1 -0
- package/dist/utils/brain-lookup.d.ts +11 -0
- package/dist/utils/brain-lookup.d.ts.map +1 -0
- package/dist/utils/brain-lookup.js +36 -0
- package/dist/utils/brain-lookup.js.map +1 -0
- package/dist/utils/brain-paths.d.ts +19 -0
- package/dist/utils/brain-paths.d.ts.map +1 -0
- package/dist/utils/brain-paths.js +34 -0
- package/dist/utils/brain-paths.js.map +1 -0
- package/dist/utils/brain-phrases.d.ts +31 -0
- package/dist/utils/brain-phrases.d.ts.map +1 -0
- package/dist/utils/brain-phrases.js +116 -0
- package/dist/utils/brain-phrases.js.map +1 -0
- package/dist/utils/brain-stats.d.ts +25 -0
- package/dist/utils/brain-stats.d.ts.map +1 -0
- package/dist/utils/brain-stats.js +64 -0
- package/dist/utils/brain-stats.js.map +1 -0
- package/dist/utils/brain-storage.d.ts +28 -0
- package/dist/utils/brain-storage.d.ts.map +1 -0
- package/dist/utils/brain-storage.js +87 -0
- package/dist/utils/brain-storage.js.map +1 -0
- package/dist/utils/caption-probe.d.ts +7 -0
- package/dist/utils/caption-probe.d.ts.map +1 -0
- package/dist/utils/caption-probe.js +36 -0
- package/dist/utils/caption-probe.js.map +1 -0
- package/dist/utils/channel-images.d.ts +16 -0
- package/dist/utils/channel-images.d.ts.map +1 -0
- package/dist/utils/channel-images.js +35 -0
- package/dist/utils/channel-images.js.map +1 -0
- package/dist/utils/channel-lookup.d.ts +5 -0
- package/dist/utils/channel-lookup.d.ts.map +1 -0
- package/dist/utils/channel-lookup.js +37 -0
- package/dist/utils/channel-lookup.js.map +1 -0
- package/dist/utils/chapters.d.ts +11 -0
- package/dist/utils/chapters.d.ts.map +1 -0
- package/dist/utils/chapters.js +24 -0
- package/dist/utils/chapters.js.map +1 -0
- package/dist/utils/clips.d.ts.map +1 -1
- package/dist/utils/clips.js +22 -30
- package/dist/utils/clips.js.map +1 -1
- package/dist/utils/env.d.ts +26 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +72 -0
- package/dist/utils/env.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 +20 -2
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/file-lock.d.ts +34 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +91 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/flat-listing.d.ts +89 -0
- package/dist/utils/flat-listing.d.ts.map +1 -0
- package/dist/utils/flat-listing.js +68 -0
- package/dist/utils/flat-listing.js.map +1 -0
- package/dist/utils/format.d.ts +1 -3
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +1 -5
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/guard.d.ts +21 -0
- package/dist/utils/guard.d.ts.map +1 -0
- package/dist/utils/guard.js +105 -0
- package/dist/utils/guard.js.map +1 -0
- package/dist/utils/image-dimensions.d.ts +19 -0
- package/dist/utils/image-dimensions.d.ts.map +1 -0
- package/dist/utils/image-dimensions.js +93 -0
- package/dist/utils/image-dimensions.js.map +1 -0
- package/dist/utils/image-fetch.d.ts +31 -0
- package/dist/utils/image-fetch.d.ts.map +1 -0
- package/dist/utils/image-fetch.js +141 -0
- package/dist/utils/image-fetch.js.map +1 -0
- package/dist/utils/json-file.d.ts +45 -0
- package/dist/utils/json-file.d.ts.map +1 -0
- package/dist/utils/json-file.js +60 -0
- package/dist/utils/json-file.js.map +1 -0
- package/dist/utils/paths.d.ts +19 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +27 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/pattern.d.ts +30 -0
- package/dist/utils/pattern.d.ts.map +1 -0
- package/dist/utils/pattern.js +130 -0
- package/dist/utils/pattern.js.map +1 -0
- package/dist/utils/search-index.d.ts +22 -7
- package/dist/utils/search-index.d.ts.map +1 -1
- package/dist/utils/search-index.js +26 -8
- package/dist/utils/search-index.js.map +1 -1
- package/dist/utils/storage.d.ts +14 -44
- package/dist/utils/storage.d.ts.map +1 -1
- package/dist/utils/storage.js +47 -84
- package/dist/utils/storage.js.map +1 -1
- package/dist/utils/subtitles.d.ts +7 -0
- package/dist/utils/subtitles.d.ts.map +1 -0
- package/dist/utils/subtitles.js +30 -0
- package/dist/utils/subtitles.js.map +1 -0
- package/dist/utils/thumbnail-entry.d.ts +19 -0
- package/dist/utils/thumbnail-entry.d.ts.map +1 -0
- package/dist/utils/thumbnail-entry.js +119 -0
- package/dist/utils/thumbnail-entry.js.map +1 -0
- package/dist/utils/thumbnail-fetch.d.ts +37 -0
- package/dist/utils/thumbnail-fetch.d.ts.map +1 -0
- package/dist/utils/thumbnail-fetch.js +89 -0
- package/dist/utils/thumbnail-fetch.js.map +1 -0
- package/dist/utils/thumbnail-ladder.d.ts +44 -0
- package/dist/utils/thumbnail-ladder.d.ts.map +1 -0
- package/dist/utils/thumbnail-ladder.js +68 -0
- package/dist/utils/thumbnail-ladder.js.map +1 -0
- package/dist/utils/thumbnail-listing.d.ts +16 -0
- package/dist/utils/thumbnail-listing.d.ts.map +1 -0
- package/dist/utils/thumbnail-listing.js +26 -0
- package/dist/utils/thumbnail-listing.js.map +1 -0
- package/dist/utils/thumbnail-paths.d.ts +20 -0
- package/dist/utils/thumbnail-paths.d.ts.map +1 -0
- package/dist/utils/thumbnail-paths.js +35 -0
- package/dist/utils/thumbnail-paths.js.map +1 -0
- package/dist/utils/thumbnail-store.d.ts +19 -0
- package/dist/utils/thumbnail-store.d.ts.map +1 -0
- package/dist/utils/thumbnail-store.js +125 -0
- package/dist/utils/thumbnail-store.js.map +1 -0
- package/dist/utils/transcript-cache.d.ts +20 -0
- package/dist/utils/transcript-cache.d.ts.map +1 -0
- package/dist/utils/transcript-cache.js +127 -0
- package/dist/utils/transcript-cache.js.map +1 -0
- package/dist/utils/transcript-search.d.ts +20 -0
- package/dist/utils/transcript-search.d.ts.map +1 -0
- package/dist/utils/transcript-search.js +35 -0
- package/dist/utils/transcript-search.js.map +1 -0
- package/dist/utils/transcript.d.ts +14 -34
- package/dist/utils/transcript.d.ts.map +1 -1
- package/dist/utils/transcript.js +5 -64
- package/dist/utils/transcript.js.map +1 -1
- package/dist/utils/validate-youtube.d.ts +35 -0
- package/dist/utils/validate-youtube.d.ts.map +1 -0
- package/dist/utils/validate-youtube.js +87 -0
- package/dist/utils/validate-youtube.js.map +1 -0
- package/dist/utils/validate.d.ts +11 -3
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +43 -18
- package/dist/utils/validate.js.map +1 -1
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +19 -0
- package/dist/utils/version.js.map +1 -0
- package/dist/utils/youtube-channel.d.ts +44 -0
- package/dist/utils/youtube-channel.d.ts.map +1 -0
- package/dist/utils/youtube-channel.js +61 -0
- package/dist/utils/youtube-channel.js.map +1 -0
- package/dist/utils/youtube-download.d.ts +27 -0
- package/dist/utils/youtube-download.d.ts.map +1 -0
- package/dist/utils/youtube-download.js +118 -0
- package/dist/utils/youtube-download.js.map +1 -0
- package/dist/utils/youtube-schemas.d.ts +394 -0
- package/dist/utils/youtube-schemas.d.ts.map +1 -0
- package/dist/utils/youtube-schemas.js +118 -0
- package/dist/utils/youtube-schemas.js.map +1 -0
- package/dist/utils/youtube-search.d.ts +14 -0
- package/dist/utils/youtube-search.d.ts.map +1 -0
- package/dist/utils/youtube-search.js +39 -0
- package/dist/utils/youtube-search.js.map +1 -0
- package/dist/utils/youtube-url.d.ts +25 -0
- package/dist/utils/youtube-url.d.ts.map +1 -0
- package/dist/utils/youtube-url.js +75 -0
- package/dist/utils/youtube-url.js.map +1 -0
- package/dist/utils/youtube-video.d.ts +47 -0
- package/dist/utils/youtube-video.d.ts.map +1 -0
- package/dist/utils/youtube-video.js +98 -0
- package/dist/utils/youtube-video.js.map +1 -0
- package/dist/utils/youtube.d.ts +13 -111
- package/dist/utils/youtube.d.ts.map +1 -1
- package/dist/utils/youtube.js +11 -480
- package/dist/utils/youtube.js.map +1 -1
- package/dist/utils/ytdlp-env.d.ts +33 -0
- package/dist/utils/ytdlp-env.d.ts.map +1 -0
- package/dist/utils/ytdlp-env.js +121 -0
- package/dist/utils/ytdlp-env.js.map +1 -0
- package/dist/utils/ytdlp-limiter.d.ts +24 -0
- package/dist/utils/ytdlp-limiter.d.ts.map +1 -0
- package/dist/utils/ytdlp-limiter.js +69 -0
- package/dist/utils/ytdlp-limiter.js.map +1 -0
- package/dist/utils/ytdlp-parse.d.ts +13 -0
- package/dist/utils/ytdlp-parse.d.ts.map +1 -0
- package/dist/utils/ytdlp-parse.js +48 -0
- package/dist/utils/ytdlp-parse.js.map +1 -0
- package/dist/utils/ytdlp.d.ts +13 -18
- package/dist/utils/ytdlp.d.ts.map +1 -1
- package/dist/utils/ytdlp.js +21 -110
- package/dist/utils/ytdlp.js.map +1 -1
- package/package.json +2 -1
|
@@ -20,24 +20,27 @@ export declare const searchVideosOutputSchema: {
|
|
|
20
20
|
channel: z.ZodOptional<z.ZodString>;
|
|
21
21
|
viewCount: z.ZodOptional<z.ZodNumber>;
|
|
22
22
|
uploadDate: z.ZodOptional<z.ZodString>;
|
|
23
|
+
thumbnailUrl: z.ZodOptional<z.ZodString>;
|
|
23
24
|
}, "strip", z.ZodTypeAny, {
|
|
24
|
-
url: string;
|
|
25
|
-
title: string;
|
|
26
25
|
id: string;
|
|
27
|
-
|
|
26
|
+
title: string;
|
|
28
27
|
durationSeconds: number;
|
|
28
|
+
durationFormatted: string;
|
|
29
|
+
url: string;
|
|
29
30
|
channel?: string | undefined;
|
|
30
|
-
uploadDate?: string | undefined;
|
|
31
31
|
viewCount?: number | undefined;
|
|
32
|
+
uploadDate?: string | undefined;
|
|
33
|
+
thumbnailUrl?: string | undefined;
|
|
32
34
|
}, {
|
|
33
|
-
url: string;
|
|
34
|
-
title: string;
|
|
35
35
|
id: string;
|
|
36
|
-
|
|
36
|
+
title: string;
|
|
37
37
|
durationSeconds: number;
|
|
38
|
+
durationFormatted: string;
|
|
39
|
+
url: string;
|
|
38
40
|
channel?: string | undefined;
|
|
39
|
-
uploadDate?: string | undefined;
|
|
40
41
|
viewCount?: number | undefined;
|
|
42
|
+
uploadDate?: string | undefined;
|
|
43
|
+
thumbnailUrl?: string | undefined;
|
|
41
44
|
}>, "many">;
|
|
42
45
|
};
|
|
43
46
|
export declare function searchVideosHandler({ query, limit, }: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-videos.d.ts","sourceRoot":"","sources":["../../src/tools/search-videos.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,eAAO,MAAM,kBAAkB;;;CAU9B,CAAC;AAEF,eAAO,MAAM,wBAAwB
|
|
1
|
+
{"version":3,"file":"search-videos.d.ts","sourceRoot":"","sources":["../../src/tools/search-videos.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,eAAO,MAAM,kBAAkB;;;CAU9B,CAAC;AAEF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAIpC,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,EACxC,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,cAAc,CAAC,CA6B1B"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `work` over the items, `size` at a time.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not a rolling window: a batch boundary is a natural place for a
|
|
5
|
+
* checkpoint and a cancellation check, and the limiter inside yt-dlp is what
|
|
6
|
+
* actually paces the network. Results come back in item order, whichever
|
|
7
|
+
* finished first.
|
|
8
|
+
*
|
|
9
|
+
* A batch always settles before a failure propagates. `Promise.all` would
|
|
10
|
+
* reject the moment one item did — on a cancelled request, before the items
|
|
11
|
+
* already in flight had finished — and a checkpoint taken in that instant
|
|
12
|
+
* would record none of them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function inBatches<T, R>(items: T[], size: number, work: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
15
|
+
//# sourceMappingURL=batches.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batches.d.ts","sourceRoot":"","sources":["../../src/utils/batches.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,CAAC,EAClC,KAAK,EAAE,CAAC,EAAE,EACV,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3C,OAAO,CAAC,CAAC,EAAE,CAAC,CAiBd"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `work` over the items, `size` at a time.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not a rolling window: a batch boundary is a natural place for a
|
|
5
|
+
* checkpoint and a cancellation check, and the limiter inside yt-dlp is what
|
|
6
|
+
* actually paces the network. Results come back in item order, whichever
|
|
7
|
+
* finished first.
|
|
8
|
+
*
|
|
9
|
+
* A batch always settles before a failure propagates. `Promise.all` would
|
|
10
|
+
* reject the moment one item did — on a cancelled request, before the items
|
|
11
|
+
* already in flight had finished — and a checkpoint taken in that instant
|
|
12
|
+
* would record none of them.
|
|
13
|
+
*/
|
|
14
|
+
export async function inBatches(items, size, work) {
|
|
15
|
+
const results = [];
|
|
16
|
+
const width = Math.max(1, size);
|
|
17
|
+
for (let start = 0; start < items.length; start += width) {
|
|
18
|
+
const slice = items.slice(start, start + width);
|
|
19
|
+
const settled = await Promise.allSettled(slice.map((item, offset) => work(item, start + offset)));
|
|
20
|
+
for (const outcome of settled) {
|
|
21
|
+
if (outcome.status === 'rejected')
|
|
22
|
+
throw asError(outcome.reason);
|
|
23
|
+
results.push(outcome.value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
function asError(reason) {
|
|
29
|
+
return reason instanceof Error ? reason : new Error(String(reason));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=batches.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batches.js","sourceRoot":"","sources":["../../src/utils/batches.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAU,EACV,IAAY,EACZ,IAA4C;IAE5C,MAAM,OAAO,GAAQ,EAAE,CAAC;IACxB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAEhC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,KAAK,EAAE,CAAC;QACzD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;QAChD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,CACxD,CAAC;QAEF,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;YAC9B,IAAI,OAAO,CAAC,MAAM,KAAK,UAAU;gBAAE,MAAM,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACjE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,OAAO,CAAC,MAAe;IAC9B,OAAO,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BrainManifest } from '../brain-schemas.js';
|
|
2
|
+
import { type VideoFilters } from './brain-ingest.js';
|
|
3
|
+
import type { ChannelInfo, VideoListItem } from './youtube.js';
|
|
4
|
+
/**
|
|
5
|
+
* Reading a channel into a brain.
|
|
6
|
+
*
|
|
7
|
+
* The shape of this is dictated by how long it takes. Several hundred videos is
|
|
8
|
+
* several hundred yt-dlp invocations, so a build is something that gets
|
|
9
|
+
* interrupted: cancelled by the client, throttled by YouTube, or killed along
|
|
10
|
+
* with the editor it was started from. Every choice below follows from that —
|
|
11
|
+
* per-video state rather than one "built" flag, checkpoints rather than a
|
|
12
|
+
* single write at the end, and a failed video costing only itself.
|
|
13
|
+
*
|
|
14
|
+
* Which is also why there is no separate refresh: a second build sees what it
|
|
15
|
+
* already has, skips it, and reads whatever is new.
|
|
16
|
+
*/
|
|
17
|
+
/** Frequent enough that little work is lost, rare enough not to dominate. */
|
|
18
|
+
export declare const CHECKPOINT_EVERY_VIDEOS = 10;
|
|
19
|
+
/** Past this many throttled videos in a row, YouTube means it. */
|
|
20
|
+
export declare const RATE_LIMIT_TOLERANCE = 3;
|
|
21
|
+
export interface BuildBrainOptions extends VideoFilters {
|
|
22
|
+
channel: ChannelInfo;
|
|
23
|
+
videos: VideoListItem[];
|
|
24
|
+
existing: BrainManifest | undefined;
|
|
25
|
+
/** Caption language to read. A brain is built from one language at a time. */
|
|
26
|
+
language: string;
|
|
27
|
+
}
|
|
28
|
+
export interface BuildBrainResult {
|
|
29
|
+
manifest: BrainManifest;
|
|
30
|
+
/** Videos the filters kept, whether or not this call had to read them. */
|
|
31
|
+
considered: number;
|
|
32
|
+
/** Videos read during this call, as opposed to carried over from an earlier one. */
|
|
33
|
+
processed: number;
|
|
34
|
+
skipped: number;
|
|
35
|
+
excluded: number;
|
|
36
|
+
stoppedEarly: boolean;
|
|
37
|
+
stopReason?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function buildBrain(options: BuildBrainOptions): Promise<BuildBrainResult>;
|
|
40
|
+
//# sourceMappingURL=brain-build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-build.d.ts","sourceRoot":"","sources":["../../src/utils/brain-build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,aAAa,EAAmB,MAAM,qBAAqB,CAAC;AAEtF,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE/D;;;;;;;;;;;;GAYG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAc1C,kEAAkE;AAClE,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,QAAQ,EAAE,aAAa,GAAG,SAAS,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,aAAa,CAAC;IACxB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4EtF"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { readChunks, writeChunks } from './brain-index.js';
|
|
2
|
+
import { excluded, ingestVideo, isExcluded, pendingState, } from './brain-ingest.js';
|
|
3
|
+
import { computeStats } from './brain-stats.js';
|
|
4
|
+
import { BRAIN_MANIFEST_VERSION, writeManifest } from './brain-storage.js';
|
|
5
|
+
import { reportProgress, throwIfAborted } from './context.js';
|
|
6
|
+
import { concurrencyState } from './ytdlp.js';
|
|
7
|
+
import { inBatches } from './batches.js';
|
|
8
|
+
/**
|
|
9
|
+
* Reading a channel into a brain.
|
|
10
|
+
*
|
|
11
|
+
* The shape of this is dictated by how long it takes. Several hundred videos is
|
|
12
|
+
* several hundred yt-dlp invocations, so a build is something that gets
|
|
13
|
+
* interrupted: cancelled by the client, throttled by YouTube, or killed along
|
|
14
|
+
* with the editor it was started from. Every choice below follows from that —
|
|
15
|
+
* per-video state rather than one "built" flag, checkpoints rather than a
|
|
16
|
+
* single write at the end, and a failed video costing only itself.
|
|
17
|
+
*
|
|
18
|
+
* Which is also why there is no separate refresh: a second build sees what it
|
|
19
|
+
* already has, skips it, and reads whatever is new.
|
|
20
|
+
*/
|
|
21
|
+
/** Frequent enough that little work is lost, rare enough not to dominate. */
|
|
22
|
+
export const CHECKPOINT_EVERY_VIDEOS = 10;
|
|
23
|
+
/**
|
|
24
|
+
* How many videos to read at once: whatever the yt-dlp limiter is actually set
|
|
25
|
+
* to, which `YOUTUBE_MCP_MAX_CONCURRENCY` can change.
|
|
26
|
+
*
|
|
27
|
+
* Read rather than copied. A second number that merely happened to match would
|
|
28
|
+
* stop matching the moment someone tuned the first, and would then either queue
|
|
29
|
+
* work behind a limit it could not see or batch below one it was allowed.
|
|
30
|
+
*/
|
|
31
|
+
function buildConcurrency() {
|
|
32
|
+
return concurrencyState().limit;
|
|
33
|
+
}
|
|
34
|
+
/** Past this many throttled videos in a row, YouTube means it. */
|
|
35
|
+
export const RATE_LIMIT_TOLERANCE = 3;
|
|
36
|
+
export async function buildBrain(options) {
|
|
37
|
+
const { channel, videos, existing, language, since, minDurationSeconds } = options;
|
|
38
|
+
const filters = { minDurationSeconds, ...(since === undefined ? {} : { since }) };
|
|
39
|
+
const createdAt = existing?.createdAt ?? new Date().toISOString();
|
|
40
|
+
const chunksByVideo = groupByVideo(await readChunks(channel.channelId));
|
|
41
|
+
const states = reconciled(existing, videos, chunksByVideo, filters);
|
|
42
|
+
const outstanding = videos.filter((video) => isOutstanding(states.get(video.id)));
|
|
43
|
+
/**
|
|
44
|
+
* `phrases` is the difference between a checkpoint and the end of a build:
|
|
45
|
+
* the phrase pass reads the whole corpus and takes seconds at the sizes a
|
|
46
|
+
* brain can reach, so it runs once, when there is a finished corpus to run it
|
|
47
|
+
* over. A checkpoint records what has been read, which is all it is for.
|
|
48
|
+
*/
|
|
49
|
+
const save = async (phrases) => {
|
|
50
|
+
const passages = flatten(states, chunksByVideo);
|
|
51
|
+
const manifest = toManifest(channel, states, passages, createdAt, language, phrases);
|
|
52
|
+
await writeChunks(channel.channelId, passages);
|
|
53
|
+
await writeManifest(manifest);
|
|
54
|
+
return manifest;
|
|
55
|
+
};
|
|
56
|
+
let attempted = 0;
|
|
57
|
+
let processed = 0;
|
|
58
|
+
let consecutiveThrottles = 0;
|
|
59
|
+
let stopReason;
|
|
60
|
+
try {
|
|
61
|
+
await inBatches(outstanding, buildConcurrency(), async (video) => {
|
|
62
|
+
if (stopReason !== undefined)
|
|
63
|
+
return;
|
|
64
|
+
throwIfAborted();
|
|
65
|
+
const { state, chunks } = await ingestVideo(video, countChunks(chunksByVideo), {
|
|
66
|
+
language,
|
|
67
|
+
...filters,
|
|
68
|
+
});
|
|
69
|
+
states.set(video.id, state);
|
|
70
|
+
if (chunks.length > 0)
|
|
71
|
+
chunksByVideo.set(video.id, chunks);
|
|
72
|
+
else
|
|
73
|
+
chunksByVideo.delete(video.id);
|
|
74
|
+
attempted++;
|
|
75
|
+
// A video the filters ruled out was looked at, not read.
|
|
76
|
+
if (state.state !== 'excluded')
|
|
77
|
+
processed++;
|
|
78
|
+
consecutiveThrottles = state.error === 'RATE_LIMITED' ? consecutiveThrottles + 1 : 0;
|
|
79
|
+
if (consecutiveThrottles >= RATE_LIMIT_TOLERANCE) {
|
|
80
|
+
stopReason = 'YouTube is rate limiting this client.';
|
|
81
|
+
}
|
|
82
|
+
reportProgress(attempted, outstanding.length, `Read ${attempted} of ${outstanding.length}`);
|
|
83
|
+
if (attempted % CHECKPOINT_EVERY_VIDEOS === 0)
|
|
84
|
+
await save(false);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
// A cancelled build must not throw away the videos read since the last
|
|
89
|
+
// checkpoint. Saving first is what makes "interrupt it and call it again"
|
|
90
|
+
// true rather than nearly true — and it is a checkpoint, not a finished
|
|
91
|
+
// corpus, so it does not pay for the phrase pass either.
|
|
92
|
+
await save(false);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const manifest = await save(true);
|
|
96
|
+
const excludedCount = videos.filter((video) => states.get(video.id)?.state === 'excluded').length;
|
|
97
|
+
return {
|
|
98
|
+
manifest,
|
|
99
|
+
considered: videos.length - excludedCount,
|
|
100
|
+
processed,
|
|
101
|
+
skipped: videos.length - excludedCount - processed,
|
|
102
|
+
excluded: excludedCount,
|
|
103
|
+
stoppedEarly: stopReason !== undefined,
|
|
104
|
+
...(stopReason === undefined ? {} : { stopReason }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Per-video state, reconciled against the passages on disk and re-tested
|
|
109
|
+
* against the filters in force right now.
|
|
110
|
+
*
|
|
111
|
+
* Two corrections happen here, both for the same reason: what the manifest says
|
|
112
|
+
* was true when it was written, and neither the corpus nor the caller's filters
|
|
113
|
+
* are obliged to have stayed the same.
|
|
114
|
+
*
|
|
115
|
+
* The manifest and the passage file are two documents, and only one can be
|
|
116
|
+
* written first. A crash between them, a half-restored backup, or a
|
|
117
|
+
* `chunks.json` truncated by a full disk all end the same way — the manifest
|
|
118
|
+
* says a video was read and there is nothing to show for it. Left alone that
|
|
119
|
+
* brain is stranded for good: every build skips the video as already done while
|
|
120
|
+
* every search returns nothing. So the passages win, and a video the corpus
|
|
121
|
+
* cannot account for goes back to pending.
|
|
122
|
+
*
|
|
123
|
+
* A filter is re-applied rather than remembered. Narrowing one excludes videos
|
|
124
|
+
* already read; widening one brings excluded videos back. Both are decided from
|
|
125
|
+
* the dates and lengths already recorded, so changing your mind costs nothing
|
|
126
|
+
* until there is something new to fetch.
|
|
127
|
+
*/
|
|
128
|
+
function reconciled(existing, videos, chunksByVideo, filters) {
|
|
129
|
+
const states = new Map();
|
|
130
|
+
for (const [videoId, recorded] of Object.entries(existing?.videos ?? {})) {
|
|
131
|
+
const lost = recorded.state === 'indexed' && (chunksByVideo.get(videoId)?.length ?? 0) === 0;
|
|
132
|
+
const state = lost
|
|
133
|
+
? { ...recorded, state: 'pending', chunkCount: 0, wordCount: 0 }
|
|
134
|
+
: recorded;
|
|
135
|
+
states.set(videoId, refiltered(state, filters));
|
|
136
|
+
}
|
|
137
|
+
for (const video of videos) {
|
|
138
|
+
// The listing's own length is real data, so a filter that can be decided
|
|
139
|
+
// from it is decided here — before spending a request to learn what the
|
|
140
|
+
// listing already said.
|
|
141
|
+
if (!states.has(video.id))
|
|
142
|
+
states.set(video.id, refiltered(pendingState(video), filters));
|
|
143
|
+
}
|
|
144
|
+
return states;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `undefined` — the filters cannot be decided from what is recorded — leaves the
|
|
148
|
+
* state alone, so the video is read and the question answered from the metadata
|
|
149
|
+
* that read fetches.
|
|
150
|
+
*/
|
|
151
|
+
function refiltered(state, filters) {
|
|
152
|
+
const ruledOut = isExcluded(state, filters);
|
|
153
|
+
if (ruledOut === true)
|
|
154
|
+
return excluded(state);
|
|
155
|
+
if (ruledOut === false && state.state === 'excluded') {
|
|
156
|
+
return { ...state, state: 'pending' };
|
|
157
|
+
}
|
|
158
|
+
return state;
|
|
159
|
+
}
|
|
160
|
+
/** Anything not already read: never attempted, or attempted and failed. */
|
|
161
|
+
function isOutstanding(state) {
|
|
162
|
+
return state === undefined || state.state === 'pending' || state.state === 'failed';
|
|
163
|
+
}
|
|
164
|
+
function groupByVideo(chunks) {
|
|
165
|
+
const grouped = new Map();
|
|
166
|
+
for (const chunk of chunks) {
|
|
167
|
+
grouped.set(chunk.videoId, [...(grouped.get(chunk.videoId) ?? []), chunk]);
|
|
168
|
+
}
|
|
169
|
+
return grouped;
|
|
170
|
+
}
|
|
171
|
+
function countChunks(chunksByVideo) {
|
|
172
|
+
let total = 0;
|
|
173
|
+
for (const chunks of chunksByVideo.values())
|
|
174
|
+
total += chunks.length;
|
|
175
|
+
return total;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The corpus: every passage of every video the brain holds, in manifest order so
|
|
179
|
+
* the file changes as little as possible between builds.
|
|
180
|
+
*
|
|
181
|
+
* A video the filters exclude contributes nothing, which is what makes the
|
|
182
|
+
* filters mean something. Leaving its passages in would let `ask_brain` quote a
|
|
183
|
+
* video that `get_brain_info` says is not part of this brain, and no wording
|
|
184
|
+
* makes that defensible.
|
|
185
|
+
*/
|
|
186
|
+
function flatten(states, chunksByVideo) {
|
|
187
|
+
return [...states.entries()].flatMap(([videoId, state]) => state.state === 'excluded' ? [] : (chunksByVideo.get(videoId) ?? []));
|
|
188
|
+
}
|
|
189
|
+
function toManifest(channel, states, passages, createdAt, language, phrases) {
|
|
190
|
+
return {
|
|
191
|
+
version: BRAIN_MANIFEST_VERSION,
|
|
192
|
+
channel,
|
|
193
|
+
language,
|
|
194
|
+
createdAt,
|
|
195
|
+
updatedAt: new Date().toISOString(),
|
|
196
|
+
videos: Object.fromEntries(states),
|
|
197
|
+
stats: computeStats([...states.values()], passages, { phrases }),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=brain-build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-build.js","sourceRoot":"","sources":["../../src/utils/brain-build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BrainChunk } from '../brain-schemas.js';
|
|
2
|
+
import { type TranscriptSegment } from './transcript.js';
|
|
3
|
+
import type { Chapter } from './youtube.js';
|
|
4
|
+
/**
|
|
5
|
+
* Transcripts cut into retrievable passages.
|
|
6
|
+
*
|
|
7
|
+
* A brain is searched, not read: a whole transcript is too long to rank
|
|
8
|
+
* usefully and far too long to return, while a single caption cue is too short
|
|
9
|
+
* to carry a thought. A passage of a few sentences is what BM25 scores well and
|
|
10
|
+
* what a reader can check against the video in one click.
|
|
11
|
+
*
|
|
12
|
+
* Chunks do not overlap. Overlap is worth its cost with embeddings, where a
|
|
13
|
+
* concept split across a boundary can be missed entirely; BM25 matches terms,
|
|
14
|
+
* and a duplicated passage would only mean two results saying the same thing.
|
|
15
|
+
*/
|
|
16
|
+
/** Where a passage stops being short enough to keep growing. */
|
|
17
|
+
export declare const CHUNK_TARGET_CHARS = 800;
|
|
18
|
+
/**
|
|
19
|
+
* Where it stops regardless. Speech without a pause for this long is rare, and
|
|
20
|
+
* an unbroken passage is worse than one broken in an awkward place.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CHUNK_MAX_CHARS = 1200;
|
|
23
|
+
/** A pause this long is a sentence or a thought ending. */
|
|
24
|
+
export declare const CHUNK_BREAK_GAP_SECONDS = 2;
|
|
25
|
+
export interface ChunkTranscriptOptions {
|
|
26
|
+
videoId: string;
|
|
27
|
+
title: string;
|
|
28
|
+
segments: TranscriptSegment[];
|
|
29
|
+
/** Chapter starts are always passage boundaries: a new chapter is a new subject. */
|
|
30
|
+
chapters?: Chapter[];
|
|
31
|
+
}
|
|
32
|
+
export declare function chunkTranscript(options: ChunkTranscriptOptions): BrainChunk[];
|
|
33
|
+
//# sourceMappingURL=brain-chunks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-chunks.d.ts","sourceRoot":"","sources":["../../src/utils/brain-chunks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;;;;;;GAWG;AAEH,gEAAgE;AAChE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;;GAGG;AACH,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,2DAA2D;AAC3D,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,UAAU,EAAE,CAwC7E"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { segmentsToText } from './transcript.js';
|
|
2
|
+
/**
|
|
3
|
+
* Transcripts cut into retrievable passages.
|
|
4
|
+
*
|
|
5
|
+
* A brain is searched, not read: a whole transcript is too long to rank
|
|
6
|
+
* usefully and far too long to return, while a single caption cue is too short
|
|
7
|
+
* to carry a thought. A passage of a few sentences is what BM25 scores well and
|
|
8
|
+
* what a reader can check against the video in one click.
|
|
9
|
+
*
|
|
10
|
+
* Chunks do not overlap. Overlap is worth its cost with embeddings, where a
|
|
11
|
+
* concept split across a boundary can be missed entirely; BM25 matches terms,
|
|
12
|
+
* and a duplicated passage would only mean two results saying the same thing.
|
|
13
|
+
*/
|
|
14
|
+
/** Where a passage stops being short enough to keep growing. */
|
|
15
|
+
export const CHUNK_TARGET_CHARS = 800;
|
|
16
|
+
/**
|
|
17
|
+
* Where it stops regardless. Speech without a pause for this long is rare, and
|
|
18
|
+
* an unbroken passage is worse than one broken in an awkward place.
|
|
19
|
+
*/
|
|
20
|
+
export const CHUNK_MAX_CHARS = 1200;
|
|
21
|
+
/** A pause this long is a sentence or a thought ending. */
|
|
22
|
+
export const CHUNK_BREAK_GAP_SECONDS = 2;
|
|
23
|
+
export function chunkTranscript(options) {
|
|
24
|
+
const { videoId, title, segments, chapters = [] } = options;
|
|
25
|
+
const chapterStarts = chapters.map((chapter) => chapter.startTime).sort((a, b) => a - b);
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let current = [];
|
|
28
|
+
let currentChars = 0;
|
|
29
|
+
let nextChapter = 0;
|
|
30
|
+
const flush = () => {
|
|
31
|
+
const chunk = toChunk(videoId, title, current, chunks.length);
|
|
32
|
+
if (chunk !== undefined)
|
|
33
|
+
chunks.push(chunk);
|
|
34
|
+
current = [];
|
|
35
|
+
currentChars = 0;
|
|
36
|
+
};
|
|
37
|
+
for (const segment of segments) {
|
|
38
|
+
// Consume every chapter that starts at or before this segment. More than
|
|
39
|
+
// one can land in the same gap when a chapter is shorter than a cue.
|
|
40
|
+
let startsChapter = false;
|
|
41
|
+
while (nextChapter < chapterStarts.length) {
|
|
42
|
+
const start = chapterStarts[nextChapter];
|
|
43
|
+
if (start === undefined || start > segment.start)
|
|
44
|
+
break;
|
|
45
|
+
nextChapter++;
|
|
46
|
+
startsChapter = true;
|
|
47
|
+
}
|
|
48
|
+
const previous = current[current.length - 1];
|
|
49
|
+
if (previous !== undefined && shouldBreak(previous, segment, currentChars, startsChapter)) {
|
|
50
|
+
flush();
|
|
51
|
+
}
|
|
52
|
+
current.push(segment);
|
|
53
|
+
// Plus one for the space this segment will be joined with.
|
|
54
|
+
currentChars += segment.text.length + 1;
|
|
55
|
+
}
|
|
56
|
+
flush();
|
|
57
|
+
return chunks;
|
|
58
|
+
}
|
|
59
|
+
function shouldBreak(previous, segment, currentChars, startsChapter) {
|
|
60
|
+
if (startsChapter)
|
|
61
|
+
return true;
|
|
62
|
+
if (currentChars >= CHUNK_MAX_CHARS)
|
|
63
|
+
return true;
|
|
64
|
+
return (currentChars >= CHUNK_TARGET_CHARS && segment.start - previous.end >= CHUNK_BREAK_GAP_SECONDS);
|
|
65
|
+
}
|
|
66
|
+
/** Undefined for an empty run, and for one that turned out to be only whitespace. */
|
|
67
|
+
function toChunk(videoId, title, segments, ordinal) {
|
|
68
|
+
const first = segments[0];
|
|
69
|
+
const last = segments[segments.length - 1];
|
|
70
|
+
if (first === undefined || last === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
const text = segmentsToText(segments);
|
|
73
|
+
if (text === '')
|
|
74
|
+
return undefined;
|
|
75
|
+
return {
|
|
76
|
+
// Ordinal rather than start time: two passages can begin in the same second
|
|
77
|
+
// when cues are short, and a colliding id would silently drop one of them
|
|
78
|
+
// from the index. The start time is a field of its own.
|
|
79
|
+
id: `${videoId}:${ordinal}`,
|
|
80
|
+
videoId,
|
|
81
|
+
title,
|
|
82
|
+
startSeconds: first.start,
|
|
83
|
+
endSeconds: last.end,
|
|
84
|
+
text,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=brain-chunks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-chunks.js","sourceRoot":"","sources":["../../src/utils/brain-chunks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type BrainChunk, type BrainPassage } from '../brain-schemas.js';
|
|
2
|
+
/**
|
|
3
|
+
* The searchable half of a brain: the passages, and a BM25 index over them.
|
|
4
|
+
*
|
|
5
|
+
* The passages are the only thing persisted. An index file alongside them would
|
|
6
|
+
* hold a second copy of every word — the serialised index carries the text it
|
|
7
|
+
* scored — and would then be free to disagree with them. It is built in memory
|
|
8
|
+
* on first use instead and kept until the passages change: measured at 1.5s for
|
|
9
|
+
* a corpus at this brain's 100,000-passage ceiling, once, against a class of
|
|
10
|
+
* bugs forever. A search over that corpus then costs 73ms.
|
|
11
|
+
*
|
|
12
|
+
* Retrieval is the only way a corpus reaches a model. That ceiling is 81MB of
|
|
13
|
+
* transcript, so there is no "give me everything" path here: `readChunks`
|
|
14
|
+
* exists for statistics and rebuilding, not for answering.
|
|
15
|
+
*/
|
|
16
|
+
export declare const BRAIN_CHUNK_FILE_VERSION = 1;
|
|
17
|
+
export declare function forgetBrainCorpus(channelId: string): void;
|
|
18
|
+
export declare function readChunks(channelId: string): Promise<BrainChunk[]>;
|
|
19
|
+
export declare function writeChunks(channelId: string, chunks: BrainChunk[]): Promise<void>;
|
|
20
|
+
export interface BrainSearchResults {
|
|
21
|
+
passages: BrainPassage[];
|
|
22
|
+
/** Passages matching the query, not passages returned. */
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
/** Passages ranked against the query, each with the link that opens it. */
|
|
26
|
+
export declare function searchBrain(channelId: string, query: string, limit: number, offset?: number): Promise<BrainSearchResults>;
|
|
27
|
+
//# sourceMappingURL=brain-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-index.d.ts","sourceRoot":"","sources":["../../src/utils/brain-index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAO/F;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAoB1C,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAGzE;AAED,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CASxF;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;CACf;AAED,2EAA2E;AAC3E,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,MAAM,SAAI,GACT,OAAO,CAAC,kBAAkB,CAAC,CAuB7B"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { brainChunkFileSchema } from '../brain-schemas.js';
|
|
4
|
+
import { brainDir, chunksPath } from './brain-paths.js';
|
|
5
|
+
import { ensurePrivateDir } from './paths.js';
|
|
6
|
+
import { readJsonFile, writeJsonAtomic } from './json-file.js';
|
|
7
|
+
import { SearchIndex } from './search-index.js';
|
|
8
|
+
import { deepLink, formatTimestamp } from './transcript.js';
|
|
9
|
+
/**
|
|
10
|
+
* The searchable half of a brain: the passages, and a BM25 index over them.
|
|
11
|
+
*
|
|
12
|
+
* The passages are the only thing persisted. An index file alongside them would
|
|
13
|
+
* hold a second copy of every word — the serialised index carries the text it
|
|
14
|
+
* scored — and would then be free to disagree with them. It is built in memory
|
|
15
|
+
* on first use instead and kept until the passages change: measured at 1.5s for
|
|
16
|
+
* a corpus at this brain's 100,000-passage ceiling, once, against a class of
|
|
17
|
+
* bugs forever. A search over that corpus then costs 73ms.
|
|
18
|
+
*
|
|
19
|
+
* Retrieval is the only way a corpus reaches a model. That ceiling is 81MB of
|
|
20
|
+
* transcript, so there is no "give me everything" path here: `readChunks`
|
|
21
|
+
* exists for statistics and rebuilding, not for answering.
|
|
22
|
+
*/
|
|
23
|
+
export const BRAIN_CHUNK_FILE_VERSION = 1;
|
|
24
|
+
/**
|
|
25
|
+
* Rebuilding the index on every query would re-read and re-tokenise the whole
|
|
26
|
+
* corpus each time. The passage file's modification time decides when the
|
|
27
|
+
* cached copy is wrong, so a build running in another process is picked up
|
|
28
|
+
* without any coordination between the two.
|
|
29
|
+
*/
|
|
30
|
+
const cache = new Map();
|
|
31
|
+
export function forgetBrainCorpus(channelId) {
|
|
32
|
+
cache.delete(channelId);
|
|
33
|
+
}
|
|
34
|
+
export async function readChunks(channelId) {
|
|
35
|
+
const file = await readJsonFile(chunksPath(channelId), brainChunkFileSchema);
|
|
36
|
+
return file?.chunks ?? [];
|
|
37
|
+
}
|
|
38
|
+
export async function writeChunks(channelId, chunks) {
|
|
39
|
+
await ensurePrivateDir(brainDir(channelId));
|
|
40
|
+
// Not pretty-printed: indentation would be a fifth of a file nobody reads.
|
|
41
|
+
await writeJsonAtomic(chunksPath(channelId), { version: BRAIN_CHUNK_FILE_VERSION, chunks }, { pretty: false });
|
|
42
|
+
forgetBrainCorpus(channelId);
|
|
43
|
+
}
|
|
44
|
+
/** Passages ranked against the query, each with the link that opens it. */
|
|
45
|
+
export async function searchBrain(channelId, query, limit, offset = 0) {
|
|
46
|
+
const { index, byId } = await corpusOf(channelId);
|
|
47
|
+
const { hits, total } = index.search(query, limit, offset);
|
|
48
|
+
const passages = hits.flatMap((hit) => {
|
|
49
|
+
const chunk = byId.get(hit.id);
|
|
50
|
+
if (chunk === undefined)
|
|
51
|
+
return [];
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
videoId: chunk.videoId,
|
|
55
|
+
title: chunk.title,
|
|
56
|
+
startSeconds: chunk.startSeconds,
|
|
57
|
+
startFormatted: formatTimestamp(chunk.startSeconds),
|
|
58
|
+
endSeconds: chunk.endSeconds,
|
|
59
|
+
score: hit.score,
|
|
60
|
+
text: chunk.text,
|
|
61
|
+
url: deepLink(chunk.videoId, chunk.startSeconds),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
});
|
|
65
|
+
return { passages, total };
|
|
66
|
+
}
|
|
67
|
+
async function corpusOf(channelId) {
|
|
68
|
+
const path = chunksPath(channelId);
|
|
69
|
+
if (!existsSync(path))
|
|
70
|
+
return emptyCorpus();
|
|
71
|
+
const { mtimeMs } = await stat(path);
|
|
72
|
+
const cached = cache.get(channelId);
|
|
73
|
+
if (cached?.mtimeMs === mtimeMs)
|
|
74
|
+
return cached.corpus;
|
|
75
|
+
const corpus = buildCorpus(await readChunks(channelId));
|
|
76
|
+
cache.set(channelId, { corpus, mtimeMs });
|
|
77
|
+
return corpus;
|
|
78
|
+
}
|
|
79
|
+
function emptyCorpus() {
|
|
80
|
+
return { index: new SearchIndex(), byId: new Map() };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The shared index is used unchanged: a passage already has an id, a video, a
|
|
84
|
+
* title and text, which is everything BM25 scores on. `kind` distinguishes
|
|
85
|
+
* these rows from the library's summaries and skills.
|
|
86
|
+
*/
|
|
87
|
+
function buildCorpus(chunks) {
|
|
88
|
+
const corpus = emptyCorpus();
|
|
89
|
+
for (const chunk of chunks) {
|
|
90
|
+
corpus.index.add({
|
|
91
|
+
id: chunk.id,
|
|
92
|
+
videoId: chunk.videoId,
|
|
93
|
+
title: chunk.title,
|
|
94
|
+
kind: 'chunk',
|
|
95
|
+
text: chunk.text,
|
|
96
|
+
});
|
|
97
|
+
corpus.byId.set(chunk.id, chunk);
|
|
98
|
+
}
|
|
99
|
+
return corpus;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=brain-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-index.js","sourceRoot":"","sources":["../../src/utils/brain-index.ts"],"names":[],"mappings":"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"}
|