declare namespace SIMPLEFFMPEG {
  // ─────────────────────────────────────────────────────────────────────────────
  // Error Classes
  // ─────────────────────────────────────────────────────────────────────────────

  /** Base error class for all simple-ffmpeg errors */
  class SimpleffmpegError extends Error {}

  /** Thrown when clip validation fails */
  class ValidationError extends SimpleffmpegError {
    name: "ValidationError";
    errors: string[];
    warnings: string[];
  }

  /** Thrown when FFmpeg command execution fails */
  class FFmpegError extends SimpleffmpegError {
    name: "FFmpegError";
    stderr: string;
    command: string;
    exitCode: number | null;
    /** Structured error details for bug reporting */
    readonly details: {
      stderrTail: string;
      command: string;
      exitCode: number | null;
    };
  }

  /** Thrown when a media file cannot be found or accessed */
  class MediaNotFoundError extends SimpleffmpegError {
    name: "MediaNotFoundError";
    path: string;
  }

  /** Thrown when export is cancelled via AbortSignal */
  class ExportCancelledError extends SimpleffmpegError {
    name: "ExportCancelledError";
  }

  /** Discriminator for SIMPLEFFMPEG.transcode() failure modes */
  type TranscodeErrorCode =
    | "INVALID_PATH"
    | "INPUT_MISSING"
    | "FFMPEG_NOT_FOUND"
    | "TIMEOUT"
    | "NONZERO_EXIT"
    | "SIGNAL"
    | "ABORTED";

  /** Thrown when SIMPLEFFMPEG.transcode() fails */
  class TranscodeError extends SimpleffmpegError {
    name: "TranscodeError";
    code: TranscodeErrorCode;
    /** Tail of ffmpeg stderr, capped at 16 KB */
    stderr: string;
    exitCode: number | null;
    signal: string | null;
    /** Structured error details for bug reporting */
    readonly details: {
      code: TranscodeErrorCode;
      stderr: string;
      exitCode: number | null;
      signal: string | null;
    };
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Clip Types
  // ─────────────────────────────────────────────────────────────────────────────

  type ClipType =
    | "video"
    | "audio"
    | "text"
    | "music"
    | "backgroundAudio"
    | "image"
    | "subtitle"
    | "color"
    | "effect";

  interface BaseClip {
    type: ClipType;
    url?: string;
    /** Start time on timeline in seconds. For video/image/audio: omit to auto-sequence after the previous clip. */
    position?: number;
    /** End time on timeline in seconds. Mutually exclusive with duration. */
    end?: number;
    /** Duration in seconds (alternative to end). Computes end = position + duration. Mutually exclusive with end. */
    duration?: number;
  }

  interface VideoClip extends BaseClip {
    type: "video";
    url: string;
    cutFrom?: number;
    volume?: number;
    transition?: { type: string; duration: number };
  }

  interface AudioClip extends BaseClip {
    type: "audio";
    url: string;
    cutFrom?: number;
    volume?: number;
  }

  interface BackgroundMusicClip extends BaseClip {
    type: "music" | "backgroundAudio";
    url: string;
    cutFrom?: number;
    volume?: number;
    /** Loop the audio to fill the entire video duration */
    loop?: boolean;
  }

  type KenBurnsEffect =
    | "zoom-in"
    | "zoom-out"
    | "pan-left"
    | "pan-right"
    | "pan-up"
    | "pan-down"
    | "smart"
    | "custom";

  type KenBurnsAnchor = "top" | "bottom" | "left" | "right";
  type KenBurnsEasing = "linear" | "ease-in" | "ease-out" | "ease-in-out";

  interface KenBurnsSpec {
    type?: KenBurnsEffect;
    startZoom?: number;
    endZoom?: number;
    startX?: number;
    startY?: number;
    endX?: number;
    endY?: number;
    anchor?: KenBurnsAnchor;
    easing?: KenBurnsEasing;
  }

  /** How an image is fitted into the output frame when aspect ratios differ. */
  type ImageFit = "cover" | "contain" | "blur-fill";

  interface ImageClip extends BaseClip {
    type: "image";
    url: string;
    width?: number;
    height?: number;
    kenBurns?: KenBurnsEffect | KenBurnsSpec;
    /** How the image is fitted when its aspect ratio differs from the output.
     *  - "blur-fill": scale to fit, fill bars with a blurred version of the image (default without Ken Burns)
     *  - "cover": scale to fill the frame, center-crop excess (default with Ken Burns)
     *  - "contain": scale to fit, pad with black bars
     */
    imageFit?: ImageFit;
    /** Blur strength for the blur-fill background (Gaussian sigma). Higher = blurrier.
     *  Only applies when imageFit is "blur-fill". Default: 40. Typical range: 10-80. */
    blurIntensity?: number;
  }

  type TextMode = "static" | "word-replace" | "word-sequential" | "karaoke";
  type TextAnimationType =
    | "none"
    | "fade-in"
    | "fade-out"
    | "fade-in-out"
    | "fade"
    | "pop"
    | "pop-bounce"
    | "scale-in"
    | "pulse"
    | "typewriter";

  interface TextWordWindow {
    text: string;
    start: number;
    end: number;
    /** Add line break after this word (for multi-line karaoke) */
    lineBreak?: boolean;
  }

  interface TextClip {
    type: "text";
    text?: string;
    /** Start time on timeline in seconds. Required unless fullDuration is true. */
    position?: number;
    /** End time on timeline in seconds. Mutually exclusive with fullDuration. */
    end?: number;
    /** When true, the clip spans the full visual timeline (position 0 to end of last video/image/color clip). Mutually exclusive with end and duration. */
    fullDuration?: boolean;
    mode?: TextMode;
    words?: TextWordWindow[];
    wordTimestamps?: number[];

