← Files Cecil-IA Labs FFmpegARCHIVED FILE

dist/pipeline/inline.js

10.3 KB · Sep 30, 2026 · 23:17 UTC

↓ Download file

import path from "node:path";
import { ZodError } from "zod";
import { ToolkitRuntimeError } from "../core/errors.js";
import { pipelineDocumentSchema } from "./schema.js";
const ACTIONS = new Set(["validate", "print", "run"]);
const GLOBAL_FLAGS = new Set([
    "--overwrite",
    "--dry-run",
    "--json",
    "--quiet",
    "--verbose",
    "--no-progress",
    "--no-color",
    "--keep-temp",
]);
const GLOBAL_VALUE_FLAGS = new Set(["--ffmpeg-path", "--ffprobe-path"]);
function invalid(message, details) {
    throw new ToolkitRuntimeError("E_USAGE_INVALID_ARGUMENT", message, {
        ...(details !== undefined ? { details } : {}),
    });
}
function valueAt(tokens, index, flag) {
    const value = tokens[index + 1];
    if (value === undefined)
        invalid(`${flag} requires a value.`, { flag });
    if (value.startsWith("--"))
        invalid(`${flag} requires a value.`, { flag });
    return value;
}
function numberValue(value, flag) {
    const parsed = Number(value);
    if (!Number.isFinite(parsed))
        invalid(`${flag} must be a finite number.`, { flag, value });
    return parsed;
}
function integerValue(value, flag) {
    const parsed = numberValue(value, flag);
    if (!Number.isInteger(parsed))
        invalid(`${flag} must be an integer.`, { flag, value });
    return parsed;
}
function normalizePath(value, cwd) {
    return path.resolve(cwd, value);
}
function samePath(left, right, cwd) {
    return normalizePath(left, cwd) === normalizePath(right, cwd);
}
function collectActions(tokens) {
    let selected;
    for (const [index, token] of tokens.entries()) {
        if (!ACTIONS.has(token))
            continue;
        selected = { action: token, index };
    }
    if (selected === undefined) {
        invalid("pipeline requires a final action: validate, print, or run.");
    }
    return selected;
}
function stripGlobalOptions(tokens) {
    const positional = [];
    for (let index = 0; index < tokens.length; index += 1) {
        const token = tokens[index];
        if (token === undefined)
            continue;
        if (GLOBAL_FLAGS.has(token))
            continue;
        if (GLOBAL_VALUE_FLAGS.has(token)) {
            valueAt(tokens, index, token);
            index += 1;
            continue;
        }
        if (token === "--output") {
            valueAt(tokens, index, token);
            index += 1;
            continue;
        }
        if (token === "--")
            continue;
        if (token.startsWith("--"))
            invalid(`Unknown pipeline option "${token}".`);
        positional.push(token);
    }
    return positional;
}
function parseFileInvocation(tokens, action, actionIndex) {
    const beforeAction = stripGlobalOptions(tokens.slice(0, actionIndex));
    const afterAction = stripGlobalOptions(tokens.slice(actionIndex + 1));
    if (beforeAction.length !== 1 || afterAction.length !== 0) {
        invalid("File pipelines use: pipeline <file> <validate|print|run>.", {
            beforeAction,
            afterAction,
        });
    }
    const file = beforeAction[0];
    if (file === undefined)
        invalid("Pipeline file is missing.");
    return { action, source: "file", file };
}
function setValue(builder, flag, value) {
    if (flag === "--input") {
        builder.input = String(value);
        return;
    }
    if (flag === "--output") {
        builder.output = String(value);
        return;
    }
    const keyByFlag = {
        "--trim-start": "start",
        "--start": "start",
        "--trim-end": "end",
        "--end": "end",
        "--duration": "duration",
        "--mode": "mode",
        "--factor": "factor",
        "--audio": "audio",
        "--width": "width",
        "--height": "height",
        "--fit": "fit",
        "--background": "background",
        "--profile": "profile",
        "--fps": "fps",
        "--crf": "crf",
        "--preset": "preset",
        "--video-preset": "preset",
        "--to": "to",
        "--hardware": "hardware",
        "--hardware-device": "hardwareDevice",
        "--pixel-format": "pixelFormat",
        "--sample-rate": "sampleRate",
        "--channels": "channels",
        "--quality": "quality",
        "--max-colors": "maxColors",
        "--loop": "loop",
        "--audio-bitrate": "audioBitrate",
        "--name": "name",
        "--codec": "codec",
    };
    const key = keyByFlag[flag];
    if (key === undefined)
        invalid(`Option ${flag} is not supported for inline pipelines.`, { flag });
    builder.values[key] = value;
}
function buildStep(builder) {
    switch (builder.kind) {
        case "trim":
            return { trim: builder.values };
        case "speed":
            return { speed: builder.values };
        case "resize":
            return { resize: builder.values };
        case "normalize":
            return { normalize: builder.values };
        case "audio":
            return { audio: { normalize: true, ...builder.values } };
        case "convert":
            return { convert: builder.values };
        case "preset": {
            const name = builder.values["name"] ?? builder.values["preset"];
            if (typeof name !== "string" || name.trim().length === 0) {
                invalid("Inline preset steps require --name <preset> or --preset <preset>.");
            }
            return { preset: name };
        }
        default:
            invalid(`Unsupported inline pipeline step "${builder.kind}".`, { step: builder.kind });
    }
}
function parseInlineDocument(tokens, actionIndex, cwd, outputOverride) {
    const builders = [];
    let current;
    let initialInput;
    let finalOutput;
    let outputCodec;
    const pushCurrent = () => {
        if (current === undefined)
            return;
        builders.push(current);
        current = undefined;
    };
    for (let index = 0; index < actionIndex; index += 1) {
        const token = tokens[index];
        if (token === undefined)
            continue;
        if (GLOBAL_FLAGS.has(token))
            continue;
        if (GLOBAL_VALUE_FLAGS.has(token)) {
            valueAt(tokens, index, token);
            index += 1;
            continue;
        }
        if (token === "--")
            continue;
        if (token === "--step") {
            pushCurrent();
            current = { kind: valueAt(tokens, index, token), values: {} };
            index += 1;
            continue;
        }
        if (token === "--hardware-strict") {
            if (current === undefined)
                invalid("--hardware-strict must follow --step.");
            current.values["hardwareStrict"] = true;
            continue;
        }
        if (!token.startsWith("--"))
            invalid(`Unexpected inline pipeline token "${token}".`);
        const value = valueAt(tokens, index, token);
        const numericFlags = new Set([
            "--trim-start", "--start", "--trim-end", "--end", "--duration", "--factor", "--width",
            "--height", "--fps", "--crf", "--sample-rate", "--channels", "--quality", "--max-colors", "--loop",
        ]);
        const parsedValue = numericFlags.has(token)
            ? ["--width", "--height", "--sample-rate", "--channels", "--max-colors", "--loop"].includes(token)
                ? integerValue(value, token)
                : numberValue(value, token)
            : value;
        if (token === "--input") {
            if (current === undefined)
                initialInput = value;
            else
                setValue(current, token, value);
        }
        else if (token === "--output") {
            if (current === undefined)
                finalOutput = value;
            else
                setValue(current, token, value);
        }
        else if (token === "--codec") {
            if (value !== "h264" && value !== "vp9")
                invalid("--codec must be h264 or vp9.", { value });
            outputCodec = value;
        }
        else {
            if (current === undefined)
                invalid(`${token} must follow --step.`, { flag: token });
            setValue(current, token, parsedValue);
        }
        index += 1;
    }
    pushCurrent();
    if (builders.length === 0)
        invalid("Inline pipelines require at least one --step.");
    const steps = builders.map(buildStep);
    const input = initialInput ?? builders[0]?.input;
    if (input === undefined || input.trim().length === 0) {
        invalid("Inline pipelines require --input <path> on the first step.");
    }
    let previousOutput;
    for (const [index, builder] of builders.entries()) {
        if (index === 0 && builder.input !== undefined && !samePath(builder.input, input, cwd)) {
            invalid("The first inline step input must match the pipeline input.");
        }
        if (index > 0 && builder.input !== undefined && previousOutput !== undefined && !samePath(builder.input, previousOutput, cwd)) {
            invalid("Inline pipeline steps must chain each --input from the previous --output.", {
                step: index + 1,
                input: builder.input,
                previousOutput,
            });
        }
        if (builder.output !== undefined)
            previousOutput = builder.output;
    }
    const output = finalOutput ?? previousOutput ?? outputOverride;
    if (output === undefined || output.trim().length === 0) {
        invalid("Inline pipelines require --output <path> on the final step.");
    }
    const raw = {
        version: 1,
        input,
        steps,
        output: { path: output, ...(outputCodec !== undefined ? { codec: outputCodec } : {}) },
    };
    const parsed = pipelineDocumentSchema.safeParse(raw);
    if (!parsed.success) {
        const details = parsed.error instanceof ZodError
            ? parsed.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }))
            : undefined;
        invalid("Inline pipeline does not satisfy the pipeline v1 schema.", details ? { issues: details } : undefined);
    }
    return parsed.data;
}
export function parsePipelineInvocation(tokens, options = {}) {
    const cwd = path.resolve(options.cwd ?? process.cwd());
    const { action, index: actionIndex } = collectActions(tokens);
    const beforeAction = tokens.slice(0, actionIndex);
    if (!beforeAction.includes("--step"))
        return parseFileInvocation(tokens, action, actionIndex);
    return {
        action,
        source: "inline",
        document: parseInlineDocument(tokens, actionIndex, cwd, options.outputOverride),
    };
}
//# sourceMappingURL=inline.js.map

SHA-256: 9e86f90502585e774f464604328d8f5eeb0b941dc5c901b69bbd5dc9effdc981