youtube-knowledge-mcp 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -41
- package/dist/brain-schemas.d.ts +10 -0
- package/dist/brain-schemas.d.ts.map +1 -1
- package/dist/harvest-schemas.d.ts +6 -6
- package/dist/harvest-schemas.d.ts.map +1 -1
- package/dist/harvest-schemas.js +5 -0
- package/dist/harvest-schemas.js.map +1 -1
- package/dist/registry/brain-tools.d.ts.map +1 -1
- package/dist/registry/brain-tools.js +16 -0
- package/dist/registry/brain-tools.js.map +1 -1
- package/dist/registry/channel-tools.d.ts.map +1 -1
- package/dist/registry/channel-tools.js +16 -0
- package/dist/registry/channel-tools.js.map +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +4 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/media-tools.d.ts.map +1 -1
- package/dist/registry/media-tools.js +31 -0
- package/dist/registry/media-tools.js.map +1 -1
- package/dist/registry/store-tools.d.ts +9 -0
- package/dist/registry/store-tools.d.ts.map +1 -0
- package/dist/registry/store-tools.js +59 -0
- package/dist/registry/store-tools.js.map +1 -0
- package/dist/registry/transcript-tools.d.ts +4 -1
- package/dist/registry/transcript-tools.d.ts.map +1 -1
- package/dist/registry/transcript-tools.js +20 -1
- package/dist/registry/transcript-tools.js.map +1 -1
- package/dist/registry/video-tools.d.ts.map +1 -1
- package/dist/registry/video-tools.js +48 -0
- package/dist/registry/video-tools.js.map +1 -1
- package/dist/registry/vision-tools.d.ts +7 -0
- package/dist/registry/vision-tools.d.ts.map +1 -0
- package/dist/registry/vision-tools.js +55 -0
- package/dist/registry/vision-tools.js.map +1 -0
- package/dist/schemas.d.ts +6 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +2 -0
- package/dist/schemas.js.map +1 -1
- package/dist/thumbnail-schemas.d.ts +10 -0
- package/dist/thumbnail-schemas.d.ts.map +1 -1
- package/dist/tools/ask-brains.d.ts +66 -0
- package/dist/tools/ask-brains.d.ts.map +1 -0
- package/dist/tools/ask-brains.js +111 -0
- package/dist/tools/ask-brains.js.map +1 -0
- package/dist/tools/check-health.d.ts +16 -6
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +30 -1
- package/dist/tools/check-health.js.map +1 -1
- package/dist/tools/check-new-uploads.d.ts +55 -0
- package/dist/tools/check-new-uploads.d.ts.map +1 -0
- package/dist/tools/check-new-uploads.js +74 -0
- package/dist/tools/check-new-uploads.js.map +1 -0
- package/dist/tools/clips.d.ts +15 -0
- package/dist/tools/clips.d.ts.map +1 -1
- package/dist/tools/clips.js +5 -5
- package/dist/tools/clips.js.map +1 -1
- package/dist/tools/detect-scenes.d.ts +38 -0
- package/dist/tools/detect-scenes.d.ts.map +1 -0
- package/dist/tools/detect-scenes.js +66 -0
- package/dist/tools/detect-scenes.js.map +1 -0
- package/dist/tools/export-data.d.ts +85 -0
- package/dist/tools/export-data.d.ts.map +1 -0
- package/dist/tools/export-data.js +99 -0
- package/dist/tools/export-data.js.map +1 -0
- package/dist/tools/frames-subtitles.d.ts.map +1 -1
- package/dist/tools/frames-subtitles.js +16 -8
- package/dist/tools/frames-subtitles.js.map +1 -1
- package/dist/tools/frames.d.ts +73 -0
- package/dist/tools/frames.d.ts.map +1 -0
- package/dist/tools/frames.js +136 -0
- package/dist/tools/frames.js.map +1 -0
- package/dist/tools/get-channel-info.d.ts +2 -0
- package/dist/tools/get-channel-info.d.ts.map +1 -1
- package/dist/tools/get-channel-info.js +4 -0
- package/dist/tools/get-channel-info.js.map +1 -1
- package/dist/tools/get-comments.d.ts +5 -5
- package/dist/tools/get-coverage.d.ts +5 -5
- package/dist/tools/get-sponsor-segments.d.ts +49 -0
- package/dist/tools/get-sponsor-segments.d.ts.map +1 -0
- package/dist/tools/get-sponsor-segments.js +61 -0
- package/dist/tools/get-sponsor-segments.js.map +1 -0
- package/dist/tools/get-thumbnail.d.ts.map +1 -1
- package/dist/tools/get-thumbnail.js +2 -1
- package/dist/tools/get-thumbnail.js.map +1 -1
- package/dist/tools/get-transcript.d.ts +1 -0
- package/dist/tools/get-transcript.d.ts.map +1 -1
- package/dist/tools/get-transcript.js +11 -4
- package/dist/tools/get-transcript.js.map +1 -1
- package/dist/tools/get-video-details.d.ts +118 -0
- package/dist/tools/get-video-details.d.ts.map +1 -0
- package/dist/tools/get-video-details.js +147 -0
- package/dist/tools/get-video-details.js.map +1 -0
- package/dist/tools/gif-concat.d.ts +49 -0
- package/dist/tools/gif-concat.d.ts.map +1 -0
- package/dist/tools/gif-concat.js +96 -0
- package/dist/tools/gif-concat.js.map +1 -0
- package/dist/tools/harvest-channel.d.ts +5 -5
- package/dist/tools/harvest-comments.d.ts +5 -5
- package/dist/tools/harvest-details.d.ts +84 -0
- package/dist/tools/harvest-details.d.ts.map +1 -0
- package/dist/tools/harvest-details.js +71 -0
- package/dist/tools/harvest-details.js.map +1 -0
- package/dist/tools/list-captions.d.ts +17 -0
- package/dist/tools/list-captions.d.ts.map +1 -0
- package/dist/tools/list-captions.js +60 -0
- package/dist/tools/list-captions.js.map +1 -0
- package/dist/tools/list-channel-playlists.d.ts +30 -0
- package/dist/tools/list-channel-playlists.d.ts.map +1 -0
- package/dist/tools/list-channel-playlists.js +44 -0
- package/dist/tools/list-channel-playlists.js.map +1 -0
- package/dist/tools/query-comments.d.ts +5 -5
- package/dist/tools/query-videos.d.ts +5 -5
- package/dist/tools/search-channels.d.ts +6 -0
- package/dist/tools/search-channels.d.ts.map +1 -1
- package/dist/tools/transcribe-video.d.ts +28 -0
- package/dist/tools/transcribe-video.d.ts.map +1 -0
- package/dist/tools/transcribe-video.js +52 -0
- package/dist/tools/transcribe-video.js.map +1 -0
- package/dist/utils/brain-fusion.d.ts +24 -0
- package/dist/utils/brain-fusion.d.ts.map +1 -0
- package/dist/utils/brain-fusion.js +18 -0
- package/dist/utils/brain-fusion.js.map +1 -0
- package/dist/utils/channel-playlists.d.ts +21 -0
- package/dist/utils/channel-playlists.d.ts.map +1 -0
- package/dist/utils/channel-playlists.js +56 -0
- package/dist/utils/channel-playlists.js.map +1 -0
- package/dist/utils/clips.d.ts.map +1 -1
- package/dist/utils/clips.js +3 -17
- package/dist/utils/clips.js.map +1 -1
- package/dist/utils/concat.d.ts +19 -0
- package/dist/utils/concat.d.ts.map +1 -0
- package/dist/utils/concat.js +135 -0
- package/dist/utils/concat.js.map +1 -0
- package/dist/utils/contact-sheet.d.ts +28 -0
- package/dist/utils/contact-sheet.d.ts.map +1 -0
- package/dist/utils/contact-sheet.js +65 -0
- package/dist/utils/contact-sheet.js.map +1 -0
- package/dist/utils/detail-batch.d.ts +25 -0
- package/dist/utils/detail-batch.d.ts.map +1 -0
- package/dist/utils/detail-batch.js +58 -0
- package/dist/utils/detail-batch.js.map +1 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +10 -0
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/export-data.d.ts +41 -0
- package/dist/utils/export-data.d.ts.map +1 -0
- package/dist/utils/export-data.js +114 -0
- package/dist/utils/export-data.js.map +1 -0
- package/dist/utils/export-format.d.ts +23 -0
- package/dist/utils/export-format.d.ts.map +1 -0
- package/dist/utils/export-format.js +59 -0
- package/dist/utils/export-format.js.map +1 -0
- package/dist/utils/export-load.d.ts +28 -0
- package/dist/utils/export-load.d.ts.map +1 -0
- package/dist/utils/export-load.js +70 -0
- package/dist/utils/export-load.js.map +1 -0
- package/dist/utils/export-rows.d.ts +35 -0
- package/dist/utils/export-rows.d.ts.map +1 -0
- package/dist/utils/export-rows.js +69 -0
- package/dist/utils/export-rows.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +26 -0
- package/dist/utils/ffmpeg.d.ts.map +1 -0
- package/dist/utils/ffmpeg.js +28 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +4 -0
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/frame-plan.d.ts +24 -0
- package/dist/utils/frame-plan.d.ts.map +1 -0
- package/dist/utils/frame-plan.js +58 -0
- package/dist/utils/frame-plan.js.map +1 -0
- package/dist/utils/frames.d.ts +32 -0
- package/dist/utils/frames.d.ts.map +1 -0
- package/dist/utils/frames.js +60 -0
- package/dist/utils/frames.js.map +1 -0
- package/dist/utils/gif.d.ts +31 -0
- package/dist/utils/gif.d.ts.map +1 -0
- package/dist/utils/gif.js +77 -0
- package/dist/utils/gif.js.map +1 -0
- package/dist/utils/harvest-catalog.d.ts +10 -0
- package/dist/utils/harvest-catalog.d.ts.map +1 -1
- package/dist/utils/harvest-catalog.js +14 -5
- package/dist/utils/harvest-catalog.js.map +1 -1
- package/dist/utils/harvest-details.d.ts +30 -0
- package/dist/utils/harvest-details.d.ts.map +1 -0
- package/dist/utils/harvest-details.js +98 -0
- package/dist/utils/harvest-details.js.map +1 -0
- package/dist/utils/local-transcription.d.ts +14 -0
- package/dist/utils/local-transcription.d.ts.map +1 -0
- package/dist/utils/local-transcription.js +85 -0
- package/dist/utils/local-transcription.js.map +1 -0
- package/dist/utils/moment.d.ts +24 -0
- package/dist/utils/moment.d.ts.map +1 -0
- package/dist/utils/moment.js +22 -0
- package/dist/utils/moment.js.map +1 -0
- package/dist/utils/most-replayed.d.ts +15 -0
- package/dist/utils/most-replayed.d.ts.map +1 -0
- package/dist/utils/most-replayed.js +25 -0
- package/dist/utils/most-replayed.js.map +1 -0
- package/dist/utils/scenes.d.ts +39 -0
- package/dist/utils/scenes.d.ts.map +1 -0
- package/dist/utils/scenes.js +83 -0
- package/dist/utils/scenes.js.map +1 -0
- package/dist/utils/seconds.d.ts +5 -0
- package/dist/utils/seconds.d.ts.map +1 -0
- package/dist/utils/seconds.js +7 -0
- package/dist/utils/seconds.js.map +1 -0
- package/dist/utils/sponsorblock.d.ts +24 -0
- package/dist/utils/sponsorblock.d.ts.map +1 -0
- package/dist/utils/sponsorblock.js +153 -0
- package/dist/utils/sponsorblock.js.map +1 -0
- package/dist/utils/transcript-cache.d.ts +5 -1
- package/dist/utils/transcript-cache.d.ts.map +1 -1
- package/dist/utils/transcript-cache.js +31 -50
- package/dist/utils/transcript-cache.js.map +1 -1
- package/dist/utils/transcript-files.d.ts +24 -0
- package/dist/utils/transcript-files.d.ts.map +1 -0
- package/dist/utils/transcript-files.js +61 -0
- package/dist/utils/transcript-files.js.map +1 -0
- package/dist/utils/transcript.d.ts +3 -0
- package/dist/utils/transcript.d.ts.map +1 -1
- package/dist/utils/transcript.js +2 -0
- package/dist/utils/transcript.js.map +1 -1
- package/dist/utils/upload-check.d.ts +38 -0
- package/dist/utils/upload-check.d.ts.map +1 -0
- package/dist/utils/upload-check.js +93 -0
- package/dist/utils/upload-check.js.map +1 -0
- package/dist/utils/validate.d.ts +2 -0
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +4 -0
- package/dist/utils/validate.js.map +1 -1
- package/dist/utils/video-detail-store.d.ts +47 -0
- package/dist/utils/video-detail-store.d.ts.map +1 -0
- package/dist/utils/video-detail-store.js +89 -0
- package/dist/utils/video-detail-store.js.map +1 -0
- package/dist/utils/video-stats-cache.js +1 -1
- package/dist/utils/video-stats-schema.d.ts +187 -0
- package/dist/utils/video-stats-schema.d.ts.map +1 -0
- package/dist/utils/video-stats-schema.js +50 -0
- package/dist/utils/video-stats-schema.js.map +1 -0
- package/dist/utils/video-stats-storage.d.ts +16 -0
- package/dist/utils/video-stats-storage.d.ts.map +1 -0
- package/dist/utils/video-stats-storage.js +37 -0
- package/dist/utils/video-stats-storage.js.map +1 -0
- package/dist/utils/video-stats.d.ts +23 -127
- package/dist/utils/video-stats.d.ts.map +1 -1
- package/dist/utils/video-stats.js +42 -102
- package/dist/utils/video-stats.js.map +1 -1
- package/dist/utils/video-stream.d.ts +21 -0
- package/dist/utils/video-stream.d.ts.map +1 -0
- package/dist/utils/video-stream.js +36 -0
- package/dist/utils/video-stream.js.map +1 -0
- package/dist/utils/whisper.d.ts +39 -0
- package/dist/utils/whisper.d.ts.map +1 -0
- package/dist/utils/whisper.js +137 -0
- package/dist/utils/whisper.js.map +1 -0
- package/dist/utils/youtube-channel.d.ts +4 -0
- package/dist/utils/youtube-channel.d.ts.map +1 -1
- package/dist/utils/youtube-channel.js +6 -0
- package/dist/utils/youtube-channel.js.map +1 -1
- package/dist/utils/youtube-schemas.d.ts +6 -0
- package/dist/utils/youtube-schemas.d.ts.map +1 -1
- package/dist/utils/youtube-schemas.js +2 -0
- package/dist/utils/youtube-schemas.js.map +1 -1
- package/package.json +9 -3
|
@@ -20,6 +20,8 @@ export declare const searchChannelsOutputSchema: {
|
|
|
20
20
|
description: z.ZodString;
|
|
21
21
|
avatarUrl: z.ZodOptional<z.ZodString>;
|
|
22
22
|
bannerUrl: z.ZodOptional<z.ZodString>;
|
|
23
|
+
isVerified: z.ZodOptional<z.ZodBoolean>;
|
|
24
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
23
25
|
}, "strip", z.ZodTypeAny, {
|
|
24
26
|
description: string;
|
|
25
27
|
name: string;
|
|
@@ -27,8 +29,10 @@ export declare const searchChannelsOutputSchema: {
|
|
|
27
29
|
handle: string;
|
|
28
30
|
subscriberCount: number;
|
|
29
31
|
channelUrl: string;
|
|
32
|
+
tags?: string[] | undefined;
|
|
30
33
|
avatarUrl?: string | undefined;
|
|
31
34
|
bannerUrl?: string | undefined;
|
|
35
|
+
isVerified?: boolean | undefined;
|
|
32
36
|
}, {
|
|
33
37
|
description: string;
|
|
34
38
|
name: string;
|
|
@@ -36,8 +40,10 @@ export declare const searchChannelsOutputSchema: {
|
|
|
36
40
|
handle: string;
|
|
37
41
|
subscriberCount: number;
|
|
38
42
|
channelUrl: string;
|
|
43
|
+
tags?: string[] | undefined;
|
|
39
44
|
avatarUrl?: string | undefined;
|
|
40
45
|
bannerUrl?: string | undefined;
|
|
46
|
+
isVerified?: boolean | undefined;
|
|
41
47
|
}>, "many">;
|
|
42
48
|
};
|
|
43
49
|
export declare function searchChannelsHandler({ query, limit, }: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-channels.d.ts","sourceRoot":"","sources":["../../src/tools/search-channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,eAAO,MAAM,oBAAoB;;;CAUhC,CAAC;AAEF,eAAO,MAAM,0BAA0B
|
|
1
|
+
{"version":3,"file":"search-channels.d.ts","sourceRoot":"","sources":["../../src/tools/search-channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,eAAO,MAAM,oBAAoB;;;CAUhC,CAAC;AAEF,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAItC,CAAC;AAEF,wBAAsB,qBAAqB,CAAC,EAC1C,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,cAAc,CAAC,CA4B1B"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Transcribing a video's audio on this machine, for the videos YouTube has no
|
|
5
|
+
* captions for. What it writes is read back by get_transcript, search_transcript,
|
|
6
|
+
* export_subtitles and the channel brains, exactly like captions.
|
|
7
|
+
*/
|
|
8
|
+
export declare const transcribeVideoSchema: {
|
|
9
|
+
video: z.ZodString;
|
|
10
|
+
language: z.ZodDefault<z.ZodString>;
|
|
11
|
+
refresh: z.ZodDefault<z.ZodBoolean>;
|
|
12
|
+
};
|
|
13
|
+
export declare const transcribeVideoOutputSchema: {
|
|
14
|
+
videoId: z.ZodString;
|
|
15
|
+
language: z.ZodString;
|
|
16
|
+
source: z.ZodLiteral<"local-asr">;
|
|
17
|
+
segmentCount: z.ZodNumber;
|
|
18
|
+
wordCount: z.ZodNumber;
|
|
19
|
+
durationSeconds: z.ZodNumber;
|
|
20
|
+
cached: z.ZodBoolean;
|
|
21
|
+
};
|
|
22
|
+
export interface TranscribeVideoArgs {
|
|
23
|
+
video: string;
|
|
24
|
+
language: string;
|
|
25
|
+
refresh: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function transcribeVideoHandler(args: TranscribeVideoArgs): Promise<CallToolResult>;
|
|
28
|
+
//# sourceMappingURL=transcribe-video.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcribe-video.d.ts","sourceRoot":"","sources":["../../src/tools/transcribe-video.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAKzE;;;;GAIG;AAEH,eAAO,MAAM,qBAAqB;;;;CAYjC,CAAC;AAEF,eAAO,MAAM,2BAA2B;;;;;;;;CAQvC,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwB/F"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { toolResult } from '../utils/format.js';
|
|
3
|
+
import { transcribeLocally } from '../utils/local-transcription.js';
|
|
4
|
+
import { formatTimestamp } from '../utils/transcript.js';
|
|
5
|
+
/**
|
|
6
|
+
* Transcribing a video's audio on this machine, for the videos YouTube has no
|
|
7
|
+
* captions for. What it writes is read back by get_transcript, search_transcript,
|
|
8
|
+
* export_subtitles and the channel brains, exactly like captions.
|
|
9
|
+
*/
|
|
10
|
+
export const transcribeVideoSchema = {
|
|
11
|
+
video: z.string().describe('YouTube video ID (e.g., dQw4w9WgXcQ) or full URL'),
|
|
12
|
+
language: z
|
|
13
|
+
.string()
|
|
14
|
+
.default('en')
|
|
15
|
+
.describe('The language spoken in the video, as a tag such as en, fr or pt-BR. Always set it explicitly: guessing the language from the audio is unreliable. Default: en'),
|
|
16
|
+
refresh: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.default(false)
|
|
19
|
+
.describe('Transcribe again even if a transcription was already kept. Default: false'),
|
|
20
|
+
};
|
|
21
|
+
export const transcribeVideoOutputSchema = {
|
|
22
|
+
videoId: z.string(),
|
|
23
|
+
language: z.string(),
|
|
24
|
+
source: z.literal('local-asr'),
|
|
25
|
+
segmentCount: z.number().int(),
|
|
26
|
+
wordCount: z.number().int(),
|
|
27
|
+
durationSeconds: z.number().describe('Where the last transcribed segment ends'),
|
|
28
|
+
cached: z.boolean().describe('True when a kept transcription was returned without redoing it'),
|
|
29
|
+
};
|
|
30
|
+
export async function transcribeVideoHandler(args) {
|
|
31
|
+
const { video, language, refresh } = args;
|
|
32
|
+
const result = await transcribeLocally(video, { language, refresh });
|
|
33
|
+
const wordCount = result.transcript.split(/\s+/).filter(Boolean).length;
|
|
34
|
+
const durationSeconds = result.segments.at(-1)?.end ?? 0;
|
|
35
|
+
const lines = [
|
|
36
|
+
result.cached ? '✓ Already transcribed locally' : '✓ Transcribed locally',
|
|
37
|
+
'',
|
|
38
|
+
`${result.segments.length.toLocaleString()} segments, ${wordCount.toLocaleString()} words, ${formatTimestamp(durationSeconds)} (${result.language})`,
|
|
39
|
+
'',
|
|
40
|
+
'This is a machine transcription, not YouTube captions. Read it with get_transcript, find a phrase with search_transcript, or export it with export_subtitles.',
|
|
41
|
+
];
|
|
42
|
+
return toolResult(lines.join('\n'), {
|
|
43
|
+
videoId: result.videoId,
|
|
44
|
+
language: result.language,
|
|
45
|
+
source: result.source,
|
|
46
|
+
segmentCount: result.segments.length,
|
|
47
|
+
wordCount,
|
|
48
|
+
durationSeconds,
|
|
49
|
+
cached: result.cached,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=transcribe-video.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcribe-video.js","sourceRoot":"","sources":["../../src/tools/transcribe-video.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD;;;;GAIG;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,kDAAkD,CAAC;IAC9E,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CACP,+JAA+J,CAChK;IACH,OAAO,EAAE,CAAC;SACP,OAAO,EAAE;SACT,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,2EAA2E,CAAC;CACzF,CAAC;AAEF,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC;IAC9B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC3B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;IAC/E,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,gEAAgE,CAAC;CAC/F,CAAC;AAQF,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,IAAyB;IACpE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC1C,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IAErE,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACxE,MAAM,eAAe,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IAEzD,MAAM,KAAK,GAAG;QACZ,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,uBAAuB;QACzE,EAAE;QACF,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,cAAc,SAAS,CAAC,cAAc,EAAE,WAAW,eAAe,CAAC,eAAe,CAAC,KAAK,MAAM,CAAC,QAAQ,GAAG;QACpJ,EAAE;QACF,+JAA+J;KAChK,CAAC;IAEF,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAClC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM;QACpC,SAAS;QACT,eAAe;QACf,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merging the results of searching several corpora.
|
|
3
|
+
*
|
|
4
|
+
* A BM25 score depends on the corpus it came from: the same word is rare in one
|
|
5
|
+
* channel and common in another, so a 90 from one and a 3 from another say nothing
|
|
6
|
+
* about which passage answers better. Comparing them would quietly let the channel
|
|
7
|
+
* with the most distinctive vocabulary win every query. Rank is the one thing the
|
|
8
|
+
* lists share, so results are interleaved by rank, best of every list first.
|
|
9
|
+
*/
|
|
10
|
+
export interface RankedList<T> {
|
|
11
|
+
/** What the list came from, carried onto each of its results. */
|
|
12
|
+
source: string;
|
|
13
|
+
/** Best first, as the corpus's own search returned them. */
|
|
14
|
+
items: T[];
|
|
15
|
+
}
|
|
16
|
+
export interface Interleaved<T> {
|
|
17
|
+
source: string;
|
|
18
|
+
item: T;
|
|
19
|
+
}
|
|
20
|
+
/** Up to `limit` results: by rank, then the higher score, then the earlier list. */
|
|
21
|
+
export declare function interleaveByRank<T extends {
|
|
22
|
+
score: number;
|
|
23
|
+
}>(lists: RankedList<T>[], limit: number): Interleaved<T>[];
|
|
24
|
+
//# sourceMappingURL=brain-fusion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-fusion.d.ts","sourceRoot":"","sources":["../../src/utils/brain-fusion.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,KAAK,EAAE,CAAC,EAAE,CAAC;CACZ;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,CAAC,CAAC;CACT;AAOD,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAC1D,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,EACtB,KAAK,EAAE,MAAM,GACZ,WAAW,CAAC,CAAC,CAAC,EAAE,CASlB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merging the results of searching several corpora.
|
|
3
|
+
*
|
|
4
|
+
* A BM25 score depends on the corpus it came from: the same word is rare in one
|
|
5
|
+
* channel and common in another, so a 90 from one and a 3 from another say nothing
|
|
6
|
+
* about which passage answers better. Comparing them would quietly let the channel
|
|
7
|
+
* with the most distinctive vocabulary win every query. Rank is the one thing the
|
|
8
|
+
* lists share, so results are interleaved by rank, best of every list first.
|
|
9
|
+
*/
|
|
10
|
+
/** Up to `limit` results: by rank, then the higher score, then the earlier list. */
|
|
11
|
+
export function interleaveByRank(lists, limit) {
|
|
12
|
+
const candidates = lists.flatMap((list, order) => list.items.map((item, rank) => ({ source: list.source, item, rank, order })));
|
|
13
|
+
return candidates
|
|
14
|
+
.sort((a, b) => a.rank - b.rank || b.item.score - a.item.score || a.order - b.order)
|
|
15
|
+
.slice(0, limit)
|
|
16
|
+
.map(({ source, item }) => ({ source, item }));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=brain-fusion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-fusion.js","sourceRoot":"","sources":["../../src/utils/brain-fusion.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmBH,oFAAoF;AACpF,MAAM,UAAU,gBAAgB,CAC9B,KAAsB,EACtB,KAAa;IAEb,MAAM,UAAU,GAAmB,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAC/D,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAC7E,CAAC;IAEF,OAAO,UAAU;SACd,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;SACnF,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;SACf,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playlists a channel has published, from its playlists tab.
|
|
3
|
+
*
|
|
4
|
+
* Verified against yt-dlp 2026.07.04 on a real channel: a flat entry carries the
|
|
5
|
+
* playlist's id, title and url, and no video count, so none is promised here.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_CHANNEL_PLAYLISTS = 200;
|
|
8
|
+
export interface ChannelPlaylist {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ChannelPlaylists {
|
|
14
|
+
channelId: string;
|
|
15
|
+
channelName: string;
|
|
16
|
+
playlists: ChannelPlaylist[];
|
|
17
|
+
/** True when the limit was reached, so the channel may have more. */
|
|
18
|
+
mayHaveMore: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function listChannelPlaylists(channel: string, limit: number): Promise<ChannelPlaylists>;
|
|
21
|
+
//# sourceMappingURL=channel-playlists.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-playlists.d.ts","sourceRoot":"","sources":["../../src/utils/channel-playlists.ts"],"names":[],"mappings":"AAMA;;;;;GAKG;AAEH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAUzC,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,qEAAqE;IACrE,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,gBAAgB,CAAC,CAuC3B"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { TIMEOUTS, parseYtDlpJsonLines, runYtDlp } from './ytdlp.js';
|
|
3
|
+
import { isAbsentTab } from './harvest-catalog.js';
|
|
4
|
+
import { getChannelInfo } from './youtube-channel.js';
|
|
5
|
+
import { resolveListTarget } from './youtube-url.js';
|
|
6
|
+
/**
|
|
7
|
+
* The playlists a channel has published, from its playlists tab.
|
|
8
|
+
*
|
|
9
|
+
* Verified against yt-dlp 2026.07.04 on a real channel: a flat entry carries the
|
|
10
|
+
* playlist's id, title and url, and no video count, so none is promised here.
|
|
11
|
+
*/
|
|
12
|
+
export const MAX_CHANNEL_PLAYLISTS = 200;
|
|
13
|
+
const PLAYLIST_PRINT_TEMPLATE = '%(.{id,title,url})j';
|
|
14
|
+
const playlistEntrySchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
title: z.string().nullish(),
|
|
17
|
+
url: z.string().nullish(),
|
|
18
|
+
});
|
|
19
|
+
export async function listChannelPlaylists(channel, limit) {
|
|
20
|
+
const info = await getChannelInfo(channel);
|
|
21
|
+
let stdout;
|
|
22
|
+
try {
|
|
23
|
+
stdout = await runYtDlp([
|
|
24
|
+
'--skip-download',
|
|
25
|
+
'--flat-playlist',
|
|
26
|
+
'--lazy-playlist',
|
|
27
|
+
'--print',
|
|
28
|
+
PLAYLIST_PRINT_TEMPLATE,
|
|
29
|
+
'--playlist-items',
|
|
30
|
+
`1:${String(limit)}`,
|
|
31
|
+
], {
|
|
32
|
+
label: 'list_channel_playlists',
|
|
33
|
+
timeoutMs: TIMEOUTS.transcript,
|
|
34
|
+
target: resolveListTarget(`${info.channelUrl}/playlists`),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// A channel without a playlists tab has no playlists: an answer, not a failure.
|
|
39
|
+
if (isAbsentTab(error))
|
|
40
|
+
stdout = '';
|
|
41
|
+
else
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
const playlists = parseYtDlpJsonLines(stdout, playlistEntrySchema).map((entry) => ({
|
|
45
|
+
id: entry.id,
|
|
46
|
+
title: entry.title ?? 'Untitled',
|
|
47
|
+
url: entry.url ?? `https://www.youtube.com/playlist?list=${entry.id}`,
|
|
48
|
+
}));
|
|
49
|
+
return {
|
|
50
|
+
channelId: info.channelId,
|
|
51
|
+
channelName: info.name,
|
|
52
|
+
playlists,
|
|
53
|
+
mayHaveMore: playlists.length >= limit,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=channel-playlists.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-playlists.js","sourceRoot":"","sources":["../../src/utils/channel-playlists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC,MAAM,uBAAuB,GAAG,qBAAqB,CAAC;AAEtD,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IAC3B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;CAC1B,CAAC,CAAC;AAgBH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,KAAa;IAEb,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;IAE3C,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,QAAQ,CACrB;YACE,iBAAiB;YACjB,iBAAiB;YACjB,iBAAiB;YACjB,SAAS;YACT,uBAAuB;YACvB,kBAAkB;YAClB,KAAK,MAAM,CAAC,KAAK,CAAC,EAAE;SACrB,EACD;YACE,KAAK,EAAE,wBAAwB;YAC/B,SAAS,EAAE,QAAQ,CAAC,UAAU;YAC9B,MAAM,EAAE,iBAAiB,CAAC,GAAG,IAAI,CAAC,UAAU,YAAY,CAAC;SAC1D,CACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,gFAAgF;QAChF,IAAI,WAAW,CAAC,KAAK,CAAC;YAAE,MAAM,GAAG,EAAE,CAAC;;YAC/B,MAAM,KAAK,CAAC;IACnB,CAAC;IAED,MAAM,SAAS,GAAG,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjF,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,UAAU;QAChC,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,yCAAyC,KAAK,CAAC,EAAE,EAAE;KACtE,CAAC,CAAC,CAAC;IAEJ,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,WAAW,EAAE,IAAI,CAAC,IAAI;QACtB,SAAS;QACT,WAAW,EAAE,SAAS,CAAC,MAAM,IAAI,KAAK;KACvC,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clips.d.ts","sourceRoot":"","sources":["../../src/utils/clips.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"clips.d.ts","sourceRoot":"","sources":["../../src/utils/clips.ts"],"names":[],"mappings":"AA+BA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgBhE;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAwCjF;AAMD,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAChE,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CA2DrB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,KAAK,GAAG,KAAK,CAAA;CAAE,GACrD,OAAO,CAAC,WAAW,CAAC,CAkDtB;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC"}
|
package/dist/utils/clips.js
CHANGED
|
@@ -10,6 +10,7 @@ import { formatSrtTimestamp } from './transcript.js';
|
|
|
10
10
|
import { getChapters, getVideoInfo } from './youtube.js';
|
|
11
11
|
import { resolveChapter } from './chapters.js';
|
|
12
12
|
import { dataDir } from './paths.js';
|
|
13
|
+
import { DETAIL_STREAM_SELECTOR, FFMPEG_NETWORK_INPUT_ARGS, resolveStreamUrl, } from './video-stream.js';
|
|
13
14
|
/**
|
|
14
15
|
* Pulling usable excerpts out of a video for editing.
|
|
15
16
|
*
|
|
@@ -147,26 +148,11 @@ export async function extractFrame(video, timestamp, options) {
|
|
|
147
148
|
throw new YouTubeError('INVALID_INPUT', `${Math.round(timestamp)}s is past the end of this ${info.duration}s video.`, { nextStep: 'Pick a timestamp inside the video.' });
|
|
148
149
|
}
|
|
149
150
|
await mkdir(targetDir, { recursive: true });
|
|
150
|
-
const
|
|
151
|
-
const mediaUrl = (await runYtDlp(['-f', 'bestvideo[height<=1080]/best', '--get-url', '--no-playlist'], {
|
|
152
|
-
label: 'extract_frame (resolve)',
|
|
153
|
-
target: url,
|
|
154
|
-
}))
|
|
155
|
-
.trim()
|
|
156
|
-
.split('\n')[0];
|
|
157
|
-
// ffmpeg accepts file:, pipe:, concat: and more as inputs; the only thing
|
|
158
|
-
// yt-dlp should have resolved is an https media URL, and the allowlist
|
|
159
|
-
// below makes ffmpeg refuse anything else even if it had.
|
|
160
|
-
if (!mediaUrl?.startsWith('https://')) {
|
|
161
|
-
throw new YouTubeError('YTDLP_FAILED', 'Could not resolve a video stream for this video.', {
|
|
162
|
-
nextStep: 'Check the video is playable, or call check_health.',
|
|
163
|
-
});
|
|
164
|
-
}
|
|
151
|
+
const mediaUrl = await resolveStreamUrl(info.id, DETAIL_STREAM_SELECTOR, 'extract_frame');
|
|
165
152
|
const filePath = join(targetDir, `${safeStem(info.title, info.id)} [${Math.round(timestamp)}s].${options.format}`);
|
|
166
153
|
try {
|
|
167
154
|
await execa('ffmpeg', [
|
|
168
|
-
|
|
169
|
-
'https,tls,tcp,crypto',
|
|
155
|
+
...FFMPEG_NETWORK_INPUT_ARGS,
|
|
170
156
|
// -ss before -i seeks without decoding everything up to that point.
|
|
171
157
|
'-ss',
|
|
172
158
|
String(timestamp),
|
package/dist/utils/clips.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clips.js","sourceRoot":"","sources":["../../src/utils/clips.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"clips.js","sourceRoot":"","sources":["../../src/utils/clips.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joining clips into one video, in the order given.
|
|
3
|
+
*
|
|
4
|
+
* The inputs are files already on this machine, typically what extract_clip wrote.
|
|
5
|
+
* They are re-encoded rather than copied, because clips cut at different moments or
|
|
6
|
+
* from different sources rarely share the exact stream parameters a copy needs.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MIN_CONCAT_FILES = 2;
|
|
9
|
+
export declare const MAX_CONCAT_FILES = 20;
|
|
10
|
+
export interface ConcatResult {
|
|
11
|
+
filePath: string;
|
|
12
|
+
clips: number;
|
|
13
|
+
bytes: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function concatClips(inputs: string[], options: {
|
|
16
|
+
name?: string;
|
|
17
|
+
outputDir?: string;
|
|
18
|
+
}): Promise<ConcatResult>;
|
|
19
|
+
//# sourceMappingURL=concat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concat.d.ts","sourceRoot":"","sources":["../../src/utils/concat.ts"],"names":[],"mappings":"AAWA;;;;;;GAMG;AAEH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAKnC,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AA8DD,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,OAAO,CAAC,YAAY,CAAC,CAwEvB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { safeStem } from './clips.js';
|
|
5
|
+
import { YouTubeError } from './errors.js';
|
|
6
|
+
import { runFfmpeg } from './ffmpeg.js';
|
|
7
|
+
import { dataDir } from './paths.js';
|
|
8
|
+
import { requireFfmpeg } from './preflight.js';
|
|
9
|
+
import { assertInsideHome, resolveOutputDir } from './validate.js';
|
|
10
|
+
import { TIMEOUTS } from './ytdlp.js';
|
|
11
|
+
/**
|
|
12
|
+
* Joining clips into one video, in the order given.
|
|
13
|
+
*
|
|
14
|
+
* The inputs are files already on this machine, typically what extract_clip wrote.
|
|
15
|
+
* They are re-encoded rather than copied, because clips cut at different moments or
|
|
16
|
+
* from different sources rarely share the exact stream parameters a copy needs.
|
|
17
|
+
*/
|
|
18
|
+
export const MIN_CONCAT_FILES = 2;
|
|
19
|
+
export const MAX_CONCAT_FILES = 20;
|
|
20
|
+
const MONTAGES_DIR = dataDir('montages');
|
|
21
|
+
const NAME_HASH_LENGTH = 8;
|
|
22
|
+
/** A path as one entry of ffmpeg's concat list: single-quoted, with any quote escaped. */
|
|
23
|
+
function listEntry(path) {
|
|
24
|
+
return `file '${path.replace(/'/g, `'\\''`)}'`;
|
|
25
|
+
}
|
|
26
|
+
async function exists(path) {
|
|
27
|
+
try {
|
|
28
|
+
await stat(path);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Where the result may go.
|
|
37
|
+
*
|
|
38
|
+
* Never onto one of the clips being joined: ffmpeg would read and write the same file
|
|
39
|
+
* and destroy it. And never over a file the caller did not make: a name the caller
|
|
40
|
+
* chose can collide with anything in the folder, so an existing file is refused. A name
|
|
41
|
+
* this server derived from the inputs is its own, so repeating the call rewrites it.
|
|
42
|
+
*/
|
|
43
|
+
async function assertSafeOutput(filePath, inputs, chosenName) {
|
|
44
|
+
if (inputs.includes(filePath)) {
|
|
45
|
+
throw new YouTubeError('INVALID_INPUT', `The result would overwrite ${filePath}, a clip being joined.`, {
|
|
46
|
+
nextStep: 'Choose a different name, or another outputDir.',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (chosenName && (await exists(filePath))) {
|
|
50
|
+
throw new YouTubeError('INVALID_INPUT', `${filePath} already exists.`, {
|
|
51
|
+
nextStep: 'Choose a different name, or remove the existing file first.',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function assertReadableFile(path) {
|
|
56
|
+
let isFile;
|
|
57
|
+
try {
|
|
58
|
+
isFile = (await stat(path)).isFile();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new YouTubeError('NOT_FOUND', `There is no file at ${path}.`, {
|
|
62
|
+
nextStep: 'Pass the paths extract_clip returned, in the order you want them.',
|
|
63
|
+
cause: error,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (!isFile) {
|
|
67
|
+
throw new YouTubeError('INVALID_INPUT', `${path} is not a file.`, {
|
|
68
|
+
nextStep: 'Pass paths to video files, not folders.',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function concatClips(inputs, options) {
|
|
73
|
+
if (inputs.length < MIN_CONCAT_FILES || inputs.length > MAX_CONCAT_FILES) {
|
|
74
|
+
throw new YouTubeError('INVALID_INPUT', `Join between ${String(MIN_CONCAT_FILES)} and ${String(MAX_CONCAT_FILES)} clips at a time.`, { nextStep: 'Join in stages: concatenate groups, then concatenate the results.' });
|
|
75
|
+
}
|
|
76
|
+
await requireFfmpeg('Joining clips');
|
|
77
|
+
const targetDir = resolveOutputDir(options.outputDir, MONTAGES_DIR);
|
|
78
|
+
const paths = inputs.map((input) => assertInsideHome(input, 'clip'));
|
|
79
|
+
for (const path of paths)
|
|
80
|
+
await assertReadableFile(path);
|
|
81
|
+
const hash = createHash('sha256')
|
|
82
|
+
.update(paths.join('\n'))
|
|
83
|
+
.digest('hex')
|
|
84
|
+
.slice(0, NAME_HASH_LENGTH);
|
|
85
|
+
const stem = options.name === undefined ? `montage-${hash}` : safeStem(options.name, `montage-${hash}`);
|
|
86
|
+
const filePath = join(targetDir, `${stem}.mp4`);
|
|
87
|
+
await assertSafeOutput(filePath, paths, options.name !== undefined);
|
|
88
|
+
const resultExisted = await exists(filePath);
|
|
89
|
+
await mkdir(targetDir, { recursive: true });
|
|
90
|
+
// One folder per call: two calls joining the same clips must not share, and then
|
|
91
|
+
// delete, each other's list.
|
|
92
|
+
const workDir = await mkdtemp(join(targetDir, '.concat-'));
|
|
93
|
+
const listPath = join(workDir, 'list.txt');
|
|
94
|
+
await writeFile(listPath, `${paths.map(listEntry).join('\n')}\n`, 'utf-8');
|
|
95
|
+
try {
|
|
96
|
+
await runFfmpeg([
|
|
97
|
+
// The list is the only thing ffmpeg reads, and it names files only.
|
|
98
|
+
'-protocol_whitelist',
|
|
99
|
+
'file',
|
|
100
|
+
'-f',
|
|
101
|
+
'concat',
|
|
102
|
+
// Paths in the list are absolute and already checked above.
|
|
103
|
+
'-safe',
|
|
104
|
+
'0',
|
|
105
|
+
'-i',
|
|
106
|
+
listPath,
|
|
107
|
+
'-c:v',
|
|
108
|
+
'libx264',
|
|
109
|
+
'-c:a',
|
|
110
|
+
'aac',
|
|
111
|
+
'-movflags',
|
|
112
|
+
'+faststart',
|
|
113
|
+
'-y',
|
|
114
|
+
filePath,
|
|
115
|
+
], {
|
|
116
|
+
failure: 'ffmpeg could not join those clips.',
|
|
117
|
+
nextStep: 'Check that every file is a playable video, or call check_health.',
|
|
118
|
+
// Joining re-encodes, so it takes as long as the clips do; the client's
|
|
119
|
+
// cancellation is the lever, as for a download.
|
|
120
|
+
timeoutMs: TIMEOUTS.download,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
// Only a file this call began is removed: before it, nothing existed at that path
|
|
125
|
+
// unless the name was derived, and a derived name is this server's own.
|
|
126
|
+
if (!resultExisted)
|
|
127
|
+
await rm(filePath, { force: true });
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await rm(workDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
return { filePath, clips: paths.length, bytes: (await stat(filePath)).size };
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=concat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concat.js","sourceRoot":"","sources":["../../src/utils/concat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FramePlanRequest } from './frame-plan.js';
|
|
2
|
+
/**
|
|
3
|
+
* A whole video's worth of looking in one picture.
|
|
4
|
+
*
|
|
5
|
+
* A model reading a video does not need every frame; it needs to see the shape of
|
|
6
|
+
* it. Tiling a couple of dozen small stills into one image is a single inline
|
|
7
|
+
* block instead of two dozen, and the order of the cells is the order of the
|
|
8
|
+
* timestamps returned beside it, so no text has to be burned into the pixels.
|
|
9
|
+
*/
|
|
10
|
+
/** Each cell's width; four columns come to about 1300px, sharp but still a single block. */
|
|
11
|
+
export declare const SHEET_TILE_WIDTH = 320;
|
|
12
|
+
export declare const MAX_SHEET_FRAMES = 24;
|
|
13
|
+
export declare const DEFAULT_SHEET_COLUMNS = 4;
|
|
14
|
+
export interface ContactSheet {
|
|
15
|
+
bytes: Buffer;
|
|
16
|
+
columns: number;
|
|
17
|
+
rows: number;
|
|
18
|
+
}
|
|
19
|
+
export interface VideoContactSheet extends ContactSheet {
|
|
20
|
+
videoId: string;
|
|
21
|
+
title: string;
|
|
22
|
+
duration: number;
|
|
23
|
+
/** One per cell, left to right then top to bottom. */
|
|
24
|
+
timestamps: number[];
|
|
25
|
+
}
|
|
26
|
+
export declare function buildContactSheet(frames: Buffer[], columns: number): Promise<ContactSheet>;
|
|
27
|
+
export declare function captureContactSheet(video: string, request: FramePlanRequest, columns: number): Promise<VideoContactSheet>;
|
|
28
|
+
//# sourceMappingURL=contact-sheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contact-sheet.d.ts","sourceRoot":"","sources":["../../src/utils/contact-sheet.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAIxD;;;;;;;GAOG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAIvC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAqChG;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,iBAAiB,CAAC,CAkB5B"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { YouTubeError } from './errors.js';
|
|
2
|
+
import { FFMPEG_JPEG_QUALITY, runFfmpeg } from './ffmpeg.js';
|
|
3
|
+
import { captureFrames } from './frames.js';
|
|
4
|
+
import { TIMEOUTS } from './ytdlp.js';
|
|
5
|
+
/**
|
|
6
|
+
* A whole video's worth of looking in one picture.
|
|
7
|
+
*
|
|
8
|
+
* A model reading a video does not need every frame; it needs to see the shape of
|
|
9
|
+
* it. Tiling a couple of dozen small stills into one image is a single inline
|
|
10
|
+
* block instead of two dozen, and the order of the cells is the order of the
|
|
11
|
+
* timestamps returned beside it, so no text has to be burned into the pixels.
|
|
12
|
+
*/
|
|
13
|
+
/** Each cell's width; four columns come to about 1300px, sharp but still a single block. */
|
|
14
|
+
export const SHEET_TILE_WIDTH = 320;
|
|
15
|
+
export const MAX_SHEET_FRAMES = 24;
|
|
16
|
+
export const DEFAULT_SHEET_COLUMNS = 4;
|
|
17
|
+
const TILE_GAP = 4;
|
|
18
|
+
export async function buildContactSheet(frames, columns) {
|
|
19
|
+
if (frames.length === 0) {
|
|
20
|
+
throw new YouTubeError('INVALID_INPUT', 'A contact sheet needs at least one frame.');
|
|
21
|
+
}
|
|
22
|
+
// A grid wider than the frames it holds is only empty margin.
|
|
23
|
+
const width = Math.max(1, Math.min(columns, frames.length));
|
|
24
|
+
const rows = Math.ceil(frames.length / width);
|
|
25
|
+
const failure = 'ffmpeg could not assemble the contact sheet.';
|
|
26
|
+
const nextStep = 'Try fewer frames, or check that ffmpeg is working.';
|
|
27
|
+
const { stdout } = await runFfmpeg([
|
|
28
|
+
'-f',
|
|
29
|
+
'image2pipe',
|
|
30
|
+
'-c:v',
|
|
31
|
+
'mjpeg',
|
|
32
|
+
'-i',
|
|
33
|
+
'pipe:0',
|
|
34
|
+
'-vf',
|
|
35
|
+
`tile=${String(width)}x${String(rows)}:margin=${String(TILE_GAP)}:padding=${String(TILE_GAP)}:color=black`,
|
|
36
|
+
'-frames:v',
|
|
37
|
+
'1',
|
|
38
|
+
'-q:v',
|
|
39
|
+
FFMPEG_JPEG_QUALITY,
|
|
40
|
+
'-f',
|
|
41
|
+
'image2pipe',
|
|
42
|
+
'-c:v',
|
|
43
|
+
'mjpeg',
|
|
44
|
+
'pipe:1',
|
|
45
|
+
], { failure, nextStep, timeoutMs: TIMEOUTS.transcript, input: Buffer.concat(frames) });
|
|
46
|
+
if (stdout.byteLength === 0)
|
|
47
|
+
throw new YouTubeError('YTDLP_FAILED', failure, { nextStep });
|
|
48
|
+
return { bytes: stdout, columns: width, rows };
|
|
49
|
+
}
|
|
50
|
+
export async function captureContactSheet(video, request, columns) {
|
|
51
|
+
const batch = await captureFrames(video, request, {
|
|
52
|
+
width: SHEET_TILE_WIDTH,
|
|
53
|
+
maxFrames: MAX_SHEET_FRAMES,
|
|
54
|
+
label: 'Contact sheet',
|
|
55
|
+
});
|
|
56
|
+
const sheet = await buildContactSheet(batch.frames.map((frame) => frame.bytes), columns);
|
|
57
|
+
return {
|
|
58
|
+
...sheet,
|
|
59
|
+
videoId: batch.videoId,
|
|
60
|
+
title: batch.title,
|
|
61
|
+
duration: batch.duration,
|
|
62
|
+
timestamps: batch.frames.map((frame) => frame.timestamp),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=contact-sheet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contact-sheet.js","sourceRoot":"","sources":["../../src/utils/contact-sheet.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC;;;;;;;GAOG;AAEH,4FAA4F;AAC5F,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AACpC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,MAAM,QAAQ,GAAG,CAAC,CAAC;AAgBnB,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAgB,EAAE,OAAe;IACvE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,YAAY,CAAC,eAAe,EAAE,2CAA2C,CAAC,CAAC;IACvF,CAAC;IAED,8DAA8D;IAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC;IAE9C,MAAM,OAAO,GAAG,8CAA8C,CAAC;IAC/D,MAAM,QAAQ,GAAG,oDAAoD,CAAC;IAEtE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAChC;QACE,IAAI;QACJ,YAAY;QACZ,MAAM;QACN,OAAO;QACP,IAAI;QACJ,QAAQ;QACR,KAAK;QACL,QAAQ,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,MAAM,CAAC,QAAQ,CAAC,YAAY,MAAM,CAAC,QAAQ,CAAC,cAAc;QAC1G,WAAW;QACX,GAAG;QACH,MAAM;QACN,mBAAmB;QACnB,IAAI;QACJ,YAAY;QACZ,MAAM;QACN,OAAO;QACP,QAAQ;KACT,EACD,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CACpF,CAAC;IAEF,IAAI,MAAM,CAAC,UAAU,KAAK,CAAC;QAAE,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC3F,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,KAAa,EACb,OAAyB,EACzB,OAAe;IAEf,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE;QAChD,KAAK,EAAE,gBAAgB;QACvB,SAAS,EAAE,gBAAgB;QAC3B,KAAK,EAAE,eAAe;KACvB,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,iBAAiB,CACnC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EACxC,OAAO,CACR,CAAC;IAEF,OAAO;QACL,GAAG,KAAK;QACR,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;KACzD,CAAC;AACJ,CAAC"}
|