    // Font
    fontFile?: string;
    fontFamily?: string; // defaults to 'Sans' via fontconfig
    fontSize?: number; // default 48
    fontColor?: string; // default '#FFFFFF'

    // Position (xPercent/yPercent are percentages 0-1, x/y are pixels)
    /** Horizontal position as percentage (0 = left, 0.5 = center, 1 = right) */
    xPercent?: number;
    /** Vertical position as percentage (0 = top, 0.5 = center, 1 = bottom) */
    yPercent?: number;
    /** Absolute X position in pixels */
    x?: number;
    /** Absolute Y position in pixels */
    y?: number;
    /** Pixel offset added to X position (works with x, xPercent, or center default) */
    xOffset?: number;
    /** Pixel offset added to Y position (works with y, yPercent, or center default) */
    yOffset?: number;

    // Styling
    borderColor?: string;
    borderWidth?: number;
    shadowColor?: string;
    shadowX?: number;
    shadowY?: number;
    backgroundColor?: string;
    backgroundOpacity?: number;
    padding?: number;

    // Animation
    animation?: {
      type: TextAnimationType;
      /** Entry animation duration in seconds (default: 0.25) */
      in?: number;
      /** Exit animation duration in seconds (default: same as in) */
      out?: number;
      /** Animation intensity 0-1 for scale-in and pulse (default: 0.3) */
      intensity?: number;
      /** Speed for typewriter (sec/char, default: 0.05) or pulse (cycles/sec, default: 1) */
      speed?: number;
    };

    /** Highlight color for karaoke mode (default: '#FFFF00') */
    highlightColor?: string;

    /** Highlight style for karaoke mode: 'smooth' (gradual fill) or 'instant' (default: 'smooth') */
    highlightStyle?: "smooth" | "instant";
  }

  /** Subtitle clip for importing external subtitle files */
  interface SubtitleClip {
    type: "subtitle";
    /** Path to subtitle file (.srt, .ass, .ssa, .vtt) */
    url: string;
    /** Timeline position offset (default: 0) - adds to all subtitle timestamps */
    position?: number;
    /** Optional end time to cut off subtitles */
    end?: number;

    // Styling (for SRT/VTT import - ASS files use their own styles)
    fontFamily?: string;
    fontSize?: number;
    fontColor?: string;
    borderColor?: string;
    borderWidth?: number;
    opacity?: number;
  }

  /** Gradient specification for color clips */
  interface GradientSpec {
    type: "linear-gradient" | "radial-gradient";
    /** Array of color strings (at least 2). Evenly distributed across the gradient. */
    colors: string[];
    /** For linear gradients: "vertical" (default), "horizontal", or angle in degrees */
    direction?: "vertical" | "horizontal" | number;
  }

  /** Color clip — solid color or gradient for filling gaps, transitions, etc. */
  interface ColorClip {
    type: "color";
    /** Flat color string (e.g. "black", "#FF0000") or gradient specification */
    color: string | GradientSpec;
    /** Start time on timeline in seconds. Omit to auto-sequence after previous visual clip. */
    position?: number;
    /** End time on timeline in seconds. Mutually exclusive with duration. */
    end?: number;
    /** Duration in seconds (alternative to end). end = position + duration. */
    duration?: number;
    /** Transition effect from the previous visual clip */
    transition?: { type: string; duration: number };
  }

  type EffectName =
    | "vignette"
    | "filmGrain"
    | "gaussianBlur"
    | "colorAdjust"
    | "sepia"
    | "blackAndWhite"
    | "sharpen"
    | "chromaticAberration"
    | "letterbox";

  interface EffectParamsBase {
    /** Base blend amount from 0 to 1 (default: 1) */
    amount?: number;
  }

  interface VignetteEffectParams extends EffectParamsBase {
    /** Vignette angle in radians (default: PI/5) */
    angle?: number;
  }

  interface FilmGrainEffectParams extends EffectParamsBase {
    /** Noise intensity 0-1 (default: 0.35). Independent from blend amount. */
    strength?: number;
    /** Temporal grain changes every frame (default: true) */
    temporal?: boolean;
  }

  interface GaussianBlurEffectParams extends EffectParamsBase {
    /** Gaussian blur sigma (default derived from amount) */
    sigma?: number;
  }

  interface ColorAdjustEffectParams extends EffectParamsBase {
    brightness?: number;
    contrast?: number;
    saturation?: number;
    gamma?: number;
  }

  interface SepiaEffectParams extends EffectParamsBase {}

  interface BlackAndWhiteEffectParams extends EffectParamsBase {
    /** Optional contrast boost (default: 1, range 0-3) */
    contrast?: number;
  }

  interface SharpenEffectParams extends EffectParamsBase {
    /** Unsharp amount (default: 1.0, range 0-3) */
    strength?: number;
  }

  interface ChromaticAberrationEffectParams extends EffectParamsBase {
    /** Horizontal pixel offset for R/B channels (default: 4, range 0-20) */
    shift?: number;
  }

  interface LetterboxEffectParams extends EffectParamsBase {
    /** Bar height as fraction of frame height (default: 0.12, range 0-0.5) */
    size?: number;
    /** Bar color (default: "black") */
    color?: string;
  }

  type EffectParams =
    | VignetteEffectParams
    | FilmGrainEffectParams
    | GaussianBlurEffectParams
    | ColorAdjustEffectParams
    | SepiaEffectParams
    | BlackAndWhiteEffectParams
    | SharpenEffectParams
    | ChromaticAberrationEffectParams
    | LetterboxEffectParams;

