diff --git a/.github/workflows/render-video.yml b/.github/workflows/render-video.yml new file mode 100644 index 0000000..d85cc73 --- /dev/null +++ b/.github/workflows/render-video.yml @@ -0,0 +1,90 @@ +name: Render Promo Video + +on: + push: + branches: [main, feat/video, feat/image-handling] + paths: + - "video/**" + pull_request: + branches: [main] + paths: + - "video/**" + workflow_dispatch: + inputs: + codec: + description: "Output codec" + required: false + default: "h264" + type: choice + options: + - h264 + - h265 + - vp8 + - vp9 + crf: + description: "Constant Rate Factor (quality, lower = better)" + required: false + default: "18" + +jobs: + render: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: video/package-lock.json + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + chromium-browser \ + libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 \ + libxcomposite1 libxdamage1 libxrandr2 libgbm1 \ + libpango-1.0-0 libcairo2 libxshmfence1 \ + fonts-noto fonts-liberation ffmpeg + sudo apt-get install -y libasound2t64 || sudo apt-get install -y libasound2 + echo "BROWSER_PATH=$(which chromium-browser || which chromium || which google-chrome-stable || which google-chrome)" >> $GITHUB_ENV + + - name: Install dependencies + working-directory: video + run: npm ci + + - name: Cache Remotion bundle + uses: actions/cache@v4 + with: + path: video/node_modules/.cache/remotion + key: remotion-bundle-${{ hashFiles('video/src/**') }} + restore-keys: remotion-bundle- + + - name: Render video + working-directory: video + run: | + npx remotion render src/index.ts TreeDexVideo out/treedex.mp4 \ + --browser-executable="$BROWSER_PATH" \ + --codec=${{ github.event.inputs.codec || 'h264' }} \ + --crf=${{ github.event.inputs.crf || '18' }} \ + --concurrency=4 \ + --image-format=jpeg \ + --quality=85 \ + --bundle-cache=true \ + --log=verbose + + - name: Upload rendered video + uses: actions/upload-artifact@v4 + with: + name: treedex-promo-video + path: video/out/treedex.mp4 + retention-days: 30 + + - name: Print video info + working-directory: video + run: | + ls -lh out/treedex.mp4 + ffprobe -v quiet -print_format json -show_format -show_streams out/treedex.mp4 | head -50 diff --git a/src/core.ts b/src/core.ts index e669188..3f0a3c6 100644 --- a/src/core.ts +++ b/src/core.ts @@ -23,10 +23,53 @@ import { structureContinuePrompt, retrievalPrompt, answerPrompt, + imageDescriptionPrompt, } from "./prompts.js"; +import { countTokens } from "./pdf-parser.js"; import type { Page, TreeNode, IndexData, Stats } from "./types.js"; import type { BaseLLM } from "./llm-backends.js"; +/** Append image descriptions to page text, modifying pages in place. */ +async function describeImages( + pages: Page[], + llm?: BaseLLM | null, + verbose: boolean = false, +): Promise { + for (const page of pages) { + if (!page.images || page.images.length === 0) continue; + + const descriptions: string[] = []; + for (const img of page.images) { + const alt = (img.alt_text ?? "").trim(); + if (alt) { + descriptions.push(`[Image: ${alt}]`); + } else if (llm?.supportsVision && img.data) { + try { + const desc = await llm.generateWithImage( + imageDescriptionPrompt(), + img.data, + img.mime_type, + ); + descriptions.push(`[Image: ${desc.trim()}]`); + } catch { + descriptions.push("[Image present]"); + } + } else { + descriptions.push("[Image present]"); + } + } + + if (descriptions.length > 0) { + page.text = page.text + "\n" + descriptions.join("\n"); + page.token_count = countTokens(page.text); + } + + if (verbose && descriptions.length > 0) { + console.log(` Page ${page.page_num}: ${descriptions.length} image(s) described`); + } + } +} + /** Result of a TreeDex query. */ export class QueryResult { readonly context: string; @@ -100,6 +143,7 @@ export class TreeDex { maxTokens?: number; overlap?: number; verbose?: boolean; + extractImages?: boolean; }, ): Promise { const { @@ -107,6 +151,7 @@ export class TreeDex { maxTokens = 20000, overlap = 1, verbose = true, + extractImages = false, } = options ?? {}; if (verbose) { @@ -118,7 +163,7 @@ export class TreeDex { if (loader) { pages = await loader.load(path); } else { - pages = await autoLoader(path); + pages = await autoLoader(path, { extractImages }); } if (verbose) { @@ -141,6 +186,9 @@ export class TreeDex { ): Promise { const { maxTokens = 20000, overlap = 1, verbose = true } = options ?? {}; + // Describe images before grouping — appends text markers to pages + await describeImages(pages, llm, verbose); + const groups = groupPages(pages, maxTokens, overlap); if (verbose) { @@ -288,11 +336,14 @@ export class TreeDex { const fs = await import("node:fs/promises"); const stripped = stripTextFromTree(this.tree); + // Strip images from pages — descriptions are already in text + const cleanPages: Page[] = this.pages.map(({ images: _images, ...rest }) => rest); + const data: IndexData = { version: "1.0", framework: "TreeDex", tree: stripped, - pages: this.pages, + pages: cleanPages, }; await fs.writeFile(path, JSON.stringify(data, null, 2), "utf-8"); diff --git a/src/index.ts b/src/index.ts index b37b0e0..e7824b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,5 +56,6 @@ export { structureContinuePrompt, retrievalPrompt, answerPrompt, + imageDescriptionPrompt, } from "./prompts.js"; -export type { Page, TreeNode, IndexData, Stats } from "./types.js"; +export type { Page, PageImage, TreeNode, IndexData, Stats } from "./types.js"; diff --git a/src/llm-backends.ts b/src/llm-backends.ts index 4253221..c496c67 100644 --- a/src/llm-backends.ts +++ b/src/llm-backends.ts @@ -33,6 +33,22 @@ export abstract class BaseLLM { abstract generate(prompt: string): Promise; + /** Whether this backend supports image inputs. */ + get supportsVision(): boolean { + return false; + } + + /** Send a prompt with an image and return the generated text. */ + async generateWithImage( + _prompt: string, + _imageBase64: string, + _mimeType: string, + ): Promise { + throw new Error( + `${this.constructor.name} does not support vision/image inputs.`, + ); + } + toString(): string { return `${this.constructor.name}()`; } @@ -65,11 +81,28 @@ export class GeminiLLM extends BaseLLM { } async generate(prompt: string): Promise { - const model = await this.getClient() as { generateContent(p: string): Promise<{ response: { text(): string } }> }; + const model = await this.getClient() as { generateContent(p: unknown): Promise<{ response: { text(): string } }> }; const response = await model.generateContent(prompt); return response.response.text(); } + get supportsVision(): boolean { + return true; + } + + async generateWithImage( + prompt: string, + imageBase64: string, + mimeType: string, + ): Promise { + const model = await this.getClient() as { generateContent(p: unknown): Promise<{ response: { text(): string } }> }; + const imagePart = { + inlineData: { mimeType, data: imageBase64 }, + }; + const response = await model.generateContent([prompt, imagePart]); + return response.response.text(); + } + toString(): string { return `GeminiLLM(model=${JSON.stringify(this.modelName)})`; } @@ -113,6 +146,40 @@ export class OpenAILLM extends BaseLLM { return response.choices[0].message.content; } + get supportsVision(): boolean { + return true; + } + + async generateWithImage( + prompt: string, + imageBase64: string, + mimeType: string, + ): Promise { + const client = await this.getClient() as { + chat: { + completions: { + create(opts: unknown): Promise<{ + choices: Array<{ message: { content: string } }>; + }>; + }; + }; + }; + const response = await client.chat.completions.create({ + model: this.modelName, + messages: [{ + role: "user", + content: [ + { type: "text", text: prompt }, + { + type: "image_url", + image_url: { url: `data:${mimeType};base64,${imageBase64}` }, + }, + ], + }], + }); + return response.choices[0].message.content; + } + toString(): string { return `OpenAILLM(model=${JSON.stringify(this.modelName)})`; } @@ -155,6 +222,43 @@ export class ClaudeLLM extends BaseLLM { return response.content[0].text; } + get supportsVision(): boolean { + return true; + } + + async generateWithImage( + prompt: string, + imageBase64: string, + mimeType: string, + ): Promise { + const client = await this.getClient() as { + messages: { + create(opts: unknown): Promise<{ + content: Array<{ text: string }>; + }>; + }; + }; + const response = await client.messages.create({ + model: this.modelName, + max_tokens: 4096, + messages: [{ + role: "user", + content: [ + { + type: "image", + source: { + type: "base64", + media_type: mimeType, + data: imageBase64, + }, + }, + { type: "text", text: prompt }, + ], + }], + }); + return response.content[0].text; + } + toString(): string { return `ClaudeLLM(model=${JSON.stringify(this.modelName)})`; } diff --git a/src/loaders.ts b/src/loaders.ts index e54fa91..82ab289 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -27,9 +27,15 @@ export function textToPages( /** Load PDF files using pdfjs-dist. */ export class PDFLoader { + readonly extractImages: boolean; + + constructor(options?: { extractImages?: boolean }) { + this.extractImages = options?.extractImages ?? false; + } + async load(path: string): Promise { const { extractPages } = await import("./pdf-parser.js"); - return extractPages(path); + return extractPages(path, { extractImages: this.extractImages }); } } @@ -73,8 +79,16 @@ export class HTMLLoader { let skip = false; const parser = new Parser({ - onopentag(name: string) { + onopentag(name: string, attribs: Record) { if (name === "script" || name === "style") skip = true; + if (name === "img") { + const alt = (attribs.alt || "").trim(); + if (alt) { + parts.push(`\n[Image: ${alt}]\n`); + } else { + parts.push("\n[Image]\n"); + } + } }, onclosetag(name: string) { if (name === "script" || name === "style") skip = false; @@ -98,9 +112,16 @@ export class HTMLLoader { }); } catch { // Fallback: simple regex-based tag stripping - return html + // Extract img alt text before stripping all tags + let processed = html .replace(/]*>[\s\S]*?<\/script>/gi, "") - .replace(/]*>[\s\S]*?<\/style>/gi, "") + .replace(/]*>[\s\S]*?<\/style>/gi, ""); + processed = processed.replace(/]*>/gi, (tag) => { + const altMatch = tag.match(/alt=["']([^"']*)["']/i); + const alt = altMatch ? altMatch[1].trim() : ""; + return alt ? ` [Image: ${alt}] ` : " [Image] "; + }); + return processed .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .trim(); @@ -121,8 +142,16 @@ export class DOCXLoader { // @ts-expect-error -- optional peer dependency const mammoth = await import("mammoth"); const buffer = await fs.readFile(path); - const result = await mammoth.extractRawText({ buffer }); - return textToPages(result.value, this.charsPerPage); + const result = await mammoth.convertToHtml({ buffer }); + // Replace tags with [Image: alt] markers, then strip remaining HTML + let html: string = result.value; + html = html.replace(/]*>/gi, (tag: string) => { + const altMatch = tag.match(/alt=["']([^"']*)["']/i); + const alt = altMatch ? altMatch[1].trim() : ""; + return alt ? `[Image: ${alt}]` : "[Image]"; + }); + const text = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); + return textToPages(text, this.charsPerPage); } } @@ -140,7 +169,10 @@ const EXTENSION_MAP: Record = { }; /** Auto-detect file format and load pages. */ -export async function autoLoader(filePath: string): Promise { +export async function autoLoader( + filePath: string, + options?: { extractImages?: boolean }, +): Promise { const { extname } = await import("node:path"); const ext = extname(filePath).toLowerCase(); const LoaderClass = EXTENSION_MAP[ext]; @@ -150,6 +182,9 @@ export async function autoLoader(filePath: string): Promise { `Unsupported file extension '${ext}'. Supported: ${supported}`, ); } + if (ext === ".pdf" && options?.extractImages) { + return new PDFLoader({ extractImages: true }).load(filePath); + } const loader = new LoaderClass(); return loader.load(filePath); } diff --git a/src/pdf-parser.ts b/src/pdf-parser.ts index 2fe8969..7aa3210 100644 --- a/src/pdf-parser.ts +++ b/src/pdf-parser.ts @@ -11,9 +11,12 @@ export function countTokens(text: string): number { /** * Extract text from each page of a PDF. * - * Returns a list of objects with page_num, text, and token_count. + * Returns a list of objects with page_num, text, token_count, and optionally images. */ -export async function extractPages(pdfPath: string): Promise { +export async function extractPages( + pdfPath: string, + options?: { extractImages?: boolean }, +): Promise { const fs = await import("node:fs/promises"); const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs"); @@ -21,6 +24,10 @@ export async function extractPages(pdfPath: string): Promise { const data = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); const doc = await pdfjs.getDocument({ data }).promise; + // pdfjs OPS constants for image painting + const OPS_PAINT_IMAGE = 85; // paintImageXObject + const OPS_PAINT_JPEG = 82; // paintJpegXObject + const pages: Page[] = []; for (let i = 0; i < doc.numPages; i++) { const page = await doc.getPage(i + 1); // pdfjs is 1-indexed @@ -31,11 +38,45 @@ export async function extractPages(pdfPath: string): Promise { return typeof obj.str === "string" ? obj.str : ""; }) .join(" "); - pages.push({ + + const pageObj: Page = { page_num: i, text, token_count: countTokens(text), - }); + }; + + if (options?.extractImages) { + try { + const opList = await page.getOperatorList(); + const images: Page["images"] = []; + let imgIndex = 0; + + for (let j = 0; j < opList.fnArray.length; j++) { + const op = opList.fnArray[j]; + if (op === OPS_PAINT_IMAGE || op === OPS_PAINT_JPEG) { + const imgName = opList.argsArray[j]?.[0]; + if (typeof imgName === "string") { + // Record image presence — pdfjs gives raw pixel data, not encoded formats + images.push({ + data: "", + mime_type: op === OPS_PAINT_JPEG ? "image/jpeg" : "image/unknown", + alt_text: `[Embedded image ${imgIndex + 1} on page ${i + 1}]`, + index_on_page: imgIndex, + }); + imgIndex++; + } + } + } + + if (images.length > 0) { + pageObj.images = images; + } + } catch { + // Ignore image extraction errors + } + } + + pages.push(pageObj); } return pages; diff --git a/src/prompts.ts b/src/prompts.ts index dab776e..c2d24c1 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -91,3 +91,15 @@ Return ONLY valid JSON. JSON output: `; } + +export function imageDescriptionPrompt(): string { + return `Describe this image concisely in 1-2 sentences. Focus on: +- What the image shows (diagram, chart, photo, table, etc.) +- Key information visible (labels, data points, text) +- Its likely purpose in a document context + +Be factual and specific. Do not speculate beyond what is visible. + +Description: +`; +} diff --git a/src/types.ts b/src/types.ts index 1f80a3c..2a0c834 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,17 @@ /** Shared type definitions for TreeDex. */ +export interface PageImage { + data: string; // base64-encoded image + mime_type: string; // "image/png", "image/jpeg" + alt_text?: string; // from HTML/DOCX or vision LLM + index_on_page?: number; // position within page +} + export interface Page { page_num: number; text: string; token_count: number; + images?: PageImage[]; } export interface TreeNode { diff --git a/treedex/core.py b/treedex/core.py index e9c24f5..045fc4b 100644 --- a/treedex/core.py +++ b/treedex/core.py @@ -26,9 +26,45 @@ STRUCTURE_CONTINUE_PROMPT, RETRIEVAL_PROMPT, ANSWER_PROMPT, + IMAGE_DESCRIPTION_PROMPT, ) +def _describe_images(pages: list[dict], llm=None, verbose: bool = False) -> None: + """Append image descriptions to page text, modifying pages in place.""" + from treedex.loaders import _count_tokens + + for page in pages: + images = page.get("images") + if not images: + continue + + descriptions = [] + for img in images: + alt = img.get("alt_text", "").strip() + if alt: + descriptions.append(f"[Image: {alt}]") + elif llm is not None and getattr(llm, "supports_vision", False) and img.get("data"): + try: + desc = llm.generate_with_image( + IMAGE_DESCRIPTION_PROMPT, + img["data"], + img["mime_type"], + ) + descriptions.append(f"[Image: {desc.strip()}]") + except Exception: + descriptions.append("[Image present]") + else: + descriptions.append("[Image present]") + + if descriptions: + page["text"] = page["text"] + "\n" + "\n".join(descriptions) + page["token_count"] = _count_tokens(page["text"]) + + if verbose and descriptions: + print(f" Page {page['page_num']}: {len(descriptions)} image(s) described") + + class QueryResult: """Result of a TreeDex query.""" @@ -73,7 +109,7 @@ def __init__(self, tree: list[dict], pages: list[dict], @classmethod def from_file(cls, path: str, llm, loader=None, max_tokens: int = 20000, overlap: int = 1, - verbose: bool = True): + verbose: bool = True, extract_images: bool = False): """Build a TreeDex index from a file. Args: @@ -83,6 +119,7 @@ def from_file(cls, path: str, llm, loader=None, max_tokens: Max tokens per page group for structure extraction overlap: Page overlap between groups verbose: Print progress info + extract_images: Extract images from PDFs for vision LLM description """ if verbose: print(f"Loading: {os.path.basename(path)}") @@ -90,7 +127,7 @@ def from_file(cls, path: str, llm, loader=None, if loader is not None: pages = loader.load(path) else: - pages = auto_loader(path) + pages = auto_loader(path, extract_images=extract_images) if verbose: total_tokens = sum(p["token_count"] for p in pages) @@ -104,6 +141,9 @@ def from_pages(cls, pages: list[dict], llm, max_tokens: int = 20000, overlap: int = 1, verbose: bool = True): """Build a TreeDex index from pre-extracted pages.""" + # Describe images before grouping — appends text markers to pages + _describe_images(pages, llm=llm, verbose=verbose) + groups = group_pages(pages, max_tokens=max_tokens, overlap=overlap) if verbose: @@ -205,11 +245,17 @@ def save(self, path: str) -> str: """Save the index to a JSON file.""" stripped = strip_text_from_tree(self.tree) + # Strip images from pages — descriptions are already in text + clean_pages = [] + for p in self.pages: + cp = {k: v for k, v in p.items() if k != "images"} + clean_pages.append(cp) + data = { "version": "1.0", "framework": "TreeDex", "tree": stripped, - "pages": self.pages, + "pages": clean_pages, } with open(path, "w", encoding="utf-8") as f: diff --git a/treedex/llm_backends.py b/treedex/llm_backends.py index 619cfb4..f938e6e 100644 --- a/treedex/llm_backends.py +++ b/treedex/llm_backends.py @@ -53,6 +53,17 @@ def generate(self, prompt: str) -> str: def generate(self, prompt: str) -> str: """Send a prompt and return the generated text.""" + @property + def supports_vision(self) -> bool: + """Whether this backend supports image inputs.""" + return False + + def generate_with_image(self, prompt: str, image_base64: str, mime_type: str) -> str: + """Send a prompt with an image and return the generated text.""" + raise NotImplementedError( + f"{self.__class__.__name__} does not support vision/image inputs." + ) + def __repr__(self): return f"{self.__class__.__name__}()" @@ -85,6 +96,20 @@ def generate(self, prompt: str) -> str: response = model.generate_content(prompt) return response.text + @property + def supports_vision(self) -> bool: + return True + + def generate_with_image(self, prompt: str, image_base64: str, mime_type: str) -> str: + import base64 + model = self._get_client() + image_bytes = base64.b64decode(image_base64) + response = model.generate_content([ + prompt, + {"mime_type": mime_type, "data": image_bytes}, + ]) + return response.text + def __repr__(self): return f"GeminiLLM(model={self.model_name!r})" @@ -115,6 +140,29 @@ def generate(self, prompt: str) -> str: ) return response.choices[0].message.content + @property + def supports_vision(self) -> bool: + return True + + def generate_with_image(self, prompt: str, image_base64: str, mime_type: str) -> str: + client = self._get_client() + response = client.chat.completions.create( + model=self.model_name, + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + { + "type": "image_url", + "image_url": { + "url": f"data:{mime_type};base64,{image_base64}", + }, + }, + ], + }], + ) + return response.choices[0].message.content + def __repr__(self): return f"OpenAILLM(model={self.model_name!r})" @@ -146,6 +194,32 @@ def generate(self, prompt: str) -> str: ) return response.content[0].text + @property + def supports_vision(self) -> bool: + return True + + def generate_with_image(self, prompt: str, image_base64: str, mime_type: str) -> str: + client = self._get_client() + response = client.messages.create( + model=self.model_name, + max_tokens=4096, + messages=[{ + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": mime_type, + "data": image_base64, + }, + }, + {"type": "text", "text": prompt}, + ], + }], + ) + return response.content[0].text + def __repr__(self): return f"ClaudeLLM(model={self.model_name!r})" diff --git a/treedex/loaders.py b/treedex/loaders.py index 6a2d8d2..ad9f58b 100644 --- a/treedex/loaders.py +++ b/treedex/loaders.py @@ -33,9 +33,12 @@ def _text_to_pages(text: str, chars_per_page: int = 3000) -> list[dict]: class PDFLoader: """Load PDF files using PyMuPDF.""" + def __init__(self, extract_images: bool = False): + self.extract_images = extract_images + def load(self, path: str) -> list[dict]: from treedex.pdf_parser import extract_pages - return extract_pages(path) + return extract_pages(path, extract_images=self.extract_images) class TextLoader: @@ -61,6 +64,13 @@ def __init__(self): def handle_starttag(self, tag, attrs): if tag in ("script", "style"): self._skip = True + if tag == "img": + attrs_dict = dict(attrs) + alt = attrs_dict.get("alt", "").strip() + if alt: + self._parts.append(f"\n[Image: {alt}]\n") + else: + self._parts.append("\n[Image]\n") def handle_endtag(self, tag): if tag in ("script", "style"): @@ -100,9 +110,22 @@ def __init__(self, chars_per_page: int = 3000): def load(self, path: str) -> list[dict]: import docx + from docx.oxml.ns import qn doc = docx.Document(path) - text = "\n".join(p.text for p in doc.paragraphs) + parts = [] + for paragraph in doc.paragraphs: + parts.append(paragraph.text) + # Check for inline images in the paragraph's XML + for drawing in paragraph._element.findall(f".//{qn('wp:inline')}"): + doc_pr = drawing.find(qn("wp:docPr")) + if doc_pr is not None: + alt = doc_pr.get("descr", "").strip() + if alt: + parts.append(f"[Image: {alt}]") + else: + parts.append("[Image]") + text = "\n".join(parts) return _text_to_pages(text, self.chars_per_page) @@ -116,7 +139,7 @@ def load(self, path: str) -> list[dict]: } -def auto_loader(path: str) -> list[dict]: +def auto_loader(path: str, extract_images: bool = False) -> list[dict]: """Auto-detect file format and load pages.""" ext = os.path.splitext(path)[1].lower() loader_cls = _EXTENSION_MAP.get(ext) @@ -125,4 +148,6 @@ def auto_loader(path: str) -> list[dict]: f"Unsupported file extension '{ext}'. " f"Supported: {', '.join(_EXTENSION_MAP)}" ) + if ext == ".pdf" and extract_images: + return PDFLoader(extract_images=True).load(path) return loader_cls().load(path) diff --git a/treedex/pdf_parser.py b/treedex/pdf_parser.py index 1c7bc77..5623450 100644 --- a/treedex/pdf_parser.py +++ b/treedex/pdf_parser.py @@ -1,27 +1,69 @@ +import base64 + import fitz # pymupdf import tiktoken _enc = tiktoken.get_encoding("cl100k_base") +_MIME_MAP = { + "png": "image/png", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "jpe": "image/jpeg", + "bmp": "image/bmp", + "tiff": "image/tiff", + "tif": "image/tiff", +} + +_MIN_IMAGE_BYTES = 500 +_MAX_IMAGE_BYTES = 10 * 1024 * 1024 # 10 MB + def _count_tokens(text: str) -> int: return len(_enc.encode(text)) -def extract_pages(pdf_path: str) -> list[dict]: +def extract_pages(pdf_path: str, extract_images: bool = False) -> list[dict]: """Extract text from each page of a PDF. - Returns a list of dicts with page_num, text, and token_count. + Returns a list of dicts with page_num, text, token_count, and optionally images. """ pages = [] with fitz.open(pdf_path) as doc: for i, page in enumerate(doc): text = page.get_text() - pages.append({ + page_dict = { "page_num": i, "text": text, "token_count": _count_tokens(text), - }) + } + + if extract_images: + images = [] + for img_index, img_info in enumerate(page.get_images(full=True)): + xref = img_info[0] + try: + extracted = doc.extract_image(xref) + if extracted is None: + continue + img_bytes = extracted["image"] + ext = extracted.get("ext", "png") + if len(img_bytes) < _MIN_IMAGE_BYTES: + continue + if len(img_bytes) > _MAX_IMAGE_BYTES: + continue + mime_type = _MIME_MAP.get(ext, f"image/{ext}") + images.append({ + "data": base64.b64encode(img_bytes).decode("ascii"), + "mime_type": mime_type, + "index_on_page": img_index, + }) + except Exception: + continue + if images: + page_dict["images"] = images + + pages.append(page_dict) return pages diff --git a/treedex/prompts.py b/treedex/prompts.py index 798b00d..1168bcf 100644 --- a/treedex/prompts.py +++ b/treedex/prompts.py @@ -78,3 +78,14 @@ JSON output: """ + +IMAGE_DESCRIPTION_PROMPT = """\ +Describe this image concisely in 1-2 sentences. Focus on: +- What the image shows (diagram, chart, photo, table, etc.) +- Key information visible (labels, data points, text) +- Its likely purpose in a document context + +Be factual and specific. Do not speculate beyond what is visible. + +Description: +""" diff --git a/video/package-lock.json b/video/package-lock.json new file mode 100644 index 0000000..f157322 --- /dev/null +++ b/video/package-lock.json @@ -0,0 +1,2415 @@ +{ + "name": "treedex-video", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "treedex-video", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "@remotion/cli": "3.3.102", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "remotion": "3.3.102" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "typescript": "^5.4.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.12.tgz", + "integrity": "sha512-CTWgMJtpCyCltrvipZrrcjjRu+rzm6pf9V8muCsJqtKujR3kPmU4ffbckvugNNaRmhxAF1ZI3J+0FUIFLFg8KA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.12.tgz", + "integrity": "sha512-0LacmiIW+X0/LOLMZqYtZ7d4uY9fxYABAYhSSOu+OGQVBqH4N5eIYgkT7bBFnR4Nm3qo6qS3RpHKVrDASqj/uQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.12.tgz", + "integrity": "sha512-sS5CR3XBKQXYpSGMM28VuiUnbX83Z+aWPZzClW+OB2JquKqxoiwdqucJ5qvXS8pM6Up3RtJfDnRQZkz3en2z5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.12.tgz", + "integrity": "sha512-Dpe5hOAQiQRH20YkFAg+wOpcd4PEuXud+aGgKBQa/VriPJA8zuVlgCOSTwna1CgYl05lf6o5els4dtuyk1qJxQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.12.tgz", + "integrity": "sha512-ApGRA6X5txIcxV0095X4e4KKv87HAEXfuDRcGTniDWUUN+qPia8sl/BqG/0IomytQWajnUn4C7TOwHduk/FXBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.12.tgz", + "integrity": "sha512-AMdK2gA9EU83ccXCWS1B/KcWYZCj4P3vDofZZkl/F/sBv/fphi2oUqUTox/g5GMcIxk8CF1CVYTC82+iBSyiUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.12.tgz", + "integrity": "sha512-KUKB9w8G/xaAbD39t6gnRBuhQ8vIYYlxGT2I+mT6UGRnCGRr1+ePFIGBQmf5V16nxylgUuuWVW1zU2ktKkf6WQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.12.tgz", + "integrity": "sha512-vhDdIv6z4eL0FJyNVfdr3C/vdd/Wc6h1683GJsFoJzfKb92dU/v88FhWdigg0i6+3TsbSDeWbsPUXb4dif2abg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.12.tgz", + "integrity": "sha512-29HXMLpLklDfmw7T2buGqq3HImSUaZ1ArmrPOMaNiZZQptOSZs32SQtOHEl8xWX5vfdwZqrBfNf8Te4nArVzKQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.12.tgz", + "integrity": "sha512-JFDuNDTTfgD1LJg7wHA42o2uAO/9VzHYK0leAVnCQE/FdMB599YMH73ux+nS0xGr79pv/BK+hrmdRin3iLgQjg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.12.tgz", + "integrity": "sha512-xTGzVPqm6WKfCC0iuj1fryIWr1NWEM8DMhAIo+4rFgUtwy/lfHl+Obvus4oddzRDbBetLLmojfVZGmt/g/g+Rw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.12.tgz", + "integrity": "sha512-zI1cNgHa3Gol+vPYjIYHzKhU6qMyOQrvZ82REr5Fv7rlh5PG6SkkuCoH7IryPqR+BK2c/7oISGsvPJPGnO2bHQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.12.tgz", + "integrity": "sha512-/C8OFXExoMmvTDIOAM54AhtmmuDHKoedUd0Otpfw3+AuuVGemA1nQK99oN909uZbLEU6Bi+7JheFMG3xGfZluQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.12.tgz", + "integrity": "sha512-qeouyyc8kAGV6Ni6Isz8hUsKMr00EHgVwUKWNp1r4l88fHEoNTDB8mmestvykW6MrstoGI7g2EAsgr0nxmuGYg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.12.tgz", + "integrity": "sha512-s9AyI/5vz1U4NNqnacEGFElqwnHusWa81pskAf8JNDM2eb6b2E6PpBmT8RzeZv6/TxE6/TADn2g9bb0jOUmXwQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.12.tgz", + "integrity": "sha512-e8YA7GQGLWhvakBecLptUiKxOk4E/EPtSckS1i0MGYctW8ouvNUoh7xnU15PGO2jz7BYl8q1R6g0gE5HFtzpqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.12.tgz", + "integrity": "sha512-z2+kUxmOqBS+6SRVd57iOLIHE8oGOoEnGVAmwjm2aENSP35HPS+5cK+FL1l+rhrsJOFIPrNHqDUNechpuG96Sg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.12.tgz", + "integrity": "sha512-PAonw4LqIybwn2/vJujhbg1N9W2W8lw9RtXIvvZoyzoA/4rA4CpiuahVbASmQohiytRsixbNoIOUSjRygKXpyA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.12.tgz", + "integrity": "sha512-+wr1tkt1RERi+Zi/iQtkzmMH4nS8+7UIRxjcyRz7lur84wCkAITT50Olq/HiT4JN2X2bjtlOV6vt7ptW5Gw60Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.12.tgz", + "integrity": "sha512-XEjeUSHmjsAOJk8+pXJu9pFY2O5KKQbHXZWQylJzQuIBeiGrpMeq9sTVrHefHxMOyxUgoKQTcaTS+VK/K5SviA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.12.tgz", + "integrity": "sha512-eRKPM7e0IecUAUYr2alW7JGDejrFJXmpjt4MlfonmQ5Rz9HWpKFGCjuuIRgKO7W9C/CWVFXdJ2GjddsBXqQI4A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.12.tgz", + "integrity": "sha512-iPYKN78t3op2+erv2frW568j1q0RpqX6JOLZ7oPPaAV1VaF7dDstOrNw37PVOYoTWE11pV4A1XUitpdEFNIsPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remotion/bundler": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-3.3.102.tgz", + "integrity": "sha512-E8ePtIQf/MB8ArbwN8SHR6bVBzdFeqbYzZf63RT/jF4Z/dn+aLXnPlA3H3djpJ+x7Rou6eG9584Ny523mh5TkA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "css-loader": "5.2.7", + "esbuild": "0.16.12", + "react-refresh": "0.9.0", + "remotion": "3.3.102", + "style-loader": "2.0.0", + "webpack": "5.76.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/cli": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-3.3.102.tgz", + "integrity": "sha512-SotMrGxcLNwBvNJX9YD6nH0yA7YdRoTY+8EzBIyTvRw+F+IbUcZ+R/zNTyt4RI3i1jOkXlME311sV8pxq7Diyg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/bundler": "3.3.102", + "@remotion/media-utils": "3.3.102", + "@remotion/player": "3.3.102", + "@remotion/renderer": "3.3.102", + "dotenv": "9.0.2", + "memfs": "3.4.3", + "minimist": "1.2.6", + "open": "^8.4.2", + "prompts": "2.4.1", + "remotion": "3.3.102", + "semver": "7.3.5", + "source-map": "0.6.1" + }, + "bin": { + "remotion": "remotion-cli.js" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/compositor-darwin-arm64": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-3.3.102.tgz", + "integrity": "sha512-6Djmzo4dyCbPSzsH6RymaDf4RT70QrdTEOkodFV2hsSZzUCQj26gMMyC54HPBqVaoQnzof2Lb8ABJ1vQQd6ipw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-3.3.102.tgz", + "integrity": "sha512-M14tKW+iZ6Q7i/MSko4Gv68V45VmR1wmRl2sAoFHi9bystmWbi/s8HZL3HmmoNwtsdYyII6gJ4rJSDHePmVLfQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-3.3.102.tgz", + "integrity": "sha512-v6Ys3w+leQMIkh5kkDiYBnlZa3rV0GZHDr80IRlvwamorXh/L8wH4tbyAzRwGQwIXPJO5jyvaQPM9vc7aMDNmQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-3.3.102.tgz", + "integrity": "sha512-pJVsqKTwWv9Cv4eCsdU5LiXjxxnrIt6un2vcFI300H/OxiaTFtjkGxbEhW+XAIzm6dzWO+adwSIdF/41oLZiDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-3.3.102.tgz", + "integrity": "sha512-qFp2dOUCoso4Kqar4X6lvw83YHICu8ayTSOTvy8bh0AseU+BvNkyRNeABTHl45AGFx2ARd4u8J4Ym9ioSO6QyA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-3.3.102.tgz", + "integrity": "sha512-wav82xWktCGdTLQKVzzl1ShjEGOwzTcXoHxfE6my1hs4chUGvvI9jctiuQgEuOtde2x1qAzrIlf2cyFF0CdXeg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-3.3.102.tgz", + "integrity": "sha512-mk/BjKtqjoxAU3M/aGQUoecy016YRmZtpOvPZWOIJUbA81mZjrX7Jns9Nb8IYofKev0HWHgAaTg2vERcNo7LfA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@remotion/media-utils": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-3.3.102.tgz", + "integrity": "sha512-mDjGscR+daZEmtGnNC34EFWuRKca0qY4EKkmyAw1nYPwkjDnP6q5xlvq99NEvfnAoOqboAQtycEVaWwrssoXwQ==", + "license": "MIT", + "dependencies": { + "remotion": "3.3.102" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/player": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-3.3.102.tgz", + "integrity": "sha512-SKKytLccpFmlyMrZ0EpYqu0rvsWWmSam7CKf5W0JwwMiheMlIwuNe9kZven9ghteTmUmTqdlg4JzndekPg1H/A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "remotion": "3.3.102" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-3.3.102.tgz", + "integrity": "sha512-6AS0vzo+BGazdQ0EhroOARbrKnfzM0KB3sSUulXHcwAahFFfaHBOMIciIKbSn6rdXdWfPfl9ieLBUOad2aQMew==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "execa": "5.1.1", + "extract-zip": "2.0.1", + "remotion": "3.3.102", + "source-map": "^0.8.0-beta.0", + "ws": "8.7.0" + }, + "optionalDependencies": { + "@remotion/compositor-darwin-arm64": "3.3.102", + "@remotion/compositor-darwin-x64": "3.3.102", + "@remotion/compositor-linux-arm64-gnu": "3.3.102", + "@remotion/compositor-linux-arm64-musl": "3.3.102", + "@remotion/compositor-linux-x64-gnu": "3.3.102", + "@remotion/compositor-linux-x64-musl": "3.3.102", + "@remotion/compositor-win32-x64-msvc": "3.3.102" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "deprecated": "package has been renamed to acorn-import-attributes", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", + "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "license": "ISC" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.12.tgz", + "integrity": "sha512-eq5KcuXajf2OmivCl4e89AD3j8fbV+UTE9vczEzq5haA07U9oOTzBWlh3+6ZdjJR7Rz2QfWZ2uxZyhZxBgJ4+g==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.16.12", + "@esbuild/android-arm64": "0.16.12", + "@esbuild/android-x64": "0.16.12", + "@esbuild/darwin-arm64": "0.16.12", + "@esbuild/darwin-x64": "0.16.12", + "@esbuild/freebsd-arm64": "0.16.12", + "@esbuild/freebsd-x64": "0.16.12", + "@esbuild/linux-arm": "0.16.12", + "@esbuild/linux-arm64": "0.16.12", + "@esbuild/linux-ia32": "0.16.12", + "@esbuild/linux-loong64": "0.16.12", + "@esbuild/linux-mips64el": "0.16.12", + "@esbuild/linux-ppc64": "0.16.12", + "@esbuild/linux-riscv64": "0.16.12", + "@esbuild/linux-s390x": "0.16.12", + "@esbuild/linux-x64": "0.16.12", + "@esbuild/netbsd-x64": "0.16.12", + "@esbuild/openbsd-x64": "0.16.12", + "@esbuild/sunos-x64": "0.16.12", + "@esbuild/win32-arm64": "0.16.12", + "@esbuild/win32-ia32": "0.16.12", + "@esbuild/win32-x64": "0.16.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "license": "Unlicense" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/memfs": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", + "integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", + "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remotion": { + "version": "3.3.102", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-3.3.102.tgz", + "integrity": "sha512-GhV4Ws6mkAh4jFowRjEEXBfsuM2suY2EMH49Mqm874553LElJ9cFDnNmvaBlWAKP0B+5RywstwND2Wbd/T3WhA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/style-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", + "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", + "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/video/package.json b/video/package.json new file mode 100644 index 0000000..8793fe9 --- /dev/null +++ b/video/package.json @@ -0,0 +1,22 @@ +{ + "name": "treedex-video", + "version": "1.0.0", + "private": true, + "description": "TreeDex cinematic promo video built with Remotion", + "scripts": { + "postinstall": "node patches/patch-remotion.js", + "studio": "remotion preview src/index.ts", + "render": "remotion render src/index.ts TreeDexVideo out/treedex.mp4", + "build": "remotion render src/index.ts TreeDexVideo out/treedex.mp4" + }, + "dependencies": { + "@remotion/cli": "3.3.102", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "remotion": "3.3.102" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "typescript": "^5.4.0" + } +} diff --git a/video/patches/patch-remotion.js b/video/patches/patch-remotion.js new file mode 100644 index 0000000..ccbd17f --- /dev/null +++ b/video/patches/patch-remotion.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +/** + * Patches @remotion/renderer for Android/Termux where os.cpus() returns []. + */ +const fs = require("fs"); +const path = require("path"); + +const target = path.join( + __dirname, + "..", + "node_modules", + "@remotion", + "renderer", + "dist", + "check-apple-silicon.js" +); + +if (!fs.existsSync(target)) { + console.log("Patch target not found, skipping."); + process.exit(0); +} + +let content = fs.readFileSync(target, "utf8"); +const guard = "if (!cpus || cpus.length === 0) return;"; + +if (!content.includes(guard)) { + content = content.replace( + "const cpus = os.cpus();", + `const cpus = os.cpus();\n ${guard}` + ); + fs.writeFileSync(target, content); + console.log("Patched @remotion/renderer for Android compatibility."); +} else { + console.log("Patch already applied."); +} diff --git a/video/public/audio/bgm_ambient.wav b/video/public/audio/bgm_ambient.wav new file mode 100644 index 0000000..3673ebb Binary files /dev/null and b/video/public/audio/bgm_ambient.wav differ diff --git a/video/public/audio/vo_act1.wav b/video/public/audio/vo_act1.wav new file mode 100644 index 0000000..b441c65 Binary files /dev/null and b/video/public/audio/vo_act1.wav differ diff --git a/video/public/audio/vo_act2.wav b/video/public/audio/vo_act2.wav new file mode 100644 index 0000000..b2208c4 Binary files /dev/null and b/video/public/audio/vo_act2.wav differ diff --git a/video/public/audio/vo_act3.wav b/video/public/audio/vo_act3.wav new file mode 100644 index 0000000..b1e13a6 Binary files /dev/null and b/video/public/audio/vo_act3.wav differ diff --git a/video/public/audio/vo_act4.wav b/video/public/audio/vo_act4.wav new file mode 100644 index 0000000..5e167ca Binary files /dev/null and b/video/public/audio/vo_act4.wav differ diff --git a/video/public/audio/vo_act5.wav b/video/public/audio/vo_act5.wav new file mode 100644 index 0000000..ea6157a Binary files /dev/null and b/video/public/audio/vo_act5.wav differ diff --git a/video/remotion.config.ts b/video/remotion.config.ts new file mode 100644 index 0000000..871bbc5 --- /dev/null +++ b/video/remotion.config.ts @@ -0,0 +1,4 @@ +import { Config } from "remotion"; + +Config.Rendering.setImageFormat("jpeg"); +Config.Output.setOverwriteOutput(true); diff --git a/video/scripts/generate-bgm.py b/video/scripts/generate-bgm.py new file mode 100644 index 0000000..048002f --- /dev/null +++ b/video/scripts/generate-bgm.py @@ -0,0 +1,95 @@ +""" +Generate ambient background music for TreeDex promo video. +Minimal electronic drone — dark, techy, cinematic. +60 seconds at 22050 Hz mono WAV. +""" +import struct +import math +import os + +SAMPLE_RATE = 22050 +DURATION = 62 # slightly longer than video to avoid cutoff +NUM_SAMPLES = SAMPLE_RATE * DURATION + +def write_wav(filename, samples, sample_rate=22050): + """Write 16-bit mono WAV file.""" + num = len(samples) + with open(filename, 'wb') as f: + # Header + f.write(b'RIFF') + data_size = num * 2 + f.write(struct.pack(' 0: + print(data['audios'][0]) + else: + print('ERROR:' + json.dumps(data), file=sys.stderr) + sys.exit(1) +except Exception as e: + print('ERROR:' + str(e), file=sys.stderr) + sys.exit(1) +" 2>&1) + + if echo "$audio_b64" | grep -q "^ERROR:"; then + echo " FAILED: $audio_b64" + return 1 + fi + + echo "$audio_b64" | base64 -d > "$outfile" + # Print duration + python3 -c " +import struct +with open('$outfile','rb') as f: + f.read(4);f.read(4);f.read(4) + while True: + cid=f.read(4);cs=struct.unpack(' { + return ( + <> + + + ); +}; diff --git a/video/src/TreeDexVideo.tsx b/video/src/TreeDexVideo.tsx new file mode 100644 index 0000000..738de30 --- /dev/null +++ b/video/src/TreeDexVideo.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import { AbsoluteFill, Audio as RawAudio, Sequence, Series, interpolate, staticFile } from "remotion"; + +// Remotion 3.3.102 + React 18 types require placeholder prop — cast to bypass +const Audio = RawAudio as unknown as React.FC<{ + src: string; + volume?: number | ((frame: number) => number); + startFrom?: number; + endAt?: number; + playbackRate?: number; +}>; +import { Act1Hero } from "./scenes/Act1Hero"; +import { Act2Problem } from "./scenes/Act2Problem"; +import { Act3Solution } from "./scenes/Act3Solution"; +import { Act4Query } from "./scenes/Act4Query"; +import { Act5Closing } from "./scenes/Act5Closing"; +import { ACT_DURATIONS, TOTAL_FRAMES, WIDTH, HEIGHT } from "./constants/timing"; +import { COLORS } from "./constants/colors"; +import { SceneTransition } from "./components/SceneTransition"; +import { FilmGrain } from "./components/FilmGrain"; +import { ColorGrade } from "./components/ColorGrade"; + +// ── Global frame offsets for audio placement ────── +const ACT_STARTS = { + hero: 0, + problem: ACT_DURATIONS.hero, + solution: ACT_DURATIONS.hero + ACT_DURATIONS.problem, + query: ACT_DURATIONS.hero + ACT_DURATIONS.problem + ACT_DURATIONS.solution, + closing: ACT_DURATIONS.hero + ACT_DURATIONS.problem + ACT_DURATIONS.solution + ACT_DURATIONS.query, +}; + +// ── Voiceover tracks — continuous per-act narration ─ +const VOICEOVERS = [ + { src: "audio/vo_act1.wav", startFrame: ACT_STARTS.hero + 30, durationFrames: 200 }, + { src: "audio/vo_act2.wav", startFrame: ACT_STARTS.problem + 25, durationFrames: 280 }, + { src: "audio/vo_act3.wav", startFrame: ACT_STARTS.solution + 15, durationFrames: 460 }, + { src: "audio/vo_act4.wav", startFrame: ACT_STARTS.query + 15, durationFrames: 340 }, + { src: "audio/vo_act5.wav", startFrame: ACT_STARTS.closing + 20, durationFrames: 300 }, +]; + +// ── Background music volume ducking ─────────────── +function useBgmVolume(): (f: number) => number { + return (f: number) => { + // Base volume + let vol = 0.15; + + // Duck during voiceover + for (const vo of VOICEOVERS) { + if (f >= vo.startFrame && f < vo.startFrame + vo.durationFrames) { + vol = 0.05; + break; + } + } + + // Fade in (first 3s = 90 frames) + const fadeIn = interpolate(f, [0, 90], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + // Fade out (last 3s = 90 frames) + const fadeOut = interpolate(f, [TOTAL_FRAMES - 90, TOTAL_FRAMES], [1, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return vol * fadeIn * fadeOut; + }; +} + +export const TreeDexVideo: React.FC = () => { + const bgmVolume = useBgmVolume(); + + return ( + + {/* ── Visual scenes ── */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* ── Background music ── */} + + ); +}; diff --git a/video/src/components/AnimatedTree.tsx b/video/src/components/AnimatedTree.tsx new file mode 100644 index 0000000..aafcdf7 --- /dev/null +++ b/video/src/components/AnimatedTree.tsx @@ -0,0 +1,381 @@ +import React from "react"; +import { useCurrentFrame } from "remotion"; +import { COLORS } from "../constants/colors"; +import { useTreeLayout2D, NodeLayout, EdgeLayout } from "../hooks/useTreeLayout2D"; + +interface AnimatedTreeProps { + /** 0..1 how much of the tree is revealed (staggered) */ + revealProgress: number; + /** Node IDs to highlight (glow green) */ + highlightNodes?: string[]; + /** 0..1 progress of pulse traveling from root */ + pulseProgress?: number; + width?: number; + height?: number; + style?: React.CSSProperties; +} + +export const AnimatedTree: React.FC = ({ + revealProgress, + highlightNodes = [], + pulseProgress, + width = 700, + height = 500, + style = {}, +}) => { + const frame = useCurrentFrame(); + const { nodes, edges } = useTreeLayout2D(width, height); + + const totalNodes = nodes.length; + const highlightSet = new Set(highlightNodes); + const hasHighlight = highlightNodes.length > 0; + + return ( + + {/* SVG Definitions for reusable gradients & filters */} + + {/* Node gradient — cyan core with darker edge */} + + + + + + {/* Highlighted node gradient — green */} + + + + + + {/* Glow filter for highlighted nodes */} + + + + + + + + + {/* Glow filter for normal nodes */} + + + + + + + + + {/* Pulse glow */} + + + + + + {/* Edges — drawn behind nodes */} + {edges.map((edge) => { + const fromNode = nodes.find((n) => n.nodeId === edge.from); + if (!fromNode) return null; + + const edgeThreshold = (fromNode.index + 1) / totalNodes; + const edgeVisible = revealProgress >= edgeThreshold; + const edgeProgress = edgeVisible + ? Math.min((revealProgress - edgeThreshold) / (1 / totalNodes), 1) + : 0; + + const isHighlightEdge = + hasHighlight && + (highlightSet.has(edge.from) && highlightSet.has(edge.to)); + const edgeOpacity = hasHighlight + ? isHighlightEdge ? 0.85 : 0.08 + : 0.5; + + return ( + + ); + })} + + {/* Nodes — drawn on top */} + {nodes.map((node) => { + const nodeThreshold = node.index / totalNodes; + const nodeVisible = revealProgress >= nodeThreshold; + const revealT = nodeVisible + ? Math.min((revealProgress - nodeThreshold) / (1 / totalNodes), 1) + : 0; + + const isHighlighted = highlightSet.has(node.nodeId); + const dimmed = hasHighlight && !isHighlighted; + + return ( + + ); + })} + + {/* Pulse dot traveling along edges */} + {pulseProgress !== undefined && pulseProgress > 0 && pulseProgress < 1 && ( + + )} + + ); +}; + +// ── Bezier Edge ────────────────────────────────── + +const TreeEdge: React.FC<{ + edge: EdgeLayout; + drawProgress: number; + opacity: number; + highlighted: boolean; + frame: number; +}> = ({ edge, drawProgress, opacity, highlighted, frame }) => { + // Smooth cubic bezier curve + const midY = edge.y1 + (edge.y2 - edge.y1) * 0.5; + const d = `M ${edge.x1} ${edge.y1} C ${edge.x1} ${midY}, ${edge.x2} ${midY}, ${edge.x2} ${edge.y2}`; + + const dx = edge.x2 - edge.x1; + const dy = edge.y2 - edge.y1; + const pathLen = Math.sqrt(dx * dx + dy * dy) * 1.4; + + const baseColor = highlighted ? COLORS.success : COLORS.primary; + const glowWidth = highlighted ? 6 : 3; + const strokeWidth = highlighted ? 2 : 1.2; + + return ( + + {/* Glow layer */} + + {/* Main stroke */} + + + ); +}; + +// ── Node Circle ────────────────────────────────── + +const TreeNodeCircle: React.FC<{ + node: NodeLayout; + revealT: number; + highlighted: boolean; + dimmed: boolean; + frame: number; + isRoot: boolean; +}> = ({ node, revealT, highlighted, dimmed, frame, isRoot }) => { + if (revealT <= 0) return null; + + const baseRadius = isRoot ? 10 : highlighted ? 9 : 6; + const radius = baseRadius * Math.min(revealT * 1.2, 1); + const opacity = dimmed ? 0.15 : revealT; + const pulse = highlighted ? 1 + 0.12 * Math.sin(frame * 0.12) : 1; + const filter = highlighted ? "url(#glowGreen)" : dimmed ? undefined : "url(#glowCyan)"; + const fill = highlighted ? "url(#hlGrad)" : "url(#nodeGrad)"; + + return ( + + {/* Outer ring for highlighted */} + {highlighted && ( + <> + + + + )} + {/* Main node */} + + {/* Bright center dot */} + + + {/* Label with background for readability */} + {!dimmed && ( + <> + + + {node.title.length > 20 + ? node.title.slice(0, 18) + "\u2026" + : node.title} + + + )} + + {/* Node ID badge for highlighted */} + {highlighted && ( + <> + + + {node.nodeId} + + + )} + + ); +}; + +// ── Pulse Dot ──────────────────────────────────── + +const PulseDot: React.FC<{ + edges: EdgeLayout[]; + progress: number; + frame: number; +}> = ({ edges, progress, frame }) => { + if (edges.length === 0) return null; + + const edgeIdx = Math.min( + Math.floor(progress * edges.length), + edges.length - 1, + ); + const edge = edges[edgeIdx]; + const edgeT = (progress * edges.length) % 1; + + // Interpolate along bezier curve (simplified as linear for now) + const x = edge.x1 + (edge.x2 - edge.x1) * edgeT; + const midY = edge.y1 + (edge.y2 - edge.y1) * 0.5; + // Approximate cubic bezier y + const t = edgeT; + const y = + (1 - t) * (1 - t) * (1 - t) * edge.y1 + + 3 * (1 - t) * (1 - t) * t * midY + + 3 * (1 - t) * t * t * midY + + t * t * t * edge.y2; + + const pulseSize = 4 + 2 * Math.sin(frame * 0.15); + + return ( + + {/* Trail glow */} + + {/* Outer glow ring */} + + {/* Core dot */} + + {/* Bright center */} + + + ); +}; diff --git a/video/src/components/ColorGrade.tsx b/video/src/components/ColorGrade.tsx new file mode 100644 index 0000000..cea39c8 --- /dev/null +++ b/video/src/components/ColorGrade.tsx @@ -0,0 +1,73 @@ +import React from "react"; + +interface ColorGradeProps { + /** Slight hue tint — "cool" (blue), "warm" (amber), "neutral" */ + tone?: "cool" | "warm" | "neutral"; + /** Contrast boost 0..1 */ + contrast?: number; + /** Vignette darkness 0..1 */ + vignette?: number; +} + +/** + * Cinematic color grading overlay. + * Applies subtle color correction + vignette for film-like feel. + */ +export const ColorGrade: React.FC = ({ + tone = "cool", + contrast = 0.05, + vignette = 0.35, +}) => { + const toneColor = + tone === "cool" + ? "rgba(60, 120, 200, 0.04)" + : tone === "warm" + ? "rgba(200, 150, 80, 0.04)" + : "transparent"; + + return ( + <> + {/* Color tone overlay */} + {tone !== "neutral" && ( +
+ )} + + {/* Contrast boost via mix-blend */} + {contrast > 0 && ( +
+ )} + + {/* Cinematic vignette */} + {vignette > 0 && ( +
+ )} + + ); +}; diff --git a/video/src/components/FeatureHighlight.tsx b/video/src/components/FeatureHighlight.tsx new file mode 100644 index 0000000..71b3a66 --- /dev/null +++ b/video/src/components/FeatureHighlight.tsx @@ -0,0 +1,270 @@ +import React from "react"; +import { useCurrentFrame, spring } from "remotion"; +import { FPS } from "../constants/timing"; +import { COLORS } from "../constants/colors"; + +// ── Feature Card (for closing act) ─────────────── + +export interface Feature { + icon: React.ReactNode; + title: string; + subtitle: string; + color?: string; +} + +interface FeatureHighlightProps { + features: Feature[]; + startFrame: number; + stagger?: number; + style?: React.CSSProperties; +} + +export const FeatureHighlight: React.FC = ({ + features, + startFrame, + stagger = 12, + style = {}, +}) => { + const frame = useCurrentFrame(); + + return ( +
+ {features.map((feat, i) => { + const s = spring({ + frame: Math.max(0, frame - startFrame - i * stagger), + fps: FPS, + config: { damping: 16, mass: 0.6 }, + }); + + const color = feat.color ?? COLORS.primary; + + return ( +
+
{feat.icon}
+
+ {feat.title} +
+
+ {feat.subtitle} +
+
+ ); + })} +
+ ); +}; + +// ── Narrator Text (subtitle bar for TTS sync) ──── + +interface NarratorTextProps { + text: string; + startFrame: number; + endFrame: number; + style?: React.CSSProperties; +} + +export const NarratorText: React.FC = ({ + text, + startFrame, + endFrame, + style = {}, +}) => { + const frame = useCurrentFrame(); + + if (frame < startFrame || frame >= endFrame) return null; + + const localFrame = frame - startFrame; + const duration = endFrame - startFrame; + + const fadeIn = Math.min(localFrame / 8, 1); + const fadeOut = Math.min((endFrame - frame) / 8, 1); + const opacity = Math.min(fadeIn, fadeOut); + + // Progressive word reveal + const words = text.split(" "); + const wordsToShow = Math.ceil((localFrame / (duration * 0.6)) * words.length); + const visibleText = words.slice(0, wordsToShow).join(" "); + const hiddenText = words.slice(wordsToShow).join(" "); + + return ( +
+ {/* Audio waveform indicator */} +
+ {[0.4, 0.7, 1, 0.6, 0.3].map((h, i) => ( +
i * 4 ? 0.3 : 0), + }} + /> + ))} +
+
+ + {visibleText} + + {hiddenText && ( + + {" " + hiddenText} + + )} +
+
+ ); +}; + +// ── Comparison Badge (VS indicator) ────────────── + +interface ComparisonProps { + leftLabel: string; + rightLabel: string; + leftColor?: string; + rightColor?: string; + startFrame: number; + style?: React.CSSProperties; +} + +export const ComparisonBadge: React.FC = ({ + leftLabel, + rightLabel, + leftColor = COLORS.chaosRed, + rightColor = COLORS.success, + startFrame, + style = {}, +}) => { + const frame = useCurrentFrame(); + const s = spring({ + frame: Math.max(0, frame - startFrame), + fps: FPS, + config: { damping: 16, mass: 0.6 }, + }); + + if (s < 0.01) return null; + + return ( +
+
+ {leftLabel} +
+
+ VS +
+
+ {rightLabel} +
+
+ ); +}; diff --git a/video/src/components/FilmGrain.tsx b/video/src/components/FilmGrain.tsx new file mode 100644 index 0000000..30755ac --- /dev/null +++ b/video/src/components/FilmGrain.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { useCurrentFrame } from "remotion"; + +interface FilmGrainProps { + opacity?: number; + /** Grain refresh speed — lower = slower grain update */ + speed?: number; +} + +/** + * SVG-based film grain overlay. + * Uses feTurbulence seeded by frame for animated noise. + */ +export const FilmGrain: React.FC = ({ + opacity = 0.035, + speed = 3, +}) => { + const frame = useCurrentFrame(); + // Change seed every N frames for grain flicker + const seed = Math.floor(frame / speed); + + return ( + + + + + + + + ); +}; diff --git a/video/src/components/FloatingCards.tsx b/video/src/components/FloatingCards.tsx new file mode 100644 index 0000000..7c6dedc --- /dev/null +++ b/video/src/components/FloatingCards.tsx @@ -0,0 +1,206 @@ +import React from "react"; +import { useCurrentFrame, spring } from "remotion"; +import { FPS } from "../constants/timing"; +import { COLORS, TERM } from "../constants/colors"; +import { IconHash } from "./Icons"; + +interface FloatingCardsProps { + /** 0..1 progress for floating phase */ + floatProgress: number; + /** 0..1 progress for shatter phase */ + shatterProgress: number; + width?: number; + height?: number; +} + +const CARDS = [ + { title: "chunk_07", lines: [0.85, 0.6, 0.9, 0.45, 0.7, 0.8] }, + { title: "chunk_12", lines: [0.7, 0.9, 0.5, 0.8, 0.6, 0.75] }, + { title: "chunk_23", lines: [0.6, 0.8, 0.7, 0.55, 0.9, 0.65] }, + { title: "chunk_31", lines: [0.9, 0.5, 0.75, 0.8, 0.6, 0.7] }, + { title: "chunk_38", lines: [0.75, 0.85, 0.6, 0.7, 0.5, 0.9] }, + { title: "chunk_41", lines: [0.55, 0.7, 0.85, 0.6, 0.8, 0.65] }, +]; + +function seededRandom(seed: number): number { + const x = Math.sin(seed * 12.9898 + seed * 78.233) * 43758.5453; + return x - Math.floor(x); +} + +export const FloatingCards: React.FC = ({ + floatProgress, + shatterProgress, + width = 700, + height = 600, +}) => { + const frame = useCurrentFrame(); + + return ( +
+ {/* Connection lines between cards (showing lack of structure) */} + {floatProgress > 0.3 && shatterProgress < 0.5 && ( + + {[0, 1, 2, 3, 4].map((i) => { + const opacity = (1 - shatterProgress) * 0.08; + const x1 = 120 + (i % 3) * 200 + 80; + const y1 = 120 + Math.floor(i / 3) * 200 + 90; + const x2 = 120 + ((i + 1) % 3) * 200 + 80; + const y2 = 120 + Math.floor((i + 1) / 3) * 200 + 90; + return ( + + ); + })} + + )} + + {CARDS.map((card, i) => { + const col = i % 3; + const row = Math.floor(i / 3); + const baseX = 40 + col * 200; + const baseY = 80 + row * 200; + + // Staggered entrance + const entranceS = spring({ + frame: Math.max(0, frame - i * 6), + fps: FPS, + config: { damping: 16, mass: 0.6 }, + }); + + // Multi-axis float wobble (more organic) + const t1 = frame * 0.035 + i * 1.3; + const t2 = frame * 0.028 + i * 0.9; + const wobbleX = Math.sin(t1) * 12 * floatProgress + Math.sin(t1 * 1.7) * 5 * floatProgress; + const wobbleY = Math.cos(t2) * 10 * floatProgress + Math.cos(t2 * 1.4) * 4 * floatProgress; + const wobbleRot = Math.sin(frame * 0.025 + i * 2.1) * 4 * floatProgress; + + // Shatter with physics-like easing + const shatterAngle = (i / CARDS.length) * Math.PI * 2 + seededRandom(i * 7) * 1.5; + const shatterEased = shatterProgress * shatterProgress; // Accelerating + const shatterDist = shatterEased * 800; + const shatterX = Math.cos(shatterAngle) * shatterDist; + const shatterY = Math.sin(shatterAngle) * shatterDist - shatterEased * 200; + const shatterRot = shatterEased * (seededRandom(i * 7) > 0.5 ? 540 : -540); + const shatterScale = 1 - shatterProgress * 0.5; + const shatterOpacity = Math.max(0, 1 - shatterProgress * 1.5); + + const x = baseX + wobbleX + shatterX; + const y = baseY + wobbleY + shatterY; + const rotation = wobbleRot + shatterRot; + + return ( +
+ {/* Header with icon */} +
+
+ +
+
+ {card.title} +
+
+ + {/* Fake text lines with varying opacity */} + {card.lines.map((w, j) => ( +
+ ))} + + {/* Bottom "token" indicator */} +
+ 256 tokens +
+
+ ); + })} +
+ ); +}; diff --git a/video/src/components/GlitchEffect.tsx b/video/src/components/GlitchEffect.tsx new file mode 100644 index 0000000..f6e0ac4 --- /dev/null +++ b/video/src/components/GlitchEffect.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { useCurrentFrame, interpolate } from "remotion"; + +interface GlitchEffectProps { + /** Frame range where glitch is active */ + startFrame: number; + endFrame: number; + /** Glitch intensity 0..1 */ + intensity?: number; + /** Color for chromatic aberration */ + color?: string; +} + +/** + * Overlay that adds digital glitch / interference effect. + * Horizontal scan bars + chromatic shift + jitter. + */ +export const GlitchEffect: React.FC = ({ + startFrame, + endFrame, + intensity = 0.5, + color = "#ff3333", +}) => { + const frame = useCurrentFrame(); + + if (frame < startFrame || frame > endFrame) return null; + + const progress = interpolate(frame, [startFrame, endFrame], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + // Pseudo-random from frame for deterministic glitch + const seed = Math.sin(frame * 12.9898 + 78.233) * 43758.5453; + const rand = seed - Math.floor(seed); + + // Only glitch on certain frames for stuttering effect + const isGlitchFrame = rand > 0.55; + if (!isGlitchFrame && intensity < 0.8) return null; + + const barCount = 3 + Math.floor(rand * 4); + const jitterX = (rand - 0.5) * 8 * intensity; + + return ( +
+ {/* Horizontal scan bars */} + {Array.from({ length: barCount }).map((_, i) => { + const barSeed = Math.sin((frame + i) * 45.233) * 43758.5453; + const barRand = barSeed - Math.floor(barSeed); + const y = barRand * 1080; + const h = 2 + barRand * 6 * intensity; + + return ( +
+ ); + })} + + {/* Chromatic aberration hint — thin colored line at random position */} +
+
+ ); +}; diff --git a/video/src/components/GlowOrb.tsx b/video/src/components/GlowOrb.tsx new file mode 100644 index 0000000..85e76fb --- /dev/null +++ b/video/src/components/GlowOrb.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { useCurrentFrame } from "remotion"; +import { COLORS } from "../constants/colors"; + +interface GlowOrbProps { + x: number; + y: number; + size?: number; + color?: string; + pulseSpeed?: number; + opacity?: number; +} + +export const GlowOrb: React.FC = ({ + x, + y, + size = 200, + color = COLORS.primary, + pulseSpeed = 0.04, + opacity = 0.4, +}) => { + const frame = useCurrentFrame(); + + // Multi-frequency pulse for organic feel + const pulse1 = Math.sin(frame * pulseSpeed) * 0.12; + const pulse2 = Math.sin(frame * pulseSpeed * 1.7 + 1) * 0.06; + const scale = 0.88 + pulse1 + pulse2; + + const opHex = Math.round(opacity * 255) + .toString(16) + .padStart(2, "0"); + const opHalf = Math.round(opacity * 128) + .toString(16) + .padStart(2, "0"); + + return ( + <> + {/* Outer soft glow */} +
+ {/* Inner concentrated glow */} +
+ + ); +}; diff --git a/video/src/components/Icons.tsx b/video/src/components/Icons.tsx new file mode 100644 index 0000000..692d191 --- /dev/null +++ b/video/src/components/Icons.tsx @@ -0,0 +1,158 @@ +import React from "react"; + +interface IconProps { + size?: number; + color?: string; + strokeWidth?: number; +} + +/** Scissors — split/cut */ +export const IconScissors: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + + + +); + +/** Arrow right — embed/transform */ +export const IconArrowRight: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + +); + +/** Database/grid — vector DB */ +export const IconDatabase: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + +); + +/** Search/magnifier */ +export const IconSearch: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + +); + +/** Document/file — parse structure */ +export const IconDocument: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + + + +); + +/** Tree/branch — build tree */ +export const IconTree: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + + + + + + + + + + + +); + +/** Map pin — map pages */ +export const IconMapPin: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + +); + +/** Question mark — receive query */ +export const IconQuestion: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + +); + +/** Check circle — select nodes */ +export const IconCheckCircle: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + +); + +/** Sparkles/wand — generate */ +export const IconSparkles: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + +); + +/** Lightning bolt — zero vector DB / fast */ +export const IconBolt: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + +); + +/** CPU/chip — LLM providers */ +export const IconCpu: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + + + + + + + + +); + +/** Package/box — pip install */ +export const IconPackage: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + + +); + +/** Hash — chunk identifier */ +export const IconHash: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + + +); + +/** Git branch / network — structure-aware */ +export const IconGitBranch: React.FC = ({ size = 16, color = "currentColor", strokeWidth = 2 }) => ( + + + + + + +); diff --git a/video/src/components/ParticleBackground.tsx b/video/src/components/ParticleBackground.tsx new file mode 100644 index 0000000..5ab49cd --- /dev/null +++ b/video/src/components/ParticleBackground.tsx @@ -0,0 +1,144 @@ +import React, { useMemo } from "react"; +import { useCurrentFrame } from "remotion"; +import { COLORS } from "../constants/colors"; + +interface ParticleBackgroundProps { + count?: number; + /** 0 = scattered, 1 = converged to center */ + coalescence?: number; + width?: number; + height?: number; + color?: string; +} + +interface Particle { + id: number; + startX: number; + startY: number; + size: number; + speed: number; + phase: number; + /** Orbit radius around center when coalesced */ + orbitRadius: number; + orbitSpeed: number; + /** 0=round, 1=diamond, 2=line */ + shape: number; + /** Depth layer: 0=far, 1=mid, 2=near */ + layer: number; +} + +function seededRandom(seed: number): number { + const x = Math.sin(seed * 12.9898 + seed * 78.233) * 43758.5453; + return x - Math.floor(x); +} + +export const ParticleBackground: React.FC = ({ + count = 150, + coalescence = 0, + width = 1920, + height = 1080, + color = COLORS.primary, +}) => { + const frame = useCurrentFrame(); + + const particles = useMemo(() => { + return Array.from({ length: count }, (_, i) => { + const layer = i < count * 0.3 ? 0 : i < count * 0.7 ? 1 : 2; + return { + id: i, + startX: seededRandom(i * 3 + 1) * width, + startY: seededRandom(i * 3 + 2) * height, + size: [1, 2, 3.5][layer] + seededRandom(i * 3 + 3) * [0.5, 1, 2][layer], + speed: [0.2, 0.5, 0.8][layer] + seededRandom(i * 7) * 0.3, + phase: seededRandom(i * 11) * Math.PI * 2, + orbitRadius: 20 + seededRandom(i * 13) * 180, + orbitSpeed: (seededRandom(i * 17) - 0.5) * 0.015, + shape: Math.floor(seededRandom(i * 19) * 3), + layer, + }; + }); + }, [count, width, height]); + + const centerX = width / 2; + const centerY = height / 2; + + return ( +
+ {particles.map((p) => { + // Layered parallax float + const parallax = [0.3, 0.6, 1.0][p.layer]; + const floatX = + p.startX + + Math.sin(frame * 0.018 * p.speed * parallax + p.phase) * 40 * parallax; + const floatY = + p.startY + + Math.cos(frame * 0.013 * p.speed * parallax + p.phase + 0.5) * 30 * parallax; + + // When coalescing: orbit around center instead of static converge + const orbitAngle = frame * p.orbitSpeed + p.phase; + const orbitX = centerX + Math.cos(orbitAngle) * p.orbitRadius * (1 - coalescence * 0.7); + const orbitY = centerY + Math.sin(orbitAngle) * p.orbitRadius * 0.6 * (1 - coalescence * 0.7); + + // Blend between scattered and orbiting + const x = floatX + (orbitX - floatX) * coalescence; + const y = floatY + (orbitY - floatY) * coalescence; + + // Layered opacity + const baseOpacity = [0.15, 0.3, 0.55][p.layer]; + const breathe = 0.15 * Math.sin(frame * 0.025 + p.phase); + const coalesceBoost = coalescence * 0.2; + const opacity = Math.max(0, Math.min(1, baseOpacity + breathe + coalesceBoost)); + + // Size grows slightly when near center + const dist = Math.sqrt( + (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY), + ); + const proxBoost = Math.max(0, 1 - dist / 500) * coalescence * 2; + const size = p.size + proxBoost; + + const glowSize = size * [2, 3, 4][p.layer]; + + return ( +
+ ); + })} + + {/* Central glow that intensifies with coalescence */} + {coalescence > 0.1 && ( +
+ )} +
+ ); +}; diff --git a/video/src/components/SceneTransition.tsx b/video/src/components/SceneTransition.tsx new file mode 100644 index 0000000..1a3f22e --- /dev/null +++ b/video/src/components/SceneTransition.tsx @@ -0,0 +1,135 @@ +import React from "react"; +import { useCurrentFrame, interpolate } from "remotion"; +import { COLORS } from "../constants/colors"; +import { FADE_FRAMES } from "../constants/timing"; + +interface SceneTransitionProps { + /** Total duration of this act in frames */ + duration: number; + /** Color accent for enter/exit washes */ + enterColor?: string; + exitColor?: string; + /** Transition style */ + mode?: "fade" | "zoom" | "wipe" | "blur"; + children: React.ReactNode; +} + +/** + * Wraps an act with cinematic enter/exit transitions. + * Replaces the simple opacity fade with richer effects. + */ +export const SceneTransition: React.FC = ({ + duration, + enterColor = COLORS.primary, + exitColor = COLORS.primary, + mode = "zoom", + children, +}) => { + const frame = useCurrentFrame(); + const TF = FADE_FRAMES + 5; // slightly longer transition window + + // Enter progress 0→1 + const enterP = interpolate(frame, [0, TF], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + // Exit progress 0→1 + const exitP = interpolate(frame, [duration - TF, duration], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + // Base opacity (shared by all modes) + const opacity = Math.min(enterP, 1 - exitP); + + // Mode-specific transforms + let transform = ""; + let filter = ""; + + if (mode === "zoom") { + const enterScale = interpolate(enterP, [0, 1], [1.04, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const exitScale = interpolate(exitP, [0, 1], [1, 0.97], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const scale = frame < duration / 2 ? enterScale : exitScale; + transform = `scale(${scale})`; + } + + if (mode === "blur") { + const enterBlur = interpolate(enterP, [0, 1], [8, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const exitBlur = interpolate(exitP, [0, 1], [0, 6], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const blur = frame < duration / 2 ? enterBlur : exitBlur; + filter = `blur(${blur}px)`; + } + + // Color wash overlay (subtle tint during transitions) + const enterWashOpacity = interpolate(enterP, [0, 0.3, 1], [0.15, 0.08, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const exitWashOpacity = interpolate(exitP, [0, 0.7, 1], [0, 0.08, 0.12], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return ( +
+ {/* Content with transition effects */} +
+ {children} +
+ + {/* Enter color wash */} + {enterP < 1 && ( +
+ )} + + {/* Exit color wash */} + {exitP > 0 && ( +
+ )} +
+ ); +}; diff --git a/video/src/components/StepFlow.tsx b/video/src/components/StepFlow.tsx new file mode 100644 index 0000000..f3cacbb --- /dev/null +++ b/video/src/components/StepFlow.tsx @@ -0,0 +1,182 @@ +import React from "react"; +import { useCurrentFrame, spring } from "remotion"; +import { FPS } from "../constants/timing"; +import { COLORS } from "../constants/colors"; + +export interface Step { + number: number; + label: string; + /** Optional icon (SVG component or ReactNode) */ + icon?: React.ReactNode; + color?: string; +} + +interface StepFlowProps { + steps: Step[]; + /** Frame at which first step appears */ + startFrame: number; + /** Frames between each step reveal */ + stagger?: number; + /** Which step is currently active (1-indexed, 0=none) */ + activeStep?: number; + /** "horizontal" or "vertical" */ + direction?: "horizontal" | "vertical"; + /** Overall style */ + style?: React.CSSProperties; +} + +export const StepFlow: React.FC = ({ + steps, + startFrame, + stagger = 20, + activeStep = 0, + direction = "horizontal", + style = {}, +}) => { + const frame = useCurrentFrame(); + + const isHorizontal = direction === "horizontal"; + + return ( +
+ {steps.map((step, i) => { + const stepDelay = startFrame + i * stagger; + const s = spring({ + frame: Math.max(0, frame - stepDelay), + fps: FPS, + config: { damping: 16, mass: 0.6 }, + }); + + const isActive = activeStep >= step.number; + const isCurrent = activeStep === step.number; + const color = step.color ?? COLORS.primary; + const isLast = i === steps.length - 1; + + return ( + + {/* Step node */} +
+ {/* Circle with number/icon */} +
+ {step.icon ?? step.number} +
+ + {/* Label */} +
+ {step.label} +
+
+ + {/* Connector line between steps */} + {!isLast && ( + step.number} + color={color} + horizontal={isHorizontal} + /> + )} +
+ ); + })} +
+ ); +}; + +// ── Connector ──────────────────────────────────── + +const StepConnector: React.FC<{ + frame: number; + startFrame: number; + isActive: boolean; + color: string; + horizontal: boolean; +}> = ({ frame, startFrame, isActive, color, horizontal }) => { + const s = spring({ + frame: Math.max(0, frame - startFrame), + fps: FPS, + config: { damping: 20, mass: 0.5 }, + }); + + if (horizontal) { + return ( +
+ ); + } + + return ( +
+ ); +}; diff --git a/video/src/components/TerminalWindow.tsx b/video/src/components/TerminalWindow.tsx new file mode 100644 index 0000000..7ef18a8 --- /dev/null +++ b/video/src/components/TerminalWindow.tsx @@ -0,0 +1,278 @@ +import React from "react"; +import { useCurrentFrame, spring, interpolate } from "remotion"; +import { FPS } from "../constants/timing"; +import { TERM, COLORS } from "../constants/colors"; + +export interface TerminalLine { + text: string; + type: "command" | "output" | "success" | "error" | "blank"; + /** Frame (local) at which this line starts appearing */ + startFrame: number; + /** Frames to type (only for 'command' type; output appears instantly) */ + typeDuration?: number; +} + +interface TerminalWindowProps { + lines: TerminalLine[]; + title?: string; + width?: number; + height?: number; + /** Frame at which the terminal window enters */ + entranceFrame?: number; + style?: React.CSSProperties; +} + +export const TerminalWindow: React.FC = ({ + lines, + title = "Terminal", + width = 900, + height, + entranceFrame = 0, + style = {}, +}) => { + const frame = useCurrentFrame(); + + // Spring entrance with gentle overshoot + const entrance = spring({ + frame: Math.max(0, frame - entranceFrame), + fps: FPS, + config: { damping: 16, mass: 0.7, stiffness: 120 }, + }); + + // Subtle ambient glow that breathes + const glowPulse = 0.3 + 0.1 * Math.sin(frame * 0.03); + + return ( +
+ {/* Title bar — realistic macOS style */} +
+ {/* Traffic lights with inner shadows for realism */} + + + + + {/* Title centered */} +
+ {title} +
+
+ + {/* Terminal body */} +
+ {lines.map((line, i) => ( + + ))} + {/* Blinking cursor on its own line */} + +
+
+ ); +}; + +// ── Traffic Light Button ───────────────────────── + +const TrafficLight: React.FC<{ color: string; innerShadow: string }> = ({ + color, + innerShadow, +}) => ( +
+); + +// ── Line Renderer ──────────────────────────────── + +const TerminalLineRenderer: React.FC<{ + line: TerminalLine; + frame: number; + lineIndex: number; +}> = ({ line, frame, lineIndex }) => { + const localFrame = frame - line.startFrame; + if (localFrame < 0) return null; + + if (line.type === "blank") { + return
; + } + + const isCommand = line.type === "command"; + const typeDur = line.typeDuration ?? 30; + + // For commands: progressive character reveal + // For output: fade in quickly + const outputFade = isCommand ? 1 : Math.min(localFrame / 5, 1); + + let displayText: string; + let rawText: string; + + if (isCommand) { + const isShell = line.text.startsWith("$"); + const isPython = line.text.startsWith(">>>"); + rawText = isShell + ? line.text.replace(/^\$\s*/, "") + : isPython + ? line.text.replace(/^>>>\s*/, "") + : line.text; + const charsToShow = Math.min( + Math.floor((localFrame / typeDur) * rawText.length), + rawText.length, + ); + displayText = rawText.slice(0, charsToShow); + } else { + rawText = line.text; + displayText = line.text; + } + + const colorMap: Record = { + command: TERM.command, + output: TERM.output, + success: TERM.success, + error: TERM.error, + blank: "transparent", + }; + + // Error lines get a subtle background + const isError = line.type === "error"; + const isSuccess = line.type === "success"; + + return ( +
+ {isCommand && ( + + {line.text.startsWith(">>>") ? ">>> " : "$ "} + + )} + {displayText} + {/* Inline typing cursor for commands still typing */} + {isCommand && displayText.length < rawText.length && ( + + )} +
+ ); +}; + +// ── Blinking Cursor ────────────────────────────── + +const BlinkingCursor: React.FC<{ frame: number; lines: TerminalLine[] }> = ({ + frame, + lines, +}) => { + // Only show standalone cursor after last line is fully typed + const lastLine = lines[lines.length - 1]; + if (!lastLine) return null; + + const lastLocalFrame = frame - lastLine.startFrame; + const isLastCommand = lastLine.type === "command"; + const typeDur = lastLine.typeDuration ?? 30; + const lastLineComplete = isLastCommand ? lastLocalFrame >= typeDur : lastLocalFrame >= 5; + + if (!lastLineComplete) return null; + + const visible = Math.floor(frame / 15) % 2 === 0; + const lastIsShell = lastLine.text.startsWith("$") || lastLine.type !== "command"; + const prompt = lastIsShell ? "$ " : ">>> "; + + return ( +
+ {prompt} + +
+ ); +}; diff --git a/video/src/components/TextReveal.tsx b/video/src/components/TextReveal.tsx new file mode 100644 index 0000000..42e98ee --- /dev/null +++ b/video/src/components/TextReveal.tsx @@ -0,0 +1,259 @@ +import React from "react"; +import { useCurrentFrame, spring } from "remotion"; +import { FPS } from "../constants/timing"; +import { COLORS } from "../constants/colors"; + +type RevealMode = "fade" | "slideUp" | "typewriter" | "scaleUp" | "wordByWord" | "glowFade" | "letterReveal" | "splitLine"; + +interface TextRevealProps { + text: string; + startFrame: number; + duration?: number; + mode?: RevealMode; + fontSize?: number; + color?: string; + fontFamily?: string; + fontWeight?: number; + style?: React.CSSProperties; + letterSpacing?: string; +} + +export const TextReveal: React.FC = ({ + text, + startFrame, + duration = 60, + mode = "fade", + fontSize = 48, + color = COLORS.white, + fontFamily = "system-ui, -apple-system, 'Segoe UI', sans-serif", + fontWeight = 600, + style = {}, + letterSpacing, +}) => { + const frame = useCurrentFrame(); + const localFrame = frame - startFrame; + + if (localFrame < 0) return null; + + const progress = Math.min(localFrame / duration, 1); + + const baseStyle: React.CSSProperties = { + fontSize, + color, + fontFamily, + fontWeight, + letterSpacing, + whiteSpace: "pre-wrap", + lineHeight: 1.2, + ...style, + }; + + if (mode === "fade") { + // Smooth ease-out opacity + const t = Math.min(progress * 1.5, 1); + const eased = 1 - Math.pow(1 - t, 3); + return ( +
+ {text} +
+ ); + } + + if (mode === "slideUp") { + const s = spring({ + frame: localFrame, + fps: FPS, + config: { damping: 18, mass: 0.7, stiffness: 100 }, + }); + return ( +
+ {text} +
+ ); + } + + if (mode === "typewriter") { + const charsToShow = Math.floor(progress * text.length); + const showCursor = Math.floor(frame / 12) % 2 === 0; + return ( +
+ {text.slice(0, charsToShow)} + {charsToShow < text.length && ( + + )} +
+ ); + } + + if (mode === "scaleUp") { + const s = spring({ + frame: localFrame, + fps: FPS, + config: { damping: 14, mass: 0.5, stiffness: 100 }, + }); + return ( +
+ {text} +
+ ); + } + + if (mode === "glowFade") { + const t = Math.min(progress * 1.5, 1); + const eased = 1 - Math.pow(1 - t, 3); + const glowIntensity = Math.max(0, 1 - progress * 2) * 40; + return ( +
+ {text} +
+ ); + } + + if (mode === "wordByWord") { + const words = text.split(" "); + return ( +
+ {words.map((word, i) => { + const wordDelay = (i / words.length) * duration * 0.7; + const wordLocal = localFrame - wordDelay; + const s = + wordLocal <= 0 + ? 0 + : spring({ + frame: wordLocal, + fps: FPS, + config: { damping: 16, mass: 0.6 }, + }); + return ( + + {word} + + ); + })} +
+ ); + } + + if (mode === "letterReveal") { + const letters = text.split(""); + return ( +
+ {letters.map((letter, i) => { + const letterDelay = (i / letters.length) * duration * 0.5; + const letterLocal = localFrame - letterDelay; + const s = + letterLocal <= 0 + ? 0 + : spring({ + frame: letterLocal, + fps: FPS, + config: { damping: 12, mass: 0.5, stiffness: 120 }, + }); + return ( + + {letter} + + ); + })} +
+ ); + } + + if (mode === "splitLine") { + // Text splits from center — left half slides left, right slides right + const mid = Math.ceil(text.length / 2); + const left = text.slice(0, mid); + const right = text.slice(mid); + const s = spring({ + frame: localFrame, + fps: FPS, + config: { damping: 16, mass: 0.6 }, + }); + return ( +
+ + {left} + + + {right} + +
+ ); + } + + return
{text}
; +}; diff --git a/video/src/constants/colors.ts b/video/src/constants/colors.ts new file mode 100644 index 0000000..2a6aed6 --- /dev/null +++ b/video/src/constants/colors.ts @@ -0,0 +1,35 @@ +// Pure CSS color strings — no THREE.Color +export const COLORS = { + bg: "#020817", + primary: "#06b6d4", // cyan + accent: "#3b82f6", // blue + success: "#22c55e", // green + chaos: "#f59e0b", // amber + chaosRed: "#ef4444", // red + white: "#f8fafc", + muted: "#64748b", + dim: "#1e293b", +} as const; + +// Terminal-specific +export const TERM = { + bg: "#0d1117", + border: "#21262d", + titleBar: "#161b22", + trafficRed: "#ff5f57", + trafficYellow: "#febc2e", + trafficGreen: "#28c840", + text: "#c9d1d9", + prompt: "#58a6ff", + command: "#f0f6fc", + output: "#8b949e", + success: "#3fb950", + error: "#f85149", + cursor: "#58a6ff", +} as const; + +// Gradient helpers +export const GRADIENTS = { + heroRadial: `radial-gradient(ellipse at 50% 50%, ${COLORS.primary}15, ${COLORS.bg} 70%)`, + accentRadial: `radial-gradient(circle at 50% 50%, ${COLORS.accent}20, transparent 60%)`, +} as const; diff --git a/video/src/constants/timing.ts b/video/src/constants/timing.ts new file mode 100644 index 0000000..5b46876 --- /dev/null +++ b/video/src/constants/timing.ts @@ -0,0 +1,66 @@ +// Video specs +export const FPS = 30; +export const WIDTH = 1920; +export const HEIGHT = 1080; +export const TOTAL_FRAMES = 1800; // 60s + +// Act durations (frames) — used by +export const ACT_DURATIONS = { + hero: 270, // 9s + problem: 330, // 11s + solution: 480, // 16s + query: 360, // 12s + closing: 360, // 12s +} as const; + +// Fade in/out (frames at start/end of each act) +export const FADE_FRAMES = 15; + +// ── Act 1: Hero ────────────────────────────────── +export const ACT1 = { + duration: ACT_DURATIONS.hero, // 270 + particleCoalesce: { start: 0, end: 150 }, + titleReveal: { start: 90, end: 180 }, + tagline: { start: 170, end: 260 }, +}; + +// ── Act 2: Problem ─────────────────────────────── +export const ACT2 = { + duration: ACT_DURATIONS.problem, // 330 + terminalType: { start: 20, end: 160 }, + cardsFloat: { start: 0, end: 150 }, + cardsShatter: { start: 160, end: 260 }, + failText: { start: 200, end: 310 }, +}; + +// ── Act 3: Solution (centerpiece) ──────────────── +export const ACT3 = { + duration: ACT_DURATIONS.solution, // 480 + pipInstall: { start: 10, end: 80 }, + importLine: { start: 90, end: 140 }, + createIndex: { start: 150, end: 220 }, + scanning: { start: 230, end: 300 }, + treeGrow: { start: 250, end: 420 }, + indexReady: { start: 380, end: 460 }, +}; + +// ── Act 4: Query ───────────────────────────────── +export const ACT4 = { + duration: ACT_DURATIONS.query, // 360 + queryType: { start: 10, end: 90 }, + traversal: { start: 100, end: 220 }, + highlight: { start: 180, end: 280 }, + response: { start: 240, end: 340 }, +}; + +// ── Act 5: Closing ─────────────────────────────── +export const ACT5 = { + duration: ACT_DURATIONS.closing, // 360 + treeFullGlow: { start: 0, end: 360 }, + providerOrbit: { start: 30, end: 300 }, + title: { start: 20, end: 340 }, + installBadge: { start: 80, end: 300 }, + tagline: { start: 120, end: 310 }, + github: { start: 160, end: 310 }, + fadeOut: { start: 310, end: 360 }, +}; diff --git a/video/src/constants/tree-data.ts b/video/src/constants/tree-data.ts new file mode 100644 index 0000000..0e0bfd6 --- /dev/null +++ b/video/src/constants/tree-data.ts @@ -0,0 +1,150 @@ +export interface TreeNode { + nodeId: string; + structure: string; + title: string; + startIndex: number; + endIndex: number; + children: TreeNode[]; +} + +// Flattened from examples/my_index.json — EM Waves chapter (14 nodes) +export const TREE_ROOT: TreeNode = { + nodeId: "0001", + structure: "1", + title: "Chapter Eight: EM WAVES", + startIndex: 0, + endIndex: 13, + children: [ + { + nodeId: "0002", + structure: "1.1", + title: "INTRODUCTION", + startIndex: 0, + endIndex: 0, + children: [], + }, + { + nodeId: "0003", + structure: "1.2", + title: "DISPLACEMENT CURRENT", + startIndex: 1, + endIndex: 3, + children: [], + }, + { + nodeId: "0004", + structure: "1.3", + title: "ELECTROMAGNETIC WAVES", + startIndex: 4, + endIndex: 6, + children: [ + { + nodeId: "0005", + structure: "1.3.1", + title: "Sources of EM waves", + startIndex: 4, + endIndex: 4, + children: [], + }, + { + nodeId: "0006", + structure: "1.3.2", + title: "Nature of EM waves", + startIndex: 5, + endIndex: 6, + children: [], + }, + ], + }, + { + nodeId: "0007", + structure: "1.4", + title: "EM SPECTRUM", + startIndex: 7, + endIndex: 13, + children: [ + { + nodeId: "0008", + structure: "1.4.1", + title: "Radio waves", + startIndex: 8, + endIndex: 7, + children: [], + }, + { + nodeId: "0009", + structure: "1.4.2", + title: "Microwaves", + startIndex: 8, + endIndex: 8, + children: [], + }, + { + nodeId: "0010", + structure: "1.4.3", + title: "Infrared waves", + startIndex: 9, + endIndex: 8, + children: [], + }, + { + nodeId: "0011", + structure: "1.4.4", + title: "Visible rays", + startIndex: 9, + endIndex: 8, + children: [], + }, + { + nodeId: "0012", + structure: "1.4.5", + title: "Ultraviolet rays", + startIndex: 9, + endIndex: 9, + children: [], + }, + { + nodeId: "0013", + structure: "1.4.6", + title: "X-rays", + startIndex: 10, + endIndex: 9, + children: [], + }, + { + nodeId: "0014", + structure: "1.4.7", + title: "Gamma rays", + startIndex: 10, + endIndex: 13, + children: [], + }, + ], + }, + ], +}; + +// Flat list of all 14 nodes for easy iteration +export function flattenTree(node: TreeNode): TreeNode[] { + return [node, ...node.children.flatMap(flattenTree)]; +} + +export const ALL_NODES = flattenTree(TREE_ROOT); + +// Provider names for the closing orbit +export const PROVIDERS = [ + "OpenAI", + "Anthropic", + "Google", + "Mistral", + "Cohere", + "Groq", + "Ollama", + "DeepSeek", + "HuggingFace", + "Azure", + "AWS Bedrock", + "Together", + "Fireworks", + "Replicate", +]; diff --git a/video/src/hooks/useProgress.ts b/video/src/hooks/useProgress.ts new file mode 100644 index 0000000..9c90778 --- /dev/null +++ b/video/src/hooks/useProgress.ts @@ -0,0 +1,68 @@ +import { useCurrentFrame } from "remotion"; + +/** Returns 0..1 progress for a local frame range */ +export function useProgress(start: number, end: number): number { + const frame = useCurrentFrame(); + if (frame < start) return 0; + if (frame >= end) return 1; + return (frame - start) / (end - start); +} + +/** Returns true when frame is within [start, end) */ +export function useVisible(start: number, end: number): boolean { + const frame = useCurrentFrame(); + return frame >= start && frame < end; +} + +// ── Easing functions ───────────────────────────── + +export function easeOut(t: number): number { + return 1 - Math.pow(1 - t, 3); +} + +export function easeInOut(t: number): number { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +export function easeIn(t: number): number { + return t * t * t; +} + +/** Attempt at a spring-like ease (overshoot then settle) */ +export function springEase(t: number, damping = 0.7): number { + if (t >= 1) return 1; + const decay = Math.exp(-damping * 8 * t); + return 1 - decay * Math.cos(t * Math.PI * 2); +} + +/** Linear interpolation clamped to [0, 1] */ +export function lerp(a: number, b: number, t: number): number { + const ct = Math.max(0, Math.min(1, t)); + return a + (b - a) * ct; +} + +/** Ease-back: overshoots target then settles (Apple-style) */ +export function easeOutBack(t: number, overshoot = 1.70158): number { + const c = overshoot + 1; + return 1 + c * Math.pow(t - 1, 3) + overshoot * Math.pow(t - 1, 2); +} + +/** Exponential decay (fast start, slow settle) */ +export function expDecay(t: number, rate = 4): number { + return 1 - Math.exp(-rate * t); +} + +/** Custom cubic bezier approximation */ +export function cubicBezier(t: number, p1x: number, p1y: number, p2x: number, p2y: number): number { + // Simple approximation using Newton's method + let x = t; + for (let i = 0; i < 8; i++) { + const bx = 3 * p1x * x * (1 - x) * (1 - x) + 3 * p2x * x * x * (1 - x) + x * x * x; + const dx = bx - t; + if (Math.abs(dx) < 0.001) break; + const dbx = 3 * p1x * (1 - x) * (1 - x) - 6 * p1x * x * (1 - x) + 6 * p2x * x * (1 - x) - 3 * p2x * x * x + 3 * x * x; + if (Math.abs(dbx) < 0.0001) break; + x -= dx / dbx; + } + return 3 * p1y * x * (1 - x) * (1 - x) + 3 * p2y * x * x * (1 - x) + x * x * x; +} diff --git a/video/src/hooks/useTreeLayout2D.ts b/video/src/hooks/useTreeLayout2D.ts new file mode 100644 index 0000000..87ecba0 --- /dev/null +++ b/video/src/hooks/useTreeLayout2D.ts @@ -0,0 +1,111 @@ +import { useMemo } from "react"; +import { TreeNode, TREE_ROOT, flattenTree } from "../constants/tree-data"; + +export interface NodeLayout { + nodeId: string; + title: string; + structure: string; + x: number; + y: number; + depth: number; + index: number; // BFS order index +} + +export interface EdgeLayout { + from: string; + to: string; + x1: number; + y1: number; + x2: number; + y2: number; +} + +/** + * BFS layout of tree in 2D SVG coordinates. + * @param width SVG viewport width + * @param height SVG viewport height + * @param padX horizontal padding + * @param padY vertical padding + */ +export function useTreeLayout2D( + width = 700, + height = 500, + padX = 40, + padY = 60, +): { nodes: NodeLayout[]; edges: EdgeLayout[] } { + return useMemo(() => { + const nodes: NodeLayout[] = []; + const nodeMap = new Map(); + + // BFS to assign depth + children grouping + interface QueueItem { + node: TreeNode; + depth: number; + } + + const queue: QueueItem[] = [{ node: TREE_ROOT, depth: 0 }]; + const levels: TreeNode[][] = []; + let bfsIndex = 0; + + while (queue.length > 0) { + const { node, depth } = queue.shift()!; + if (!levels[depth]) levels[depth] = []; + levels[depth].push(node); + + for (const child of node.children) { + queue.push({ node: child, depth: depth + 1 }); + } + } + + const maxDepth = levels.length - 1; + const usableW = width - padX * 2; + const usableH = height - padY * 2; + + // Assign positions per level + for (let d = 0; d <= maxDepth; d++) { + const row = levels[d]; + const y = padY + (maxDepth > 0 ? (d / maxDepth) * usableH : usableH / 2); + + for (let i = 0; i < row.length; i++) { + const x = + padX + + (row.length > 1 ? (i / (row.length - 1)) * usableW : usableW / 2); + + const layout: NodeLayout = { + nodeId: row[i].nodeId, + title: row[i].title, + structure: row[i].structure, + x, + y, + depth: d, + index: bfsIndex++, + }; + nodes.push(layout); + nodeMap.set(row[i].nodeId, layout); + } + } + + // Compute edges + const edges: EdgeLayout[] = []; + const allNodes = flattenTree(TREE_ROOT); + + for (const node of allNodes) { + const parentLayout = nodeMap.get(node.nodeId); + if (!parentLayout) continue; + for (const child of node.children) { + const childLayout = nodeMap.get(child.nodeId); + if (!childLayout) continue; + edges.push({ + from: node.nodeId, + to: child.nodeId, + x1: parentLayout.x, + y1: parentLayout.y, + x2: childLayout.x, + y2: childLayout.y, + }); + } + } + + return { nodes, edges }; + }, [width, height, padX, padY]); +} diff --git a/video/src/index.ts b/video/src/index.ts new file mode 100644 index 0000000..e78af8b --- /dev/null +++ b/video/src/index.ts @@ -0,0 +1,4 @@ +import { registerRoot } from "remotion"; +import { Root } from "./Root"; + +registerRoot(Root); diff --git a/video/src/scenes/Act1Hero.tsx b/video/src/scenes/Act1Hero.tsx new file mode 100644 index 0000000..85f71e0 --- /dev/null +++ b/video/src/scenes/Act1Hero.tsx @@ -0,0 +1,218 @@ +import React from "react"; +import { useCurrentFrame, spring } from "remotion"; +import { ACT1, FPS } from "../constants/timing"; +import { COLORS } from "../constants/colors"; +import { ParticleBackground } from "../components/ParticleBackground"; +import { TextReveal } from "../components/TextReveal"; +import { GlowOrb } from "../components/GlowOrb"; +import { NarratorText } from "../components/FeatureHighlight"; +import { useProgress, easeOut } from "../hooks/useProgress"; + +export const Act1Hero: React.FC = () => { + const frame = useCurrentFrame(); + + const coalesceRaw = useProgress(ACT1.particleCoalesce.start, ACT1.particleCoalesce.end); + const coalescence = easeOut(coalesceRaw); + + const bgScale = 1 + frame * 0.0001; + + const titleS = spring({ + frame: Math.max(0, frame - ACT1.titleReveal.start), + fps: FPS, + config: { damping: 18, mass: 0.7 }, + }); + + return ( +
+ {/* Multi-layer background */} +
+ + {/* Grid */} +
+ + + + + + + {/* Center content */} +
+ + +
+ + + + {/* Animated HR — wider, gradient both sides */} +
+ + {/* Version badge */} +
+
+ v0.1.4 +
+
+ Open Source +
+
+ + + + {/* Subtext — expanded explanation */} +
+ +
+ + {/* Narrator subtitle — hook question for TTS */} +
+ +
+ + {/* Bottom vignette */} +
+
+ ); +}; diff --git a/video/src/scenes/Act2Problem.tsx b/video/src/scenes/Act2Problem.tsx new file mode 100644 index 0000000..b387eea --- /dev/null +++ b/video/src/scenes/Act2Problem.tsx @@ -0,0 +1,242 @@ +import React from "react"; +import { useCurrentFrame, spring } from "remotion"; +import { ACT2, FPS } from "../constants/timing"; +import { COLORS } from "../constants/colors"; +import { TerminalWindow, TerminalLine } from "../components/TerminalWindow"; +import { FloatingCards } from "../components/FloatingCards"; +import { TextReveal } from "../components/TextReveal"; +import { GlowOrb } from "../components/GlowOrb"; +import { StepFlow, Step } from "../components/StepFlow"; +import { NarratorText } from "../components/FeatureHighlight"; +import { IconScissors, IconArrowRight, IconDatabase, IconSearch } from "../components/Icons"; +import { GlitchEffect } from "../components/GlitchEffect"; +import { useProgress, easeIn } from "../hooks/useProgress"; + +const TERMINAL_LINES: TerminalLine[] = [ + { text: "$ python traditional_rag.py", type: "command", startFrame: 20, typeDuration: 30 }, + { text: "Loading document: physics_ch8.pdf", type: "output", startFrame: 55 }, + { text: "Splitting into 256-token chunks...", type: "output", startFrame: 70 }, + { text: "Created 47 chunks", type: "output", startFrame: 85 }, + { text: "Embedding chunks into vector store...", type: "output", startFrame: 95 }, + { text: "", type: "blank", startFrame: 108 }, + { text: '$ python query.py "displacement current sources"', type: "command", startFrame: 112, typeDuration: 38 }, + { text: "Searching 47 chunks by cosine similarity...", type: "output", startFrame: 155 }, + { text: "", type: "blank", startFrame: 165 }, + { text: "Top 3 results:", type: "output", startFrame: 168 }, + { text: " [0.72] chunk_23: ...the magnetic field is not...", type: "output", startFrame: 178 }, + { text: " [0.69] chunk_07: ...Maxwell noticed an incons...", type: "output", startFrame: 188 }, + { text: " [0.65] chunk_41: ...radio waves are produced...", type: "output", startFrame: 198 }, + { text: "", type: "blank", startFrame: 210 }, + { text: "WARNING: Retrieved chunks lack structural context", type: "error", startFrame: 218 }, + { text: "Sections split across chunk boundaries", type: "error", startFrame: 228 }, +]; + +// Traditional RAG pipeline steps +const RAG_STEPS: Step[] = [ + { number: 1, label: "Split Text", icon: }, + { number: 2, label: "Embed", icon: }, + { number: 3, label: "Vector DB", icon: }, + { number: 4, label: "Search", icon: }, +]; + +export const Act2Problem: React.FC = () => { + const frame = useCurrentFrame(); + + const floatProgress = useProgress(ACT2.cardsFloat.start, ACT2.cardsFloat.end); + const shatterRaw = useProgress(ACT2.cardsShatter.start, ACT2.cardsShatter.end); + const shatterProgress = easeIn(shatterRaw); + const dangerGlow = shatterProgress * 0.12; + + // Which RAG step is active based on terminal progress + const ragStep = + frame < 70 ? 1 : frame < 95 ? 2 : frame < 155 ? 3 : 4; + + const labelS = spring({ + frame: Math.max(0, frame - 5), + fps: FPS, + config: { damping: 18, mass: 0.7 }, + }); + + return ( +
+ {/* Background */} +
+
+ + + + {/* Top: "THE PROBLEM" section header */} +
+ +
+ +
+
+ + {/* RAG Pipeline Step Flow — above terminal */} +
+ +
+ + {/* Left: Terminal */} +
+
+ Traditional RAG Pipeline +
+ +
+ + {/* Right: Floating chunk cards */} +
+ +
+ + {/* Narrator text: explains the problem for TTS */} +
+ +
+ + {/* Glitch interference during failure */} + + + {/* Bottom: "Structure Lost." */} +
+ +
+
+ ); +}; diff --git a/video/src/scenes/Act3Solution.tsx b/video/src/scenes/Act3Solution.tsx new file mode 100644 index 0000000..4d35276 --- /dev/null +++ b/video/src/scenes/Act3Solution.tsx @@ -0,0 +1,288 @@ +import React from "react"; +import { useCurrentFrame, interpolate, spring } from "remotion"; +import { ACT3, FPS } from "../constants/timing"; +import { COLORS } from "../constants/colors"; +import { TerminalWindow, TerminalLine } from "../components/TerminalWindow"; +import { AnimatedTree } from "../components/AnimatedTree"; +import { TextReveal } from "../components/TextReveal"; +import { GlowOrb } from "../components/GlowOrb"; +import { StepFlow, Step } from "../components/StepFlow"; +import { NarratorText } from "../components/FeatureHighlight"; +import { IconDocument, IconTree, IconMapPin } from "../components/Icons"; +import { useProgress, easeOut } from "../hooks/useProgress"; + +const TERMINAL_LINES: TerminalLine[] = [ + { text: "$ pip install treedex", type: "command", startFrame: 10, typeDuration: 25 }, + { text: "Collecting treedex", type: "output", startFrame: 40 }, + { text: "Successfully installed treedex-0.1.4", type: "success", startFrame: 55 }, + { text: "", type: "blank", startFrame: 65 }, + { text: "$ python", type: "command", startFrame: 72, typeDuration: 12 }, + { text: "Python 3.11.7 | TreeDex Runtime", type: "output", startFrame: 86 }, + { text: ">>> from treedex import create_index", type: "command", startFrame: 95, typeDuration: 28 }, + { text: "", type: "blank", startFrame: 128 }, + { + text: '>>> index = create_index("physics_ch8.pdf", provider="openai")', + type: "command", + startFrame: 150, + typeDuration: 45, + }, + { text: "", type: "blank", startFrame: 200 }, + { text: "Scanning document structure...", type: "output", startFrame: 230 }, + { text: "Found 4 sections, 9 subsections", type: "output", startFrame: 258 }, + { text: "Building tree: 14 nodes across 3 levels", type: "output", startFrame: 290 }, + { text: "Mapping page ranges to leaf nodes...", type: "output", startFrame: 320 }, + { text: "", type: "blank", startFrame: 355 }, + { text: "Index ready. 14 nodes mapped to tree.", type: "success", startFrame: 380 }, +]; + +// How TreeDex works — 3-step pipeline +const HOW_IT_WORKS: Step[] = [ + { number: 1, label: "Parse Structure", icon: , color: COLORS.accent }, + { number: 2, label: "Build Tree", icon: , color: COLORS.primary }, + { number: 3, label: "Map Pages", icon: , color: COLORS.success }, +]; + +export const Act3Solution: React.FC = () => { + const frame = useCurrentFrame(); + + const treeRevealRaw = useProgress(ACT3.treeGrow.start, ACT3.treeGrow.end); + const treeReveal = easeOut(treeRevealRaw); + + // Active "how it works" step based on terminal progress + const howStep = frame < 230 ? 1 : frame < 290 ? 2 : 3; + + const labelS = spring({ + frame: Math.max(0, frame - 3), + fps: FPS, + config: { damping: 18, mass: 0.7 }, + }); + const treeLabelS = spring({ + frame: Math.max(0, frame - ACT3.treeGrow.start + 10), + fps: FPS, + config: { damping: 16, mass: 0.7 }, + }); + const dividerOpacity = interpolate(frame, [20, 50], [0, 0.15], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return ( +
+ {/* Background */} +
+
+ + {treeReveal > 0.2 && ( + + )} + + {/* Section header */} +
+ +
+ +
+
+ + {/* How It Works step flow — top center */} +
+ +
+ + {/* Vertical divider */} +
+ + {/* Left: Terminal */} +
+
+ TreeDex Pipeline +
+ +
+ + {/* Right: Animated Tree */} +
+
+ Document Tree +
+ + + + {/* Stats row */} + {treeReveal > 0.8 && ( +
+ + + +
+ )} +
+ + {/* Narrator text — how it works explanation for TTS */} +
+ +
+ + {/* Bottom: "Structure Preserved." */} +
+ +
+
+ ); +}; + +const StatBadge: React.FC<{ label: string; value: string; color: string }> = ({ label, value, color }) => ( +
+
{value}
+
{label}
+
+); diff --git a/video/src/scenes/Act4Query.tsx b/video/src/scenes/Act4Query.tsx new file mode 100644 index 0000000..c2a6c18 --- /dev/null +++ b/video/src/scenes/Act4Query.tsx @@ -0,0 +1,291 @@ +import React from "react"; +import { useCurrentFrame, interpolate, spring } from "remotion"; +import { ACT4, FPS } from "../constants/timing"; +import { COLORS } from "../constants/colors"; +import { TerminalWindow, TerminalLine } from "../components/TerminalWindow"; +import { AnimatedTree } from "../components/AnimatedTree"; +import { TextReveal } from "../components/TextReveal"; +import { GlowOrb } from "../components/GlowOrb"; +import { StepFlow, Step } from "../components/StepFlow"; +import { NarratorText } from "../components/FeatureHighlight"; +import { IconQuestion, IconTree, IconCheckCircle, IconSparkles } from "../components/Icons"; +import { useProgress, easeInOut } from "../hooks/useProgress"; + +const TERMINAL_LINES: TerminalLine[] = [ + { + text: '>>> result = index.query("What are displacement current sources?")', + type: "command", + startFrame: 10, + typeDuration: 50, + }, + { text: "", type: "blank", startFrame: 65 }, + { text: "Traversing tree...", type: "output", startFrame: 100 }, + { text: " \u2192 Node 0001: Chapter Eight EM WAVES", type: "output", startFrame: 120 }, + { text: " \u2192 Node 0003: DISPLACEMENT CURRENT \u2713", type: "success", startFrame: 148 }, + { text: " \u2192 Node 0004: ELECTROMAGNETIC WAVES \u2713", type: "success", startFrame: 168 }, + { text: "", type: "blank", startFrame: 190 }, + { text: "Selected 2 nodes (pages 1\u20136)", type: "output", startFrame: 200 }, + { text: "Generating response from selected context...", type: "output", startFrame: 240 }, + { text: "", type: "blank", startFrame: 260 }, + { + text: '"Displacement current arises from time-varying electric fields', + type: "success", + startFrame: 272, + }, + { + text: ' between capacitor plates, as described by Maxwell..."', + type: "success", + startFrame: 285, + }, +]; + +// Query pipeline steps +const QUERY_STEPS: Step[] = [ + { number: 1, label: "Receive Query", icon: , color: COLORS.accent }, + { number: 2, label: "Traverse Tree", icon: , color: COLORS.primary }, + { number: 3, label: "Select Nodes", icon: , color: COLORS.success }, + { number: 4, label: "Generate", icon: , color: COLORS.success }, +]; + +export const Act4Query: React.FC = () => { + const frame = useCurrentFrame(); + + const pulseRaw = useProgress(ACT4.traversal.start, ACT4.traversal.end); + const pulseProgress = easeInOut(pulseRaw); + + const highlightRaw = useProgress(ACT4.highlight.start, ACT4.highlight.end); + const highlightNodes = highlightRaw > 0 ? ["0001", "0003", "0004"] : []; + + const greenGlow = highlightRaw * 0.1; + + // Active query step + const queryStep = + frame < 100 ? 1 : frame < 190 ? 2 : frame < 260 ? 3 : 4; + + const labelS = spring({ + frame: Math.max(0, frame - 3), + fps: FPS, + config: { damping: 18, mass: 0.7 }, + }); + const dividerOpacity = interpolate(frame, [10, 40], [0, 0.15], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return ( +
+ {/* Background */} +
+
+ + {highlightRaw > 0 && ( + + )} + + {/* Section header */} +
+ +
+ +
+
+ + {/* Query step flow — top */} +
+ +
+ + {/* Vertical divider */} +
+ + {/* Left: Terminal */} +
+
+ Tree-Guided Query +
+ +
+ + {/* Right: Tree with highlights & pulse */} +
+
0 ? COLORS.success : COLORS.muted, + fontFamily: "system-ui, -apple-system, sans-serif", + letterSpacing: "0.2em", + textTransform: "uppercase", + fontWeight: 500, + opacity: labelS, + }} + > + {highlightRaw > 0 ? "Nodes Selected" : "Tree Traversal"} +
+ + + {/* Result badges */} + {highlightRaw > 0.5 && ( +
+ + +
+ )} +
+ + {/* Narrator text */} +
+ +
+
+ ); +}; + +const ResultBadge: React.FC<{ nodeId: string; label: string; pages: string }> = ({ nodeId, label, pages }) => ( +
+
+ {nodeId} +
+
+
+ {label} +
+
+
+ {pages} +
+
+); diff --git a/video/src/scenes/Act5Closing.tsx b/video/src/scenes/Act5Closing.tsx new file mode 100644 index 0000000..8774981 --- /dev/null +++ b/video/src/scenes/Act5Closing.tsx @@ -0,0 +1,351 @@ +import React from "react"; +import { useCurrentFrame, spring } from "remotion"; +import { ACT5, FPS } from "../constants/timing"; +import { COLORS } from "../constants/colors"; +import { AnimatedTree } from "../components/AnimatedTree"; +import { TextReveal } from "../components/TextReveal"; +import { GlowOrb } from "../components/GlowOrb"; +import { ParticleBackground } from "../components/ParticleBackground"; +import { FeatureHighlight, Feature, NarratorText } from "../components/FeatureHighlight"; +import { IconGitBranch, IconCpu, IconBolt, IconPackage } from "../components/Icons"; +import { PROVIDERS } from "../constants/tree-data"; + +const FEATURES: Feature[] = [ + { icon: , title: "Tree-Based", subtitle: "Structure-Aware RAG", color: COLORS.primary }, + { icon: , title: "14+ LLMs", subtitle: "Provider Support", color: COLORS.accent }, + { icon: , title: "Zero Vector DB", subtitle: "No Embeddings Needed", color: COLORS.success }, + { icon: , title: "pip install", subtitle: "One-Line Setup", color: COLORS.primary }, +]; + +export const Act5Closing: React.FC = () => { + const frame = useCurrentFrame(); + + return ( +
+ {/* Multi-layer background */} +
+ + + + + + +
+ + {/* Centered tree */} +
+ +
+ + {/* Provider orbit ring */} + + + {/* Content stack */} +
+ {/* Title */} + + + {/* HR */} + + + {/* Subtitle */} +
+ +
+ + {/* Explainer line */} +
+ +
+ + {/* Feature highlights row */} +
+ +
+ + {/* Install badges */} +
+ + +
+ + {/* GitHub URL */} +
+ +
+
+ + {/* Narrator — closing CTA for TTS */} +
+ +
+ + {/* Vignettes */} +
+
+
+ ); +}; + +// ── Sub-components ─────────────────────────────── + +const HorizontalRule: React.FC<{ frame: number; startFrame: number }> = ({ frame, startFrame }) => { + const s = spring({ + frame: Math.max(0, frame - startFrame), + fps: FPS, + config: { damping: 20, mass: 0.6 }, + }); + return ( +
+ ); +}; + +const ProviderOrbit: React.FC<{ frame: number }> = ({ frame }) => { + const centerX = 960; + const centerY = 260; + const radiusX = 520; + const radiusY = 60; + const rotationSpeed = 0.007; + + return ( + <> + + + + + {PROVIDERS.map((name, i) => { + const angle = (i / PROVIDERS.length) * Math.PI * 2 + frame * rotationSpeed; + const x = centerX + Math.cos(angle) * radiusX; + const y = centerY + Math.sin(angle) * radiusY; + const depth = (Math.sin(angle) + 1) / 2; + const opacity = 0.15 + depth * 0.65; + const scale = 0.65 + depth * 0.35; + + const entrance = spring({ + frame: Math.max(0, frame - 30 - i * 4), + fps: FPS, + config: { damping: 16 }, + }); + + return ( +
0.7 ? `0 0 12px ${COLORS.primary}30` : undefined, + }} + > + {name} +
+ ); + })} + + ); +}; + +const InstallBadge: React.FC<{ + text: string; + startFrame: number; + frame: number; + color: string; + icon: string; +}> = ({ text, startFrame, frame, color, icon }) => { + const s = spring({ + frame: Math.max(0, frame - startFrame), + fps: FPS, + config: { damping: 16, mass: 0.6 }, + }); + return ( +
+ {icon} + {text} +
+ ); +}; diff --git a/video/tsconfig.json b/video/tsconfig.json new file mode 100644 index 0000000..c4300f9 --- /dev/null +++ b/video/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src"] +}