youtube-knowledge-mcp 2.2.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -36
- package/dist/brain-schemas.d.ts +10 -0
- package/dist/brain-schemas.d.ts.map +1 -1
- package/dist/harvest-schemas.d.ts +75 -0
- package/dist/harvest-schemas.d.ts.map +1 -0
- package/dist/harvest-schemas.js +111 -0
- package/dist/harvest-schemas.js.map +1 -0
- 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/harvest-tools.d.ts +10 -0
- package/dist/registry/harvest-tools.d.ts.map +1 -0
- package/dist/registry/harvest-tools.js +109 -0
- package/dist/registry/harvest-tools.js.map +1 -0
- package/dist/registry/health-tools.d.ts.map +1 -1
- package/dist/registry/health-tools.js +18 -0
- package/dist/registry/health-tools.js.map +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +6 -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 +49 -1
- 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 +245 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +58 -0
- package/dist/schemas.js.map +1 -1
- package/dist/thumbnail-schemas.d.ts +38 -28
- package/dist/thumbnail-schemas.d.ts.map +1 -1
- package/dist/tools/ask-brain.js +1 -1
- package/dist/tools/ask-brain.js.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/brain-info.js +1 -1
- package/dist/tools/brain-info.js.map +1 -1
- package/dist/tools/check-health.d.ts +75 -6
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +114 -6
- 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/fetch-channel-thumbnails.d.ts +8 -8
- package/dist/tools/fetch-videos.d.ts +15 -4
- package/dist/tools/fetch-videos.d.ts.map +1 -1
- package/dist/tools/fetch-videos.js +63 -22
- package/dist/tools/fetch-videos.js.map +1 -1
- 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 +238 -6
- package/dist/tools/get-comments.d.ts.map +1 -1
- package/dist/tools/get-comments.js +85 -21
- package/dist/tools/get-comments.js.map +1 -1
- package/dist/tools/get-coverage.d.ts +122 -0
- package/dist/tools/get-coverage.d.ts.map +1 -0
- package/dist/tools/get-coverage.js +105 -0
- package/dist/tools/get-coverage.js.map +1 -0
- 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 +92 -0
- package/dist/tools/harvest-channel.d.ts.map +1 -0
- package/dist/tools/harvest-channel.js +62 -0
- package/dist/tools/harvest-channel.js.map +1 -0
- package/dist/tools/harvest-comments.d.ts +81 -0
- package/dist/tools/harvest-comments.d.ts.map +1 -0
- package/dist/tools/harvest-comments.js +115 -0
- package/dist/tools/harvest-comments.js.map +1 -0
- 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/library.js +1 -1
- package/dist/tools/library.js.map +1 -1
- 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/list-channel-thumbnails.d.ts +10 -10
- package/dist/tools/list-channel-thumbnails.js +1 -1
- package/dist/tools/list-channel-thumbnails.js.map +1 -1
- package/dist/tools/list-library.js +1 -1
- package/dist/tools/list-library.js.map +1 -1
- package/dist/tools/prune-harvest.d.ts +59 -0
- package/dist/tools/prune-harvest.d.ts.map +1 -0
- package/dist/tools/prune-harvest.js +108 -0
- package/dist/tools/prune-harvest.js.map +1 -0
- package/dist/tools/query-comments.d.ts +125 -0
- package/dist/tools/query-comments.d.ts.map +1 -0
- package/dist/tools/query-comments.js +106 -0
- package/dist/tools/query-comments.js.map +1 -0
- package/dist/tools/query-videos.d.ts +118 -0
- package/dist/tools/query-videos.d.ts.map +1 -0
- package/dist/tools/query-videos.js +97 -0
- package/dist/tools/query-videos.js.map +1 -0
- package/dist/tools/repair-store.d.ts +34 -0
- package/dist/tools/repair-store.d.ts.map +1 -0
- package/dist/tools/repair-store.js +58 -0
- package/dist/tools/repair-store.js.map +1 -0
- package/dist/tools/search-channels.d.ts +6 -0
- package/dist/tools/search-channels.d.ts.map +1 -1
- package/dist/tools/search-channels.js +5 -1
- package/dist/tools/search-channels.js.map +1 -1
- package/dist/tools/search-videos.js +1 -1
- package/dist/tools/search-videos.js.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/comment-store.d.ts +71 -0
- package/dist/utils/comment-store.d.ts.map +1 -0
- package/dist/utils/comment-store.js +129 -0
- package/dist/utils/comment-store.js.map +1 -0
- package/dist/utils/comment-threads.d.ts +51 -0
- package/dist/utils/comment-threads.d.ts.map +1 -0
- package/dist/utils/comment-threads.js +76 -0
- package/dist/utils/comment-threads.js.map +1 -0
- 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/coverage-text.d.ts +3 -0
- package/dist/utils/coverage-text.d.ts.map +1 -0
- package/dist/utils/coverage-text.js +62 -0
- package/dist/utils/coverage-text.js.map +1 -0
- package/dist/utils/coverage.d.ts +57 -0
- package/dist/utils/coverage.d.ts.map +1 -0
- package/dist/utils/coverage.js +142 -0
- package/dist/utils/coverage.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 +19 -1
- 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 +23 -1
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +5 -1
- 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 +54 -0
- package/dist/utils/harvest-catalog.d.ts.map +1 -0
- package/dist/utils/harvest-catalog.js +173 -0
- package/dist/utils/harvest-catalog.js.map +1 -0
- 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/harvest-lock.d.ts +3 -0
- package/dist/utils/harvest-lock.d.ts.map +1 -0
- package/dist/utils/harvest-lock.js +26 -0
- package/dist/utils/harvest-lock.js.map +1 -0
- package/dist/utils/harvest-receipts.d.ts +14 -0
- package/dist/utils/harvest-receipts.d.ts.map +1 -0
- package/dist/utils/harvest-receipts.js +110 -0
- package/dist/utils/harvest-receipts.js.map +1 -0
- package/dist/utils/listing-cursor.d.ts +31 -0
- package/dist/utils/listing-cursor.d.ts.map +1 -0
- package/dist/utils/listing-cursor.js +39 -0
- package/dist/utils/listing-cursor.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/pot-preflight.d.ts +53 -0
- package/dist/utils/pot-preflight.d.ts.map +1 -0
- package/dist/utils/pot-preflight.js +146 -0
- package/dist/utils/pot-preflight.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/sleep.d.ts +9 -0
- package/dist/utils/sleep.d.ts.map +1 -0
- package/dist/utils/sleep.js +25 -0
- package/dist/utils/sleep.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/store-health.d.ts +37 -0
- package/dist/utils/store-health.d.ts.map +1 -0
- package/dist/utils/store-health.js +92 -0
- package/dist/utils/store-health.js.map +1 -0
- package/dist/utils/store-migrations.d.ts +33 -0
- package/dist/utils/store-migrations.d.ts.map +1 -0
- package/dist/utils/store-migrations.js +41 -0
- package/dist/utils/store-migrations.js.map +1 -0
- package/dist/utils/store-paths.d.ts +33 -0
- package/dist/utils/store-paths.d.ts.map +1 -0
- package/dist/utils/store-paths.js +48 -0
- package/dist/utils/store-paths.js.map +1 -0
- package/dist/utils/store-rows.d.ts +22 -0
- package/dist/utils/store-rows.d.ts.map +1 -0
- package/dist/utils/store-rows.js +62 -0
- package/dist/utils/store-rows.js.map +1 -0
- package/dist/utils/store-schema.d.ts +16 -0
- package/dist/utils/store-schema.d.ts.map +1 -0
- package/dist/utils/store-schema.js +168 -0
- package/dist/utils/store-schema.js.map +1 -0
- package/dist/utils/store.d.ts +34 -0
- package/dist/utils/store.d.ts.map +1 -0
- package/dist/utils/store.js +157 -0
- package/dist/utils/store.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.d.ts +16 -0
- package/dist/utils/video-stats-cache.d.ts.map +1 -0
- package/dist/utils/video-stats-cache.js +81 -0
- package/dist/utils/video-stats-cache.js.map +1 -0
- 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 +82 -0
- package/dist/utils/video-stats.d.ts.map +1 -0
- package/dist/utils/video-stats.js +108 -0
- package/dist/utils/video-stats.js.map +1 -0
- package/dist/utils/video-store.d.ts +69 -0
- package/dist/utils/video-store.d.ts.map +1 -0
- package/dist/utils/video-store.js +77 -0
- package/dist/utils/video-store.js.map +1 -0
- 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 +13 -2
- package/dist/utils/youtube-channel.d.ts.map +1 -1
- package/dist/utils/youtube-channel.js +39 -3
- package/dist/utils/youtube-channel.js.map +1 -1
- package/dist/utils/youtube-schemas.d.ts +104 -0
- package/dist/utils/youtube-schemas.d.ts.map +1 -1
- package/dist/utils/youtube-schemas.js +32 -0
- package/dist/utils/youtube-schemas.js.map +1 -1
- package/dist/utils/youtube-video.d.ts +22 -1
- package/dist/utils/youtube-video.d.ts.map +1 -1
- package/dist/utils/youtube-video.js +40 -28
- package/dist/utils/youtube-video.js.map +1 -1
- package/dist/utils/ytdlp-limiter.d.ts +3 -0
- package/dist/utils/ytdlp-limiter.d.ts.map +1 -1
- package/dist/utils/ytdlp-limiter.js +19 -3
- package/dist/utils/ytdlp-limiter.js.map +1 -1
- package/dist/utils/ytdlp-pacer.d.ts +64 -0
- package/dist/utils/ytdlp-pacer.d.ts.map +1 -0
- package/dist/utils/ytdlp-pacer.js +165 -0
- package/dist/utils/ytdlp-pacer.js.map +1 -0
- package/dist/utils/ytdlp-stream.d.ts +32 -0
- package/dist/utils/ytdlp-stream.d.ts.map +1 -0
- package/dist/utils/ytdlp-stream.js +61 -0
- package/dist/utils/ytdlp-stream.js.map +1 -0
- package/dist/utils/ytdlp.d.ts +10 -0
- package/dist/utils/ytdlp.d.ts.map +1 -1
- package/dist/utils/ytdlp.js +36 -25
- package/dist/utils/ytdlp.js.map +1 -1
- package/package.json +10 -4
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { coverageSchema, harvestScopeSchema } from '../harvest-schemas.js';
|
|
3
|
+
import { assertCoverageConsistent } from './coverage.js';
|
|
4
|
+
import { queryRow, queryRows, sqliteBoolean } from './store-rows.js';
|
|
5
|
+
/**
|
|
6
|
+
* Persisting completeness receipts.
|
|
7
|
+
*
|
|
8
|
+
* Written in the same transaction as the rows they describe. That is the whole
|
|
9
|
+
* reason this store is a database: there is no interval in which a receipt can
|
|
10
|
+
* claim something the store does not hold, so the reconciliation dance that
|
|
11
|
+
* `brain-build.ts` performs between its manifest and its chunks has no
|
|
12
|
+
* analogue here.
|
|
13
|
+
*/
|
|
14
|
+
const receiptRowSchema = z.object({
|
|
15
|
+
scope: harvestScopeSchema,
|
|
16
|
+
target_id: z.string(),
|
|
17
|
+
state: z.string(),
|
|
18
|
+
reason: z.string(),
|
|
19
|
+
have: z.number(),
|
|
20
|
+
expected: z.number().nullable(),
|
|
21
|
+
expected_source: z.string().nullable(),
|
|
22
|
+
source: z.string(),
|
|
23
|
+
limit_applied: z.number().nullable(),
|
|
24
|
+
sort_applied: z.string().nullable(),
|
|
25
|
+
ran_to_exhaustion: sqliteBoolean.nullable(),
|
|
26
|
+
resume_token: z.string().nullable(),
|
|
27
|
+
started_at: z.number(),
|
|
28
|
+
finished_at: z.number().nullable(),
|
|
29
|
+
attempts: z.number(),
|
|
30
|
+
error_code: z.string().nullable(),
|
|
31
|
+
note: z.string().nullable(),
|
|
32
|
+
});
|
|
33
|
+
const UPSERT = `
|
|
34
|
+
INSERT INTO harvest_receipt (
|
|
35
|
+
scope, target_id, state, reason, have, expected, expected_source, source,
|
|
36
|
+
limit_applied, sort_applied, ran_to_exhaustion, resume_token,
|
|
37
|
+
started_at, finished_at, attempts, error_code, note
|
|
38
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
39
|
+
ON CONFLICT(scope, target_id) DO UPDATE SET
|
|
40
|
+
state = excluded.state, reason = excluded.reason, have = excluded.have,
|
|
41
|
+
expected = excluded.expected, expected_source = excluded.expected_source,
|
|
42
|
+
source = excluded.source, limit_applied = excluded.limit_applied,
|
|
43
|
+
sort_applied = excluded.sort_applied, ran_to_exhaustion = excluded.ran_to_exhaustion,
|
|
44
|
+
resume_token = excluded.resume_token, finished_at = excluded.finished_at,
|
|
45
|
+
attempts = harvest_receipt.attempts + 1, error_code = excluded.error_code,
|
|
46
|
+
note = excluded.note`;
|
|
47
|
+
/** `complete` is the receipt's own word; the rest describe how it ended. */
|
|
48
|
+
function stateOf(coverage) {
|
|
49
|
+
if (coverage.complete)
|
|
50
|
+
return 'complete';
|
|
51
|
+
if (coverage.reason === 'SOURCE_REFUSED' || coverage.reason === 'TIMEOUT')
|
|
52
|
+
return 'failed';
|
|
53
|
+
if (coverage.reason === 'NOT_ATTEMPTED')
|
|
54
|
+
return 'running';
|
|
55
|
+
return 'partial';
|
|
56
|
+
}
|
|
57
|
+
function toRow(coverage) {
|
|
58
|
+
// Re-checked on the way to disk as well as on the way out of coverageOf: a
|
|
59
|
+
// receipt that reaches storage wrong is one that will be believed later.
|
|
60
|
+
assertCoverageConsistent(coverage);
|
|
61
|
+
return coverage;
|
|
62
|
+
}
|
|
63
|
+
export function saveReceipt(database, coverage, options = {}) {
|
|
64
|
+
const checked = toRow(coverage);
|
|
65
|
+
const finishedAt = Date.parse(checked.harvestedAt);
|
|
66
|
+
database
|
|
67
|
+
.prepare(UPSERT)
|
|
68
|
+
.run(checked.scope, checked.targetId, stateOf(checked), checked.reason, checked.have, checked.expected ?? null, checked.expectedSource ?? null, checked.source, checked.limitApplied ?? null, checked.sortApplied ?? null, checked.ranToExhaustion === undefined ? null : Number(checked.ranToExhaustion), checked.resumeToken ?? null, options.startedAt ?? finishedAt, finishedAt, 0, options.errorCode ?? null, checked.note ?? null);
|
|
69
|
+
database
|
|
70
|
+
.prepare('INSERT INTO harvest_event (scope, target_id, at, state, delta, note) VALUES (?,?,?,?,?,?)')
|
|
71
|
+
.run(checked.scope, checked.targetId, finishedAt, stateOf(checked), checked.have, checked.reason);
|
|
72
|
+
}
|
|
73
|
+
function fromRow(row) {
|
|
74
|
+
const expected = row.expected;
|
|
75
|
+
const expectedSource = row.expected_source;
|
|
76
|
+
return coverageSchema.parse({
|
|
77
|
+
scope: row.scope,
|
|
78
|
+
targetId: row.target_id,
|
|
79
|
+
complete: row.state === 'complete',
|
|
80
|
+
reason: row.reason,
|
|
81
|
+
have: row.have,
|
|
82
|
+
...(expected === null || expectedSource === null ? {} : { expected, expectedSource }),
|
|
83
|
+
...(row.ran_to_exhaustion === null ? {} : { ranToExhaustion: row.ran_to_exhaustion }),
|
|
84
|
+
source: row.source,
|
|
85
|
+
harvestedAt: new Date(row.finished_at ?? row.started_at).toISOString(),
|
|
86
|
+
...(row.limit_applied === null ? {} : { limitApplied: row.limit_applied }),
|
|
87
|
+
...(row.sort_applied === null ? {} : { sortApplied: row.sort_applied }),
|
|
88
|
+
...(row.resume_token === null ? {} : { resumeToken: row.resume_token }),
|
|
89
|
+
...(row.note === null ? {} : { note: row.note }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function readReceipt(database, scope, targetId) {
|
|
93
|
+
const row = queryRow(database.prepare('SELECT * FROM harvest_receipt WHERE scope = ? AND target_id = ?'), receiptRowSchema, scope, targetId);
|
|
94
|
+
return row === undefined ? undefined : fromRow(row);
|
|
95
|
+
}
|
|
96
|
+
export function listReceipts(database, options = {}) {
|
|
97
|
+
const limit = options.limit ?? 50;
|
|
98
|
+
const offset = options.offset ?? 0;
|
|
99
|
+
const rows = options.scope === undefined
|
|
100
|
+
? queryRows(database.prepare('SELECT * FROM harvest_receipt ORDER BY finished_at DESC, target_id LIMIT ? OFFSET ?'), receiptRowSchema, limit, offset)
|
|
101
|
+
: queryRows(database.prepare('SELECT * FROM harvest_receipt WHERE scope = ? ORDER BY finished_at DESC, target_id LIMIT ? OFFSET ?'), receiptRowSchema, options.scope, limit, offset);
|
|
102
|
+
return rows.map(fromRow);
|
|
103
|
+
}
|
|
104
|
+
export function countReceipts(database, scope) {
|
|
105
|
+
const row = scope === undefined
|
|
106
|
+
? queryRow(database.prepare('SELECT COUNT(*) AS n FROM harvest_receipt'), z.object({ n: z.number() }))
|
|
107
|
+
: queryRow(database.prepare('SELECT COUNT(*) AS n FROM harvest_receipt WHERE scope = ?'), z.object({ n: z.number() }), scope);
|
|
108
|
+
return row?.n ?? 0;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=harvest-receipts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harvest-receipts.js","sourceRoot":"","sources":["../../src/utils/harvest-receipts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The cursor `fetch_videos` hands back.
|
|
4
|
+
*
|
|
5
|
+
* YouTube's listings are continuation-paginated, so `--playlist-items 101:120`
|
|
6
|
+
* still walks from the start: paging server-side is O(n) per page and O(n^2)
|
|
7
|
+
* overall. It is also not a cursor in any useful sense — a new upload shifts
|
|
8
|
+
* every index by one, so an offset silently re-reads or skips at the seam.
|
|
9
|
+
*
|
|
10
|
+
* So the cursor carries the next index *and* the id of the last item already
|
|
11
|
+
* returned. If that id reappears on the next page the listing moved under us,
|
|
12
|
+
* and the duplicate is dropped rather than handed over twice.
|
|
13
|
+
*/
|
|
14
|
+
declare const cursorSchema: z.ZodObject<{
|
|
15
|
+
v: z.ZodLiteral<1>;
|
|
16
|
+
start: z.ZodNumber;
|
|
17
|
+
anchor: z.ZodString;
|
|
18
|
+
}, "strip", z.ZodTypeAny, {
|
|
19
|
+
anchor: string;
|
|
20
|
+
start: number;
|
|
21
|
+
v: 1;
|
|
22
|
+
}, {
|
|
23
|
+
anchor: string;
|
|
24
|
+
start: number;
|
|
25
|
+
v: 1;
|
|
26
|
+
}>;
|
|
27
|
+
export type ListingCursor = z.infer<typeof cursorSchema>;
|
|
28
|
+
export declare function encodeCursor(cursor: ListingCursor): string;
|
|
29
|
+
export declare function decodeCursor(token: string): ListingCursor;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=listing-cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listing-cursor.d.ts","sourceRoot":"","sources":["../../src/utils/listing-cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;GAWG;AAEH,QAAA,MAAM,YAAY;;;;;;;;;;;;EAIhB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEzD,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE1D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAezD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { YouTubeError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* The cursor `fetch_videos` hands back.
|
|
5
|
+
*
|
|
6
|
+
* YouTube's listings are continuation-paginated, so `--playlist-items 101:120`
|
|
7
|
+
* still walks from the start: paging server-side is O(n) per page and O(n^2)
|
|
8
|
+
* overall. It is also not a cursor in any useful sense — a new upload shifts
|
|
9
|
+
* every index by one, so an offset silently re-reads or skips at the seam.
|
|
10
|
+
*
|
|
11
|
+
* So the cursor carries the next index *and* the id of the last item already
|
|
12
|
+
* returned. If that id reappears on the next page the listing moved under us,
|
|
13
|
+
* and the duplicate is dropped rather than handed over twice.
|
|
14
|
+
*/
|
|
15
|
+
const cursorSchema = z.object({
|
|
16
|
+
v: z.literal(1),
|
|
17
|
+
start: z.number().int().min(1),
|
|
18
|
+
anchor: z.string(),
|
|
19
|
+
});
|
|
20
|
+
export function encodeCursor(cursor) {
|
|
21
|
+
return Buffer.from(JSON.stringify(cursor), 'utf8').toString('base64url');
|
|
22
|
+
}
|
|
23
|
+
export function decodeCursor(token) {
|
|
24
|
+
const parsed = (() => {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
const result = cursorSchema.safeParse(parsed);
|
|
33
|
+
if (result.success)
|
|
34
|
+
return result.data;
|
|
35
|
+
throw new YouTubeError('INVALID_INPUT', 'That cursor is not one this server issued.', {
|
|
36
|
+
nextStep: 'Omit `cursor` to start from the first page.',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=listing-cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listing-cursor.js","sourceRoot":"","sources":["../../src/utils/listing-cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;GAWG;AAEH,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5B,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACf,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CACnB,CAAC,CAAC;AAIH,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,MAAM,GAAG,CAAC,GAAY,EAAE;QAC5B,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9C,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IAEvC,MAAM,IAAI,YAAY,CAAC,eAAe,EAAE,4CAA4C,EAAE;QACpF,QAAQ,EAAE,6CAA6C;KACxD,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type TranscriptResult } from './transcript-cache.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turning a video's audio into a transcript when YouTube has no captions for it.
|
|
4
|
+
*
|
|
5
|
+
* Slow and CPU-heavy by nature, so it only ever runs when asked for, and what it
|
|
6
|
+
* produces is kept: a transcription is deterministic and takes minutes to redo.
|
|
7
|
+
*/
|
|
8
|
+
export interface LocalTranscriptionOptions {
|
|
9
|
+
language?: string;
|
|
10
|
+
/** Transcribe again even if a kept transcription exists. */
|
|
11
|
+
refresh?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function transcribeLocally(urlOrId: string, options: LocalTranscriptionOptions): Promise<TranscriptResult>;
|
|
14
|
+
//# sourceMappingURL=local-transcription.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-transcription.d.ts","sourceRoot":"","sources":["../../src/utils/local-transcription.ts"],"names":[],"mappings":"AAYA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAO/E;;;;;GAKG;AAEH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAcD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,gBAAgB,CAAC,CAmE3B"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { YouTubeError } from './errors.js';
|
|
5
|
+
import { ensurePrivateDir } from './paths.js';
|
|
6
|
+
import { requireFfmpeg } from './preflight.js';
|
|
7
|
+
import { TRANSCRIPT_DIR, asrPath, readTranscriptFile, writeTranscriptFile, } from './transcript-files.js';
|
|
8
|
+
import { resultFromEntry } from './transcript-cache.js';
|
|
9
|
+
import { TRANSCRIPT_CACHE_VERSION, segmentsToText } from './transcript.js';
|
|
10
|
+
import { assertLanguageTag } from './validate.js';
|
|
11
|
+
import { requireWhisper, runWhisper } from './whisper.js';
|
|
12
|
+
import { extractVideoId, watchUrl } from './youtube-url.js';
|
|
13
|
+
import { TIMEOUTS, runYtDlp } from './ytdlp.js';
|
|
14
|
+
/** What yt-dlp is told to leave behind: whisper reads 16 kHz mono, so make it that size. */
|
|
15
|
+
const AUDIO_ARGS = [
|
|
16
|
+
'-f',
|
|
17
|
+
'bestaudio',
|
|
18
|
+
'-x',
|
|
19
|
+
'--audio-format',
|
|
20
|
+
'wav',
|
|
21
|
+
'--postprocessor-args',
|
|
22
|
+
'ExtractAudio:-ar 16000 -ac 1',
|
|
23
|
+
'--no-playlist',
|
|
24
|
+
];
|
|
25
|
+
export async function transcribeLocally(urlOrId, options) {
|
|
26
|
+
const language = options.language ?? 'en';
|
|
27
|
+
const videoId = extractVideoId(urlOrId);
|
|
28
|
+
assertLanguageTag(language);
|
|
29
|
+
if (options.refresh !== true) {
|
|
30
|
+
const kept = await readTranscriptFile(asrPath(videoId, language), { expires: false });
|
|
31
|
+
if (kept)
|
|
32
|
+
return resultFromEntry(kept, videoId, 'local-asr');
|
|
33
|
+
}
|
|
34
|
+
// Cheap local checks first: a missing tool should fail now, not after the audio
|
|
35
|
+
// has been downloaded.
|
|
36
|
+
await requireFfmpeg('Local transcription');
|
|
37
|
+
const modelPath = await requireWhisper('Local transcription');
|
|
38
|
+
const tempDir = join(TRANSCRIPT_DIR, 'temp');
|
|
39
|
+
await ensurePrivateDir(tempDir);
|
|
40
|
+
// A folder of its own for this call: yt-dlp leaves the original download and `.part`
|
|
41
|
+
// files beside the wav it makes, and two calls for one video must not share, then
|
|
42
|
+
// delete, each other's audio. The whole folder goes when this call does.
|
|
43
|
+
const workDir = await mkdtemp(join(tempDir, `${videoId}-`));
|
|
44
|
+
const audioBase = join(workDir, 'audio');
|
|
45
|
+
const audioPath = `${audioBase}.wav`;
|
|
46
|
+
try {
|
|
47
|
+
await runYtDlp([...AUDIO_ARGS, '-o', `${audioBase}.%(ext)s`], {
|
|
48
|
+
label: 'transcribe_video',
|
|
49
|
+
timeoutMs: TIMEOUTS.download,
|
|
50
|
+
retry: false,
|
|
51
|
+
target: watchUrl(videoId),
|
|
52
|
+
});
|
|
53
|
+
if (!existsSync(audioPath)) {
|
|
54
|
+
throw new YouTubeError('YTDLP_FAILED', 'yt-dlp did not produce the audio to transcribe.', {
|
|
55
|
+
nextStep: 'Check the video is playable, or call check_health.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const segments = await runWhisper(audioPath, language, modelPath, join(workDir, 'whisper'));
|
|
59
|
+
if (segments.length === 0) {
|
|
60
|
+
throw new YouTubeError('NOT_FOUND', 'No speech was found in this video’s audio.', {
|
|
61
|
+
nextStep: 'The audio may be music or silence, or the model may not know this language. Try a larger model or another language.',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const entry = {
|
|
65
|
+
version: TRANSCRIPT_CACHE_VERSION,
|
|
66
|
+
videoId,
|
|
67
|
+
language,
|
|
68
|
+
fetchedAt: new Date().toISOString(),
|
|
69
|
+
segments,
|
|
70
|
+
};
|
|
71
|
+
await writeTranscriptFile(asrPath(videoId, language), entry);
|
|
72
|
+
return {
|
|
73
|
+
transcript: segmentsToText(segments),
|
|
74
|
+
segments,
|
|
75
|
+
language,
|
|
76
|
+
videoId,
|
|
77
|
+
cached: false,
|
|
78
|
+
source: 'local-asr',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await rm(workDir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=local-transcription.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-transcription.js","sourceRoot":"","sources":["../../src/utils/local-transcription.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A point in a video, in every form a caller needs it.
|
|
4
|
+
*
|
|
5
|
+
* Frames, contact-sheet cells and scene cuts all answer "when", and each used to
|
|
6
|
+
* need the same three spellings of it: seconds to compute with, a clock reading to
|
|
7
|
+
* show a person, and a link that opens the video there.
|
|
8
|
+
*/
|
|
9
|
+
export declare const videoMomentSchema: z.ZodObject<{
|
|
10
|
+
timestampSeconds: z.ZodNumber;
|
|
11
|
+
timestamp: z.ZodString;
|
|
12
|
+
url: z.ZodString;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
url: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
timestampSeconds: number;
|
|
17
|
+
}, {
|
|
18
|
+
url: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
timestampSeconds: number;
|
|
21
|
+
}>;
|
|
22
|
+
export type VideoMoment = z.infer<typeof videoMomentSchema>;
|
|
23
|
+
export declare function videoMoment(videoId: string, seconds: number): VideoMoment;
|
|
24
|
+
//# sourceMappingURL=moment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moment.d.ts","sourceRoot":"","sources":["../../src/utils/moment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;GAMG;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAMzE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { deepLink, formatTimestamp } from './transcript.js';
|
|
3
|
+
/**
|
|
4
|
+
* A point in a video, in every form a caller needs it.
|
|
5
|
+
*
|
|
6
|
+
* Frames, contact-sheet cells and scene cuts all answer "when", and each used to
|
|
7
|
+
* need the same three spellings of it: seconds to compute with, a clock reading to
|
|
8
|
+
* show a person, and a link that opens the video there.
|
|
9
|
+
*/
|
|
10
|
+
export const videoMomentSchema = z.object({
|
|
11
|
+
timestampSeconds: z.number(),
|
|
12
|
+
timestamp: z.string().describe('Clock reading, H:MM:SS or M:SS'),
|
|
13
|
+
url: z.string().describe('Link that opens the video at this moment'),
|
|
14
|
+
});
|
|
15
|
+
export function videoMoment(videoId, seconds) {
|
|
16
|
+
return {
|
|
17
|
+
timestampSeconds: seconds,
|
|
18
|
+
timestamp: formatTimestamp(seconds),
|
|
19
|
+
url: deepLink(videoId, seconds),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=moment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moment.js","sourceRoot":"","sources":["../../src/utils/moment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE5D;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE;IAC5B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;IAChE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,0CAA0C,CAAC;CACrE,CAAC,CAAC;AAIH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAe;IAC1D,OAAO;QACL,gBAAgB,EAAE,OAAO;QACzB,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC;QACnC,GAAG,EAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;KAChC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The moments of a video people replay most.
|
|
3
|
+
*
|
|
4
|
+
* YouTube's heatmap is a hundred or so equal slices, each scored from 0 to 1. A
|
|
5
|
+
* spike spans several adjacent slices, so taking the highest-scoring slices would
|
|
6
|
+
* return one spike over and over; instead each moment reserves the slices around it.
|
|
7
|
+
*/
|
|
8
|
+
export interface HeatSegment {
|
|
9
|
+
startSeconds: number;
|
|
10
|
+
endSeconds: number;
|
|
11
|
+
value: number;
|
|
12
|
+
}
|
|
13
|
+
/** Up to `count` distinct moments, strongest first. */
|
|
14
|
+
export declare function mostReplayed(heatmap: HeatSegment[], count: number): HeatSegment[];
|
|
15
|
+
//# sourceMappingURL=most-replayed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"most-replayed.d.ts","sourceRoot":"","sources":["../../src/utils/most-replayed.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAUD,uDAAuD;AACvD,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CASjF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The moments of a video people replay most.
|
|
3
|
+
*
|
|
4
|
+
* YouTube's heatmap is a hundred or so equal slices, each scored from 0 to 1. A
|
|
5
|
+
* spike spans several adjacent slices, so taking the highest-scoring slices would
|
|
6
|
+
* return one spike over and over; instead each moment reserves the slices around it.
|
|
7
|
+
*/
|
|
8
|
+
/** A moment keeps this many slice-widths clear on either side of it. */
|
|
9
|
+
const SPACING_IN_SEGMENTS = 3;
|
|
10
|
+
function isClearOf(segment, chosen) {
|
|
11
|
+
const reach = SPACING_IN_SEGMENTS * (segment.endSeconds - segment.startSeconds);
|
|
12
|
+
return chosen.every((peak) => Math.abs(segment.startSeconds - peak.startSeconds) >= reach);
|
|
13
|
+
}
|
|
14
|
+
/** Up to `count` distinct moments, strongest first. */
|
|
15
|
+
export function mostReplayed(heatmap, count) {
|
|
16
|
+
const chosen = [];
|
|
17
|
+
for (const segment of [...heatmap].sort((a, b) => b.value - a.value)) {
|
|
18
|
+
if (chosen.length >= count)
|
|
19
|
+
break;
|
|
20
|
+
if (isClearOf(segment, chosen))
|
|
21
|
+
chosen.push(segment);
|
|
22
|
+
}
|
|
23
|
+
return chosen;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=most-replayed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"most-replayed.js","sourceRoot":"","sources":["../../src/utils/most-replayed.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,wEAAwE;AACxE,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,SAAS,SAAS,CAAC,OAAoB,EAAE,MAAqB;IAC5D,MAAM,KAAK,GAAG,mBAAmB,GAAG,CAAC,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,CAAC;AAC7F,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,YAAY,CAAC,OAAsB,EAAE,KAAa;IAChE,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QACrE,IAAI,MAAM,CAAC,MAAM,IAAI,KAAK;YAAE,MAAM;QAClC,IAAI,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session preflight: what yt-dlp can prove about itself before we ask YouTube
|
|
3
|
+
* anything.
|
|
4
|
+
*
|
|
5
|
+
* Measured on 2026-09-06: with no PO token provider installed, every *player*
|
|
6
|
+
* request from this host failed with "Sign in to confirm you're not a bot",
|
|
7
|
+
* while flat listings were never affected. Rotating `player_client` across six
|
|
8
|
+
* clients and waiting out a 15-minute cooldown both failed. The cause was not
|
|
9
|
+
* rate limiting and not the address alone — `yt-dlp -v` reported
|
|
10
|
+
* `PO Token Providers: none`, so every player request went out without the
|
|
11
|
+
* integrity token a browser attaches.
|
|
12
|
+
*
|
|
13
|
+
* That diagnosis is only useful if the server can see it, so this module reads
|
|
14
|
+
* it out of yt-dlp's own debug output rather than guessing. It is deliberately
|
|
15
|
+
* provider-agnostic: it reports what yt-dlp says it has, and never looks for a
|
|
16
|
+
* particular plugin by name.
|
|
17
|
+
*/
|
|
18
|
+
export interface ProviderStatus {
|
|
19
|
+
name: string;
|
|
20
|
+
available: boolean;
|
|
21
|
+
/** Flags yt-dlp printed in parentheses, e.g. "external". */
|
|
22
|
+
flags: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface SessionReport {
|
|
25
|
+
/** False when yt-dlp reports no usable PO token provider. */
|
|
26
|
+
potAvailable: boolean;
|
|
27
|
+
potProviders: ProviderStatus[];
|
|
28
|
+
jsRuntimes: ProviderStatus[];
|
|
29
|
+
impersonateTargets: string[];
|
|
30
|
+
/** Set when the probe itself could not run; the report is then unknown, not empty. */
|
|
31
|
+
probeFailed?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Split on commas that separate entries, not the ones inside a parenthesised
|
|
35
|
+
* flag list. yt-dlp prints `bgutil:http-1.3.2 (external, unavailable)` — a
|
|
36
|
+
* plain `split(',')` tears that entry in half and loses the flag that matters.
|
|
37
|
+
*/
|
|
38
|
+
export declare function splitTopLevel(value: string): string[];
|
|
39
|
+
/** Parses one `Providers: ...` payload. `none` is an empty list, not a provider. */
|
|
40
|
+
export declare function parseProviderList(payload: string): ProviderStatus[];
|
|
41
|
+
/**
|
|
42
|
+
* Reads `yt-dlp --list-impersonate-targets`, keeping only rows that are
|
|
43
|
+
* actually usable. Every target is unavailable without curl_cffi, and passing
|
|
44
|
+
* `--impersonate` to a build that lacks it fails every spawn — so an
|
|
45
|
+
* unavailable target must be visible here rather than discovered per request.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseImpersonateTargets(stdout: string): string[];
|
|
48
|
+
export declare function runSessionPreflight(options?: {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
}): Promise<SessionReport>;
|
|
51
|
+
/** Test seam — the report is cached for the process lifetime in normal use. */
|
|
52
|
+
export declare function resetSessionPreflightCache(): void;
|
|
53
|
+
//# sourceMappingURL=pot-preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pot-preflight.d.ts","sourceRoot":"","sources":["../../src/utils/pot-preflight.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAyBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBrD;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAenE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBhE;AAqBD,wBAAsB,mBAAmB,CACvC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,OAAO,CAAC,aAAa,CAAC,CAwCxB;AAED,+EAA+E;AAC/E,wBAAgB,0BAA0B,IAAI,IAAI,CAGjD"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
const PROBE_TIMEOUT_MS = 30_000;
|
|
3
|
+
const FAILED_REPORT_TTL_MS = 60_000;
|
|
4
|
+
/**
|
|
5
|
+
* The `[youtube] [pot]` and `[youtube] [jsc]` debug lines are emitted only when
|
|
6
|
+
* the *video* extractor initialises. Measured 2026-09-06: `--version`,
|
|
7
|
+
* `ytsearch0:` and a `--flat-playlist` channel walk all produce neither line —
|
|
8
|
+
* the listing plane never touches either subsystem.
|
|
9
|
+
*
|
|
10
|
+
* So the probe uses a syntactically valid but nonexistent video id. yt-dlp
|
|
11
|
+
* initialises the extractor, prints both lines, then fails the lookup. That
|
|
12
|
+
* failure is expected and ignored: it costs one request, depends on no real
|
|
13
|
+
* video continuing to exist, and needs no fixture to be kept up to date.
|
|
14
|
+
*/
|
|
15
|
+
const PROBE_VIDEO_URL = 'https://www.youtube.com/watch?v=00000000000';
|
|
16
|
+
const PROBE_ARGS = ['-v', '--simulate', '--skip-download', PROBE_VIDEO_URL];
|
|
17
|
+
const POT_LINE = /PO Token Providers:\s*(.+)$/;
|
|
18
|
+
const JSC_LINE = /JS Challenge Providers:\s*(.+)$/;
|
|
19
|
+
let cached;
|
|
20
|
+
let cachedAt = 0;
|
|
21
|
+
/**
|
|
22
|
+
* Split on commas that separate entries, not the ones inside a parenthesised
|
|
23
|
+
* flag list. yt-dlp prints `bgutil:http-1.3.2 (external, unavailable)` — a
|
|
24
|
+
* plain `split(',')` tears that entry in half and loses the flag that matters.
|
|
25
|
+
*/
|
|
26
|
+
export function splitTopLevel(value) {
|
|
27
|
+
const parts = [];
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let start = 0;
|
|
30
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
31
|
+
const char = value[i];
|
|
32
|
+
if (char === '(')
|
|
33
|
+
depth += 1;
|
|
34
|
+
else if (char === ')')
|
|
35
|
+
depth = Math.max(0, depth - 1);
|
|
36
|
+
else if (char === ',' && depth === 0) {
|
|
37
|
+
parts.push(value.slice(start, i));
|
|
38
|
+
start = i + 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
parts.push(value.slice(start));
|
|
42
|
+
return parts.map((part) => part.trim()).filter((part) => part.length > 0);
|
|
43
|
+
}
|
|
44
|
+
/** Parses one `Providers: ...` payload. `none` is an empty list, not a provider. */
|
|
45
|
+
export function parseProviderList(payload) {
|
|
46
|
+
const trimmed = payload.trim();
|
|
47
|
+
if (trimmed === '' || trimmed.toLowerCase() === 'none')
|
|
48
|
+
return [];
|
|
49
|
+
return splitTopLevel(trimmed).map((entry) => {
|
|
50
|
+
const match = /^(.*?)\s*\((.*)\)$/.exec(entry);
|
|
51
|
+
if (match === null)
|
|
52
|
+
return { name: entry, available: true, flags: [] };
|
|
53
|
+
const flags = splitTopLevel(match[2] ?? '').map((flag) => flag.toLowerCase());
|
|
54
|
+
return {
|
|
55
|
+
name: (match[1] ?? '').trim(),
|
|
56
|
+
available: !flags.includes('unavailable'),
|
|
57
|
+
flags,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reads `yt-dlp --list-impersonate-targets`, keeping only rows that are
|
|
63
|
+
* actually usable. Every target is unavailable without curl_cffi, and passing
|
|
64
|
+
* `--impersonate` to a build that lacks it fails every spawn — so an
|
|
65
|
+
* unavailable target must be visible here rather than discovered per request.
|
|
66
|
+
*/
|
|
67
|
+
export function parseImpersonateTargets(stdout) {
|
|
68
|
+
const targets = [];
|
|
69
|
+
for (const raw of stdout.split('\n')) {
|
|
70
|
+
const line = raw.trim();
|
|
71
|
+
if (line === '' || line.startsWith('[') || line.startsWith('-'))
|
|
72
|
+
continue;
|
|
73
|
+
if (line.toLowerCase().startsWith('client'))
|
|
74
|
+
continue;
|
|
75
|
+
if (line.toLowerCase().includes('(unavailable)'))
|
|
76
|
+
continue;
|
|
77
|
+
const columns = line.split(/\s{2,}/).filter((column) => column.length > 0);
|
|
78
|
+
const client = columns[0]?.trim();
|
|
79
|
+
if (client === undefined || client === '')
|
|
80
|
+
continue;
|
|
81
|
+
const os = columns[1]?.trim();
|
|
82
|
+
targets.push(os === undefined || os === '' || os === '-' ? client : `${client}:${os}`);
|
|
83
|
+
}
|
|
84
|
+
return targets;
|
|
85
|
+
}
|
|
86
|
+
async function probe(args) {
|
|
87
|
+
try {
|
|
88
|
+
// yt-dlp writes debug lines to stderr and exits non-zero on some probes;
|
|
89
|
+
// `reject: false` keeps both streams instead of throwing them away.
|
|
90
|
+
const result = await execa('yt-dlp', args, { timeout: PROBE_TIMEOUT_MS, reject: false });
|
|
91
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function readProviders(output, pattern) {
|
|
98
|
+
for (const line of output.split('\n')) {
|
|
99
|
+
const match = pattern.exec(line);
|
|
100
|
+
if (match !== null)
|
|
101
|
+
return parseProviderList(match[1] ?? '');
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
export async function runSessionPreflight(options = {}) {
|
|
106
|
+
// Mirrors runPreflight: a good answer holds for the process, a bad one gets a
|
|
107
|
+
// short life so one slow spawn does not pin the diagnosis for good.
|
|
108
|
+
if (cached !== undefined && !options.force) {
|
|
109
|
+
if (cached.probeFailed === undefined || Date.now() - cachedAt < FAILED_REPORT_TTL_MS) {
|
|
110
|
+
return cached;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const [session, impersonate] = await Promise.all([
|
|
114
|
+
probe(PROBE_ARGS),
|
|
115
|
+
probe(['--list-impersonate-targets']),
|
|
116
|
+
]);
|
|
117
|
+
if (session === undefined) {
|
|
118
|
+
cached = {
|
|
119
|
+
potAvailable: false,
|
|
120
|
+
potProviders: [],
|
|
121
|
+
jsRuntimes: [],
|
|
122
|
+
impersonateTargets: [],
|
|
123
|
+
probeFailed: 'yt-dlp could not be probed for session capabilities.',
|
|
124
|
+
};
|
|
125
|
+
cachedAt = Date.now();
|
|
126
|
+
return cached;
|
|
127
|
+
}
|
|
128
|
+
const output = `${session.stderr}\n${session.stdout}`;
|
|
129
|
+
const potProviders = readProviders(output, POT_LINE);
|
|
130
|
+
cached = {
|
|
131
|
+
potAvailable: potProviders.some((provider) => provider.available),
|
|
132
|
+
potProviders,
|
|
133
|
+
jsRuntimes: readProviders(output, JSC_LINE),
|
|
134
|
+
impersonateTargets: impersonate === undefined
|
|
135
|
+
? []
|
|
136
|
+
: parseImpersonateTargets(`${impersonate.stdout}\n${impersonate.stderr}`),
|
|
137
|
+
};
|
|
138
|
+
cachedAt = Date.now();
|
|
139
|
+
return cached;
|
|
140
|
+
}
|
|
141
|
+
/** Test seam — the report is cached for the process lifetime in normal use. */
|
|
142
|
+
export function resetSessionPreflightCache() {
|
|
143
|
+
cached = undefined;
|
|
144
|
+
cachedAt = 0;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=pot-preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pot-preflight.js","sourceRoot":"","sources":["../../src/utils/pot-preflight.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAqC9B,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAChC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,MAAM,eAAe,GAAG,6CAA6C,CAAC;AACtE,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,CAAC,CAAC;AAE5E,MAAM,QAAQ,GAAG,6BAA6B,CAAC;AAC/C,MAAM,QAAQ,GAAG,iCAAiC,CAAC;AAEnD,IAAI,MAAiC,CAAC;AACtC,IAAI,QAAQ,GAAG,CAAC,CAAC;AAEjB;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,IAAI,KAAK,GAAG;YAAE,KAAK,IAAI,CAAC,CAAC;aACxB,IAAI,IAAI,KAAK,GAAG;YAAE,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;aACjD,IAAI,IAAI,KAAK,GAAG,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YAClC,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/B,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,CAAC,WAAW,EAAE,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC;IAElE,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1C,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAEvE,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAC9E,OAAO;YACL,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YAC7B,SAAS,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC;YACzC,KAAK;SACN,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC1E,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,SAAS;QACtD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC;YAAE,SAAS;QAE3D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3E,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE;YAAE,SAAS;QAEpD,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,KAAK,CAAC,IAAc;IACjC,IAAI,CAAC;QACH,yEAAyE;QACzE,oEAAoE;QACpE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACzF,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,MAAc,EAAE,OAAe;IACpD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,UAA+B,EAAE;IAEjC,8EAA8E;IAC9E,oEAAoE;IACpE,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC3C,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,GAAG,oBAAoB,EAAE,CAAC;YACrF,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAED,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC/C,KAAK,CAAC,UAAU,CAAC;QACjB,KAAK,CAAC,CAAC,4BAA4B,CAAC,CAAC;KACtC,CAAC,CAAC;IAEH,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,GAAG;YACP,YAAY,EAAE,KAAK;YACnB,YAAY,EAAE,EAAE;YAChB,UAAU,EAAE,EAAE;YACd,kBAAkB,EAAE,EAAE;YACtB,WAAW,EAAE,sDAAsD;SACpE,CAAC;QACF,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACtB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;IACtD,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAErD,MAAM,GAAG;QACP,YAAY,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC;QACjE,YAAY;QACZ,UAAU,EAAE,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC;QAC3C,kBAAkB,EAChB,WAAW,KAAK,SAAS;YACvB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,uBAAuB,CAAC,GAAG,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,EAAE,CAAC;KAC9E,CAAC;IACF,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACtB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,0BAA0B;IACxC,MAAM,GAAG,SAAS,CAAC;IACnB,QAAQ,GAAG,CAAC,CAAC;AACf,CAAC"}
|