  /** Effect clip — timed overlay adjustment layer over composed video */
  interface EffectClip {
    type: "effect";
    effect: EffectName;
    /** Start time on timeline in seconds. Required unless fullDuration is true. */
    position?: number;
    /** End time on timeline in seconds. Mutually exclusive with duration and fullDuration. */
    end?: number;
    /** Duration in seconds (alternative to end). end = position + duration. Mutually exclusive with fullDuration. */
    duration?: number;
    /** When true, the clip spans the full visual timeline (position 0 to end of last video/image/color clip). Mutually exclusive with end and duration. */
    fullDuration?: boolean;
    /** Ramp-in duration in seconds */
    fadeIn?: number;
    /** Ramp-out duration in seconds */
    fadeOut?: number;
    /** Effect-specific params */
    params: EffectParams;
  }

  type Clip =
    | VideoClip
    | AudioClip
    | BackgroundMusicClip
    | ImageClip
    | ColorClip
    | EffectClip
    | TextClip
    | SubtitleClip;

  // ─────────────────────────────────────────────────────────────────────────────
  // Options
  // ─────────────────────────────────────────────────────────────────────────────

  /** Platform preset names */
  type PlatformPreset =
    | "tiktok"
    | "youtube-short"
    | "instagram-reel"
    | "instagram-story"
    | "snapchat"
    | "instagram-post"
    | "instagram-square"
    | "youtube"
    | "twitter"
    | "facebook"
    | "landscape"
    | "twitter-portrait"
    | "instagram-portrait";

  /** Platform preset configuration */
  interface PresetConfig {
    width: number;
    height: number;
    fps: number;
  }

  /** Validation error/warning codes */
  const ValidationCodes: {
    readonly INVALID_TYPE: "INVALID_TYPE";
    readonly MISSING_REQUIRED: "MISSING_REQUIRED";
    readonly INVALID_VALUE: "INVALID_VALUE";
    readonly INVALID_RANGE: "INVALID_RANGE";
    readonly INVALID_TIMELINE: "INVALID_TIMELINE";
    readonly TIMELINE_GAP: "TIMELINE_GAP";
    readonly FILE_NOT_FOUND: "FILE_NOT_FOUND";
    readonly INVALID_FORMAT: "INVALID_FORMAT";
    readonly INVALID_WORD_TIMING: "INVALID_WORD_TIMING";
    readonly OUTSIDE_BOUNDS: "OUTSIDE_BOUNDS";
  };

  type ValidationCode = (typeof ValidationCodes)[keyof typeof ValidationCodes];

  /** A single validation error or warning */
  interface ValidationIssue {
    /** Error code for programmatic handling */
    code: ValidationCode;
    /** Path to the problematic field (e.g., "clips[0].url") */
    path: string;
    /** Human-readable error message */
    message: string;
    /** The actual value that caused the issue (optional) */
    received?: unknown;
  }

  /** Result from validate() */
  interface ValidationResult {
    /** Whether the configuration is valid (no errors) */
    valid: boolean;
    /** Array of validation errors (issues that will cause failures) */
    errors: ValidationIssue[];
    /** Array of validation warnings (potential issues that won't block) */
    warnings: ValidationIssue[];
  }

  /** Options for validate() */
  interface ValidateOptions {
    /** Skip file existence checks (useful for AI generating configs before files exist) */
    skipFileChecks?: boolean;
    /** Skip media URL extension/type checks for video/image clips */
    skipExtensionsCheck?: boolean;
    /** Project width - used to validate Ken Burns images are large enough */
    width?: number;
    /** Project height - used to validate Ken Burns images are large enough */
    height?: number;
    /** If true, undersized Ken Burns images will error instead of warn (default: false, images are auto-upscaled) */
    strictKenBurns?: boolean;
  }

  interface SIMPLEFFMPEGOptions {
    /** Platform preset (e.g., 'tiktok', 'youtube', 'instagram-reel'). Sets width, height, fps. */
    preset?: PlatformPreset;
    /** Frames per second (default: 30, or from preset) */
    fps?: number;
    /** Output width in pixels (default: 1920, or from preset) */
    width?: number;
    /** Output height in pixels (default: 1080, or from preset) */
    height?: number;
    /** Validation mode: 'warn' logs warnings, 'strict' throws on warnings (default: 'warn') */
    validationMode?: "warn" | "strict";
    /** Skip file existence checks during load() validation */
    skipFileChecks?: boolean;
    /** Skip media URL extension/type checks for video/image clips during load() validation */
    skipExtensionsCheck?: boolean;
    /** Default font file path (.ttf, .otf) applied to all text clips. Individual clips can override this with their own fontFile. */
    fontFile?: string;
    /** Path to a .ttf/.otf emoji font for rendering emoji in text overlays (opt-in). Without this, emoji are silently stripped from text. Recommended: Noto Emoji (B&W outline). */
    emojiFont?: string;
    /** Custom directory for temporary files — gradient images, unrotated videos, intermediate renders, text/ASS temp files. Defaults to os.tmpdir() or the output directory depending on the operation. Useful for fast SSDs, ramdisks, or environments with constrained /tmp. */
    tempDir?: string;
  }

  /** Log entry passed to onLog callback */
  interface LogEntry {
    level: "stderr" | "stdout";
    message: string;
  }

