from __future__ import annotations import asyncio import functools from pathlib import Path import time import shutil from aiohttp import web from .media import scan_media, ffprobe, media_roots, classify, make_video_thumbnail, make_audio_waveform, extract_video_frame, extract_image_snapshot, export_clip_for_comfy, send_clip_to_comfy_http from .paths import ensure_dirs, is_contained, web_root, safe_name, itda_root, input_dir, project_file from .project import load_project, save_project, default_project from .export import export_timeline, prerender_range, ExportError from .stitch import analyze_stitch, render_bridge from .analyze import detect_scenes, detect_beats ITDA_VERSION = "1.0.0" _REGISTERED = False async def _offload(fn, *args, **kwargs): """Run a blocking ffmpeg/analysis call off the event loop. These handlers shell out to ffmpeg for anything from a few seconds (waveform) to half an hour (a long export). Called directly from an async handler that stalls the whole aiohttp loop for the duration - which is ComfyUI's loop, not just ITDA's, so an export would freeze the entire UI, queue included. """ loop = asyncio.get_running_loop() return await loop.run_in_executor(None, functools.partial(fn, *args, **kwargs)) def _is_allowed_media_path(path: Path, project: str = "project") -> bool: return is_contained(path, media_roots(project)) def fonts_root() -> Path: root = Path(__file__).resolve().parents[1] / "Fonts" root.mkdir(parents=True, exist_ok=True) return root _FONT_EXTS = {".ttf", ".otf", ".woff", ".woff2"} def _font_format(path: Path) -> str: ext = path.suffix.lower() if ext == ".otf": return "opentype" if ext == ".woff": return "woff" if ext == ".woff2": return "woff2" return "truetype" def project_file_exists(name: str) -> bool: from .paths import project_file return project_file(name).exists() def register_itda_routes(routes=None) -> None: """Registers every /itda/* route onto `routes`. `routes` defaults to ComfyUI's shared PromptServer route table (the custom-node entrypoint); a standalone entrypoint can instead pass its own aiohttp.web.RouteTableDef() so this same ~450-line route body serves both without duplication - the ComfyUI coupling was always just this one lookup, not anything below it. """ global _REGISTERED if _REGISTERED: return if routes is None: try: from server import PromptServer except Exception as e: print(f"[ITDA] PromptServer unavailable: {e}") return routes = PromptServer.instance.routes # ITDA's HTML/JS/CSS are under active development and served with no # explicit cache headers by default, so browsers were free to reuse old # cached copies indefinitely (heuristic caching) - most visibly inside # the ComfyUI in-graph preview node's