  /** Progress information passed to onProgress callback */
  interface ProgressInfo {
    /** Current frame number being processed */
    frame?: number;
    /** Current processing speed in frames per second */
    fps?: number;
    /** Time processed in seconds */
    timeProcessed?: number;
    /** Progress percentage (0-100) */
    percent?: number;
    /** Processing speed multiplier (e.g., 2.0 = 2x realtime) */
    speed?: number;
    /** Current bitrate in kbits/s */
    bitrate?: number;
    /** Current output size in bytes */
    size?: number;
    /** Export phase: "rendering" during main export, "batching" during text overlay passes */
    phase?: "rendering" | "batching";
  }

  /** Metadata to embed in output file */
  interface MetadataOptions {
    title?: string;
    artist?: string;
    album?: string;
    comment?: string;
    date?: string;
    genre?: string;
    /** Custom metadata key-value pairs */
    custom?: Record<string, string>;
  }

  /** Thumbnail generation options */
  interface ThumbnailOptions {
    /** Output path for thumbnail image */
    outputPath: string;
    /** Time in seconds to capture (default: 0) */
    time?: number;
    /** Thumbnail width (maintains aspect if height omitted) */
    width?: number;
    /** Thumbnail height (maintains aspect if width omitted) */
    height?: number;
  }

  /** Keyframe extraction mode */
  type KeyframeMode = "scene-change" | "interval";

  /** Output format for extracted keyframes */
  type KeyframeFormat = "jpeg" | "png";

  /** Base options for SIMPLEFFMPEG.extractKeyframes() */
  interface ExtractKeyframesBaseOptions {
    /** Extraction mode: 'scene-change' detects visual transitions, 'interval' samples at fixed spacing (default: 'scene-change') */
    mode?: KeyframeMode;
    /** Scene detection sensitivity 0-1, lower = more frames (default: 0.3). Only for scene-change mode. */
    sceneThreshold?: number;
    /** Seconds between frames (default: 5). Only for interval mode. */
    intervalSeconds?: number;
    /** Maximum number of frames to extract */
    maxFrames?: number;
    /** Output image format (default: 'jpeg') */
    format?: KeyframeFormat;
    /** JPEG quality 1-31, lower is better (default: 2). Only applies to JPEG. */
    quality?: number;
    /** Output width in pixels (maintains aspect ratio if height omitted) */
    width?: number;
    /** Output height in pixels (maintains aspect ratio if width omitted) */
    height?: number;
    /** Custom directory for temporary files (default: os.tmpdir()). Only used when outputDir is not set. Useful for fast SSDs, ramdisks, or environments with constrained /tmp. */
    tempDir?: string;
  }

  /** Options with outputDir — writes to disk, returns string[] */
  interface ExtractKeyframesToDiskOptions extends ExtractKeyframesBaseOptions {
    /** Directory to write frame files to. Each call creates a unique `simpleffmpeg-keyframes-XXXXXX` subdirectory inside it, so repeat and concurrent calls are isolated. Always use the returned paths rather than assuming frames live directly at `${outputDir}/frame-0001.jpg`. */
    outputDir: string;
  }

  /** Options without outputDir — returns Buffer[] */
  interface ExtractKeyframesToBufferOptions extends ExtractKeyframesBaseOptions {
    outputDir?: undefined;
  }

  /** Combined options type for extractKeyframes */
  type ExtractKeyframesOptions =
    | ExtractKeyframesToDiskOptions
    | ExtractKeyframesToBufferOptions;

  /** Options for SIMPLEFFMPEG.snapshot() — capture a single frame from a video */
  interface SnapshotOptions {
    /** Output image path (extension determines format: .jpg, .png, .webp, .bmp, .tiff) */
    outputPath: string;
    /** Time in seconds to capture the frame at (default: 0) */
    time?: number;
    /** Output width in pixels (maintains aspect ratio if height omitted) */
    width?: number;
    /** Output height in pixels (maintains aspect ratio if width omitted) */
    height?: number;
    /** JPEG quality 1-31, lower is better (default: 2, only applies to JPEG output) */
    quality?: number;
  }

  /** Hardware acceleration options */
  type HardwareAcceleration =
    | "auto"
    | "videotoolbox"
    | "nvenc"
    | "vaapi"
    | "qsv"
    | "none";

  /** Video codec options */
  type VideoCodec =
    | "libx264"
    | "libx265"
    | "libvpx-vp9"
    | "libaom-av1"
    | "prores_ks"
    | "h264_videotoolbox"
    | "hevc_videotoolbox"
    | "h264_nvenc"
    | "hevc_nvenc"
    | "h264_vaapi"
    | "hevc_vaapi"
    | "h264_qsv"
    | "hevc_qsv"
    | string;

  /** Audio codec options */
  type AudioCodec =
    | "aac"
    | "libmp3lame"
    | "libopus"
    | "pcm_s16le"
    | "flac"
    | "copy"
    | string;

  /** Encoding preset options */
  type EncodingPreset =
    | "ultrafast"
    | "superfast"
    | "veryfast"
    | "faster"
    | "fast"
    | "medium"
    | "slow"
    | "slower"
    | "veryslow";

  /** Resolution presets */
  type ResolutionPreset = "480p" | "720p" | "1080p" | "1440p" | "4k";

  // ─────────────────────────────────────────────────────────────────────────────
  // Watermark Types
  // ─────────────────────────────────────────────────────────────────────────────

  /** Preset position for watermarks */
  type WatermarkPositionPreset =
    | "top-left"
    | "top-right"
    | "bottom-left"
    | "bottom-right"
    | "center";

  /** Custom position using percentages (0-1) */
  interface WatermarkPositionPercent {
    /** Horizontal position as percentage (0 = left, 0.5 = center, 1 = right) */
    xPercent: number;
    /** Vertical position as percentage (0 = top, 0.5 = center, 1 = bottom) */
    yPercent: number;
  }

  /** Custom position using pixels */
  interface WatermarkPositionPixel {
    /** X position in pixels from left */
    x: number;
    /** Y position in pixels from top */
    y: number;
  }

  /** Watermark position options */
  type WatermarkPosition =
    | WatermarkPositionPreset
    | WatermarkPositionPercent
    | WatermarkPositionPixel;

  /** Base watermark options shared by image and text watermarks */
  interface BaseWatermarkOptions {
    /** Position preset or custom coordinates (default: 'bottom-right') */
    position?: WatermarkPosition;
    /** Margin from edge in pixels when using preset positions (default: 20) */
    margin?: number;
    /** Opacity from 0 (transparent) to 1 (opaque) (default: 1) */
    opacity?: number;
    /** Start time in seconds (default: 0, start of video) */
    startTime?: number;
    /** End time in seconds (default: end of video) */
    endTime?: number;
  }

  /** Image watermark options */
  interface ImageWatermarkOptions extends BaseWatermarkOptions {
    type: "image";
    /** Path to the watermark image file */
    url: string;
    /** Scale relative to video width, 0-1 (default: 0.15, i.e., 15% of width) */
    scale?: number;
  }

  /** Text watermark options */
  interface TextWatermarkOptions extends BaseWatermarkOptions {
    type: "text";
    /** Text to display as watermark */
    text: string;
    /** Font size in pixels (default: 24) */
    fontSize?: number;
    /** Font color in hex format (default: '#FFFFFF') */
    fontColor?: string;
    /** Font family name (default: 'Sans') */
    fontFamily?: string;
    /** Path to custom font file */
    fontFile?: string;
    /** Border/outline color */
    borderColor?: string;
    /** Border/outline width in pixels */
    borderWidth?: number;
    /** Shadow color */
    shadowColor?: string;
    /** Shadow X offset */
    shadowX?: number;
    /** Shadow Y offset */
    shadowY?: number;
  }

  /** Watermark configuration - either image or text */
  type WatermarkOptions = ImageWatermarkOptions | TextWatermarkOptions;

  interface ExportOptions {
    // ─────────────────────────────────────────────────────────────────────────
    // Output
    // ─────────────────────────────────────────────────────────────────────────

    /** Output file path (default: './output.mp4') */
    outputPath?: string;

    // ─────────────────────────────────────────────────────────────────────────
    // Video Encoding
    // ─────────────────────────────────────────────────────────────────────────

    /** Video codec (default: 'libx264') */
    videoCodec?: VideoCodec;
    /** Quality level 0-51, lower is better (default: 23) */
    crf?: number;
    /** Encoding speed/quality tradeoff (default: 'medium') */
    preset?: EncodingPreset;
    /** Target video bitrate (e.g., '5M', '2500k'). Overrides CRF when set. */
    videoBitrate?: string;

    // ─────────────────────────────────────────────────────────────────────────
    // Audio Encoding
    // ─────────────────────────────────────────────────────────────────────────

    /** Audio codec (default: 'aac') */
    audioCodec?: AudioCodec;
    /** Audio bitrate (default: '192k') */
    audioBitrate?: string;
    /** Audio sample rate in Hz (default: 48000) */
    audioSampleRate?: number;

    // ─────────────────────────────────────────────────────────────────────────
    // Hardware Acceleration
    // ─────────────────────────────────────────────────────────────────────────

    /** Hardware acceleration mode (default: 'none') */
    hwaccel?: HardwareAcceleration;

    // ─────────────────────────────────────────────────────────────────────────
    // Output Resolution
    // ─────────────────────────────────────────────────────────────────────────

    /** Output width in pixels (scales the output) */
    outputWidth?: number;
    /** Output height in pixels (scales the output) */
    outputHeight?: number;
    /** Resolution preset ('720p', '1080p', '4k', etc.) */
    outputResolution?: ResolutionPreset;

    // ─────────────────────────────────────────────────────────────────────────
    // Advanced Options
    // ─────────────────────────────────────────────────────────────────────────

    /** Export audio only (no video) */
    audioOnly?: boolean;
    /** Enable two-pass encoding for better quality at target bitrate */
    twoPass?: boolean;
    /** Metadata to embed in output file */
    metadata?: MetadataOptions;
    /** Generate a thumbnail from the output */
    thumbnail?: ThumbnailOptions;

    // ─────────────────────────────────────────────────────────────────────────
    // Debug & Logging
    // ─────────────────────────────────────────────────────────────────────────

    /** Enable verbose logging */
    verbose?: boolean;
    /** FFmpeg log level (default: 'warning') */
    logLevel?:
      | "quiet"
      | "panic"
      | "fatal"
      | "error"
      | "warning"
      | "info"
      | "verbose"
      | "debug";
    /** Save FFmpeg command to file for debugging */
    saveCommand?: string;

    // ─────────────────────────────────────────────────────────────────────────
    // Callbacks & Control
    // ─────────────────────────────────────────────────────────────────────────

    /** Progress callback for monitoring export progress */
    onProgress?: (progress: ProgressInfo) => void;
    /** FFmpeg log callback for real-time stderr/stdout output */
    onLog?: (entry: LogEntry) => void;
    /** AbortSignal for cancelling the export */
    signal?: AbortSignal;

    // ─────────────────────────────────────────────────────────────────────────
    // Text Batching (Advanced)
    // ─────────────────────────────────────────────────────────────────────────

    /** Maximum text overlay nodes per FFmpeg pass (default: 75) */
    textMaxNodesPerPass?: number;
    /** Video codec for intermediate text passes (default: 'libx264') */
    intermediateVideoCodec?: string;
    /** CRF for intermediate text passes (default: 18) */
    intermediateCrf?: number;
    /** Preset for intermediate text passes (default: 'veryfast') */
    intermediatePreset?: string;

    // ─────────────────────────────────────────────────────────────────────────
    // Watermark
    // ─────────────────────────────────────────────────────────────────────────

    /** Add a watermark overlay (image or text) to the video */
    watermark?: WatermarkOptions;

    // ─────────────────────────────────────────────────────────────────────────
    // Timeline
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Automatically adjust text/subtitle timings to compensate for timeline
     * compression caused by xfade transitions (default: true).
     * When enabled, text positioned at "15s" will appear at the visual 15s mark
     * even if transitions have compressed the actual timeline.
     */
    compensateTransitions?: boolean;
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Schema Types
  // ─────────────────────────────────────────────────────────────────────────────

  /** Available schema module IDs */
  type SchemaModuleId =
    | "video"
    | "audio"
    | "image"
    | "color"
    | "effect"
    | "text"
    | "subtitle"
    | "music";

  /** Options for getSchema() */
  interface SchemaOptions {
    /** Only include these module IDs in the schema output */
    include?: SchemaModuleId[];
    /** Exclude these module IDs from the schema output */
    exclude?: SchemaModuleId[];
    /** Custom top-level instructions to embed at the top of the schema */
    instructions?: string | string[];
    /** Per-module custom instructions, keyed by module ID */
    moduleInstructions?: Partial<Record<SchemaModuleId, string | string[]>>;
  }

  /** Result from preview() method */
  interface PreviewResult {
    /** The full FFmpeg command that would be executed */
    command: string;
    /** The filter_complex string */
    filterComplex: string;
    /** Total expected duration in seconds */
    totalDuration: number;
  }

  /** Options for load() */
  interface LoadOptions {
    /** Override file existence checks for media URLs */
    skipFileChecks?: boolean;
    /** Override extension/type validation for media URLs (video/image) */
    skipExtensionsCheck?: boolean;
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Media Info (probe)
  // ─────────────────────────────────────────────────────────────────────────────

  /** Result from SIMPLEFFMPEG.probe() — comprehensive media file metadata */
  interface MediaInfo {
    /** Total duration in seconds */
    duration: number | null;
    /** Video width in pixels (null for audio-only files) */
    width: number | null;
    /** Video height in pixels (null for audio-only files) */
    height: number | null;
    /** Whether the file contains a video stream */
    hasVideo: boolean;
    /** Whether the file contains an audio stream */
    hasAudio: boolean;
    /** iPhone/mobile rotation value in degrees (0 if none) */
    rotation: number;
    /** Video codec name, e.g. "h264", "hevc", "vp9" (null if no video) */
    videoCodec: string | null;
    /** Audio codec name, e.g. "aac", "mp3", "pcm_s16le" (null if no audio) */
    audioCodec: string | null;
    /** Container format name, e.g. "mov,mp4,m4a,3gp,3g2,mj2" */
    format: string | null;
    /** Frames per second (null for non-video files) */
    fps: number | null;
    /** File size in bytes */
    size: number | null;
    /** Overall bitrate in bits per second */
    bitrate: number | null;
    /** Audio sample rate in Hz, e.g. 48000, 44100 (null if no audio) */
    sampleRate: number | null;
    /** Number of audio channels (1=mono, 2=stereo) (null if no audio) */
    channels: number | null;
    /** Pixel format, e.g. "yuv420p", "yuv420p10le", "yuvj420p" (null if no video) */
    pixelFormat: string | null;
    /** Color space, e.g. "bt709", "bt2020nc" (null if unknown) */
    colorSpace: string | null;
    /** Color transfer characteristics, e.g. "bt709", "smpte2084" (HDR10 PQ), "arib-std-b67" (HLG) */
    colorTransfer: string | null;
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Transcode
  // ─────────────────────────────────────────────────────────────────────────────

  /** Codec-safety preset for SIMPLEFFMPEG.transcode() */
  type TranscodePreset = "web-mp4";

  /** Options shared by both preset and customArgs paths */
  interface TranscodeBaseOptions {
    /** Output file path (resolved to absolute internally) */
    outputPath: string;
    /** Hard timeout in milliseconds, SIGKILL-backed (default: 300000 = 5 min) */
    timeoutMs?: number;
    /** Maps to ffmpeg -fs (default: 524288000 = 500 MB). Best-effort; not strictly enforced for MP4 in all ffmpeg versions. */
    maxOutputBytes?: number;
    /** Maps to ffmpeg -threads (default: 2). Conservative for worker pools / in-request transcoding. */
    threads?: number;
    /** Called with 0..99 during encode, 100 on success */
    onProgress?: (percent: number) => void;
    /** Cancel the transcode; triggers SIGKILL and rejects with code "ABORTED" */
    signal?: AbortSignal;
  }

  /** Options when using a built-in preset. customArgs is forbidden. */
  interface TranscodePresetOptions extends TranscodeBaseOptions {
    preset: TranscodePreset;
    /** libx264 CRF (default: 23) */
    crf?: number;
    /** Video bitrate string, e.g. "2M" */
    videoBitrate?: string;
    /** Audio bitrate string (default: "128k") */
    audioBitrate?: string;
    /** Optional scale; preserves aspect when one dim omitted */
    scale?: { width?: number; height?: number };
    customArgs?: never;
  }

  /** Options when supplying full ffmpeg argv. preset/crf/etc. are forbidden. */
  interface TranscodeCustomArgsOptions extends TranscodeBaseOptions {
    /** Full ffmpeg argv (caller owns -i, -y, output path, etc.) */
    customArgs: string[];
    preset?: never;
    crf?: never;
    videoBitrate?: never;
    audioBitrate?: never;
    scale?: never;
  }

  /** Discriminated union — pick preset XOR customArgs, never both */
  type TranscodeOptions = TranscodePresetOptions | TranscodeCustomArgsOptions;
}

declare class SIMPLEFFMPEG {
  constructor(options?: SIMPLEFFMPEG.SIMPLEFFMPEGOptions);

  /**
   * Load clips into the project
   * @param clips Array of clip descriptors (video, audio, text, image, music)
   * @param options Load options
   */
  load(
    clips: SIMPLEFFMPEG.Clip[],
    options?: SIMPLEFFMPEG.LoadOptions
  ): Promise<void[]>;

  /**
   * Get a preview of the FFmpeg command without executing it (dry-run)
   * @param options Export options
   */
  preview(
    options?: SIMPLEFFMPEG.ExportOptions
  ): Promise<SIMPLEFFMPEG.PreviewResult>;

  /**
   * Export the project to a video file
   * @param options Export options including outputPath, onProgress, and signal
   */
  export(options?: SIMPLEFFMPEG.ExportOptions): Promise<string>;

  /**
   * Get available platform presets
   * @returns Map of preset names to their configurations
   */
  static getPresets(): Record<
    SIMPLEFFMPEG.PlatformPreset,
    SIMPLEFFMPEG.PresetConfig
  >;

  /**
   * Get list of available preset names
   * @returns Array of preset names
   */
  static getPresetNames(): SIMPLEFFMPEG.PlatformPreset[];

  /**
   * Validate clips configuration without creating a project.
   * Useful for AI feedback loops and pre-validation.
   *
   * @param clips - Array of clip objects to validate
   * @param options - Validation options
   * @returns Validation result with valid flag, errors, and warnings
   *
   * @example
   * const result = SIMPLEFFMPEG.validate(clips, { skipFileChecks: true });
   * if (!result.valid) {
   *   result.errors.forEach(e => console.log(`[${e.code}] ${e.path}: ${e.message}`));
   * }
   */
  static validate(
    clips: SIMPLEFFMPEG.Clip[],
    options?: SIMPLEFFMPEG.ValidateOptions
  ): SIMPLEFFMPEG.ValidationResult;

  /**
   * Calculate the total duration of a clips configuration.
   * Resolves shorthand (duration, auto-sequencing) before computing.
   * Returns the visual timeline duration: sum of video/image clip durations
   * minus transition overlaps.
   *
   * Pure function — same clips always produce the same result. No file I/O.
   *
   * @param clips - Array of clip objects
   * @returns Total duration in seconds
   *
   * @example
   * const duration = SIMPLEFFMPEG.getDuration([
   *   { type: "video", url: "./a.mp4", duration: 5 },
   *   { type: "video", url: "./b.mp4", duration: 10,
   *     transition: { type: "fade", duration: 0.5 } },
   * ]);
   * // duration === 14.5
   */
  static getDuration(clips: SIMPLEFFMPEG.Clip[]): number;

  /**
   * Calculate the total transition overlap for a clips configuration.
   * Returns the total seconds consumed by xfade transition overlaps
   * among visual clips (video, image, color).
   *
   * Pure function — same clips always produce the same result. No file I/O.
   *
   * @param clips - Array of clip objects
   * @returns Total transition overlap in seconds
   *
   * @example
   * const overlap = SIMPLEFFMPEG.getTransitionOverlap([
   *   { type: "video", url: "./a.mp4", duration: 5 },
   *   { type: "video", url: "./b.mp4", duration: 10,
   *     transition: { type: "fade", duration: 0.5 } },
   * ]);
   * // overlap === 0.5
   */
  static getTransitionOverlap(clips: SIMPLEFFMPEG.Clip[]): number;

  /**
   * Probe a media file and return comprehensive metadata.
   *
   * Uses ffprobe to extract duration, dimensions, codecs, format,
   * bitrate, audio details, and rotation info from any media file.
   *
   * @param filePath - Path to the media file
   * @returns Media info object
   * @throws {SIMPLEFFMPEG.MediaNotFoundError} If the file cannot be found or probed
   *
   * @example
   * const info = await SIMPLEFFMPEG.probe("./video.mp4");
   * console.log(info.duration);   // 30.5
   * console.log(info.width);      // 1920
   * console.log(info.height);     // 1080
   * console.log(info.videoCodec); // "h264"
   * console.log(info.hasAudio);   // true
   */
  static probe(filePath: string): Promise<SIMPLEFFMPEG.MediaInfo>;

  /**
   * Capture a single frame from a video file and save it as an image.
   * The output format is determined by the outputPath file extension
   * (.jpg, .png, .webp, .bmp, .tiff).
   *
   * @param filePath - Path to the source video file
   * @param options - Snapshot options
   * @returns The output path
   * @throws {SIMPLEFFMPEG.SimpleffmpegError} If filePath or outputPath is missing
   * @throws {SIMPLEFFMPEG.FFmpegError} If FFmpeg fails to extract the frame
   *
   * @example
   * await SIMPLEFFMPEG.snapshot("./video.mp4", {
   *   outputPath: "./frame.png",
   *   time: 5,
   * });
   */
  static snapshot(
    filePath: string,
    options: SIMPLEFFMPEG.SnapshotOptions
  ): Promise<string>;

  /**
   * Extract keyframes from a video using scene-change detection or fixed time intervals.
   *
   * Scene-change mode uses FFmpeg's select=gt(scene,N) filter to detect visual transitions.
   * Interval mode extracts frames at fixed time intervals.
   *
   * When outputDir is provided, frames are written to disk and the method returns file paths.
   * Each call creates a unique `simpleffmpeg-keyframes-XXXXXX` subdirectory inside outputDir,
   * so repeat and concurrent calls against the same outputDir are fully isolated. Always use
   * the returned paths — do not assume frames live directly at `${outputDir}/frame-0001.jpg`.
   * Without outputDir, frames are returned as in-memory Buffer objects.
   *
   * @param filePath - Path to the source video file
   * @param options - Extraction options (with outputDir → string[], without → Buffer[])
   * @throws {SIMPLEFFMPEG.SimpleffmpegError} If arguments are invalid
   * @throws {SIMPLEFFMPEG.FFmpegError} If FFmpeg fails during extraction
   *
   * @example
   * // Scene-change detection — returns Buffer[]
   * const frames = await SIMPLEFFMPEG.extractKeyframes("./video.mp4", {
   *   mode: "scene-change",
   *   sceneThreshold: 0.4,
   *   maxFrames: 8,
   * });
   *
   * @example
   * // Fixed interval — writes to disk, returns string[]
   * const paths = await SIMPLEFFMPEG.extractKeyframes("./video.mp4", {
   *   mode: "interval",
   *   intervalSeconds: 5,
   *   outputDir: "./frames/",
   *   format: "png",
   * });
   */
  static extractKeyframes(
    filePath: string,
    options: SIMPLEFFMPEG.ExtractKeyframesToDiskOptions
  ): Promise<string[]>;
  static extractKeyframes(
    filePath: string,
    options?: SIMPLEFFMPEG.ExtractKeyframesToBufferOptions
  ): Promise<Buffer[]>;

  /**
   * Transcode a media file with hardened defaults suitable for ingestion pipelines.
   *
   * Spawns ffmpeg with no shell, explicit argv, bounded stderr capture, a
   * SIGKILL-backed timeout, path validation, output size cap, and partial
   * output cleanup on failure. The hardening wrapper applies regardless of
   * whether you use the preset or customArgs path.
   *
   * The `web-mp4` preset produces H.264 + AAC in an MP4 container with
   * yuv420p, faststart, even dimensions, profile high / level 4.1 — the
   * durable safe default for browser and downstream renderer pipelines.
   *
   * @param inputPath - Path to the source media file
   * @param options - Preset or customArgs (mutually exclusive)
   * @returns Resolved absolute output path on success
   * @throws {SIMPLEFFMPEG.SimpleffmpegError} If options are missing or both preset+customArgs are provided
   * @throws {SIMPLEFFMPEG.TranscodeError} With `code` discriminator on failure
   *
   * @example
   * // One-liner ingestion
   * await SIMPLEFFMPEG.transcode("./upload.mov", {
   *   outputPath: "./normalized.mp4",
   *   preset: "web-mp4",
   * });
   *
   * @example
   * // With overrides + progress
   * await SIMPLEFFMPEG.transcode("./in.mov", {
   *   outputPath: "./out.mp4",
   *   preset: "web-mp4",
   *   crf: 20,
   *   scale: { width: 1280 },
   *   onProgress: (pct) => console.log(`${pct}%`),
   * });
   */
  static transcode(
    inputPath: string,
    options: SIMPLEFFMPEG.TranscodeOptions
  ): Promise<string>;

  /**
   * Predicate — given a probe() result, return true when the file is already
   * web-safe (h264 in an mp4-family container with yuv420p). Lets callers
   * pair with probe() to skip transcoding when the input needs no work.
   *
   * @example
   * const info = await SIMPLEFFMPEG.probe(path);
   * if (!SIMPLEFFMPEG.isWebSafeMp4(info)) {
   *   await SIMPLEFFMPEG.transcode(path, { outputPath, preset: "web-mp4" });
   * }
   */
  static isWebSafeMp4(info: SIMPLEFFMPEG.MediaInfo): boolean;

  /**
   * Format validation result as human-readable string
   */
  static formatValidationResult(result: SIMPLEFFMPEG.ValidationResult): string;

  /**
   * Get the clip schema as formatted prompt-ready text.
   * Returns a structured description of all clip types accepted by load(),
   * optimized for LLM consumption, documentation, or code generation.
   *
   * @param options - Schema options for filtering modules and adding custom instructions
   * @returns Formatted schema text
   *
   * @example
   * // Get full schema (all clip types)
   * const schema = SIMPLEFFMPEG.getSchema();
   *
   * @example
   * // Only video and image clip types
   * const schema = SIMPLEFFMPEG.getSchema({ include: ['video', 'image'] });
   *
   * @example
   * // Everything except text, with custom instructions
   * const schema = SIMPLEFFMPEG.getSchema({
   *   exclude: ['text'],
   *   instructions: 'Keep videos under 30 seconds.',
   *   moduleInstructions: { video: 'Always use fade transitions.' }
   * });
   */
  static getSchema(options?: SIMPLEFFMPEG.SchemaOptions): string;

  /**
   * Get the list of available schema module IDs.
   * Use these IDs with getSchema({ include: [...] }) or getSchema({ exclude: [...] }).
   *
   * @returns Array of module IDs
   */
  static getSchemaModules(): SIMPLEFFMPEG.SchemaModuleId[];
}

/**
 * Synthetic default export type for ESM default-import IntelliSense
 */
declare namespace _defaultExportType {
  export { SIMPLEFFMPEG as default };
}

export = SIMPLEFFMPEG;
