mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* feat(shared): add outputModality to Tool metadata for crossing tools * feat(shared): add modalityForExtension, toolInputModality, toolOutputModality * fix(pipeline): route finalize and parent jobs to the pipeline's modality pool * feat(automate): add ConvertAudioControls pipeline step (exemplar) * feat(automate): add video tool settings controls to pipelines * feat(automate): add audio tool settings controls to pipelines * feat(automate): add document tool settings controls to pipelines * feat(automate): add chart-maker settings control to pipelines * feat(automate): warn on modality-incompatible pipeline steps * feat(automate): add single-file download button for pipeline results * refactor(automate): modality-aware icons, nav handler rename, mobile size bar * test(pipeline): cover audio, document, file, and cross-modality chains * i18n(automate): translate the modality-warning tooltip * test(pipeline): gate media-pool routing assertion on ffmpeg availability
176 lines
3.9 KiB
TypeScript
176 lines
3.9 KiB
TypeScript
export type Modality = "image" | "video" | "audio" | "document" | "file";
|
|
|
|
export interface ModalityInfo {
|
|
id: Modality;
|
|
name: string;
|
|
icon: string; // lucide icon name, same convention as CategoryInfo
|
|
color: string;
|
|
}
|
|
|
|
// Five modality values in code, surfaced as five UI sections:
|
|
// Image, Video, Audio, PDF ("document" id) and Files ("file" id).
|
|
export const MODALITIES: ModalityInfo[] = [
|
|
{ id: "image", name: "Image", icon: "Image", color: "#3B82F6" },
|
|
{ id: "video", name: "Video", icon: "Video", color: "#EF4444" },
|
|
{ id: "audio", name: "Audio", icon: "AudioLines", color: "#10B981" },
|
|
{ id: "document", name: "PDF", icon: "FileText", color: "#8B5CF6" },
|
|
{ id: "file", name: "Files", icon: "FileArchive", color: "#F59E0B" },
|
|
];
|
|
|
|
// Which BullMQ pool a modality's tools run on. AI tools override to "ai"
|
|
// at enqueue time regardless of modality.
|
|
export const MODALITY_POOL: Record<Modality, "image" | "media" | "docs"> = {
|
|
image: "image",
|
|
video: "media",
|
|
audio: "media",
|
|
document: "docs",
|
|
file: "docs",
|
|
};
|
|
|
|
// Default accepted input extensions per modality (with dots; drives the
|
|
// file picker accept attribute and docs). Tools may narrow this.
|
|
export const IMAGE_INPUTS = [
|
|
".jpg",
|
|
".jpeg",
|
|
".png",
|
|
".webp",
|
|
".gif",
|
|
".bmp",
|
|
".tiff",
|
|
".tif",
|
|
".avif",
|
|
".heic",
|
|
".heif",
|
|
".svg",
|
|
".svgz",
|
|
".ico",
|
|
".jxl",
|
|
".jp2",
|
|
".psd",
|
|
".tga",
|
|
".exr",
|
|
".hdr",
|
|
".dng",
|
|
".cr2",
|
|
".nef",
|
|
".arw",
|
|
".orf",
|
|
".rw2",
|
|
".ppm",
|
|
".pgm",
|
|
".pbm",
|
|
".qoi",
|
|
".dds",
|
|
".fits",
|
|
".dpx",
|
|
".apng",
|
|
".cur",
|
|
".eps",
|
|
];
|
|
export const VIDEO_INPUTS = [
|
|
".mp4",
|
|
".mov",
|
|
".webm",
|
|
".mkv",
|
|
".avi",
|
|
".m4v",
|
|
".mts",
|
|
".m2ts",
|
|
".3gp",
|
|
".flv",
|
|
".wmv",
|
|
".mpg",
|
|
".mpeg",
|
|
".ts",
|
|
".ogv",
|
|
];
|
|
export const AUDIO_INPUTS = [
|
|
".mp3",
|
|
".wav",
|
|
".flac",
|
|
".aac",
|
|
".m4a",
|
|
".ogg",
|
|
".opus",
|
|
".wma",
|
|
".aiff",
|
|
".amr",
|
|
".ac3",
|
|
];
|
|
export const SUBTITLE_INPUTS = [".srt", ".vtt", ".ass"] as const;
|
|
export const DOCUMENT_INPUTS = [
|
|
".pdf",
|
|
".docx",
|
|
".doc",
|
|
".xlsx",
|
|
".xls",
|
|
".pptx",
|
|
".ppt",
|
|
".odt",
|
|
".ods",
|
|
".odp",
|
|
".rtf",
|
|
".txt",
|
|
".md",
|
|
".html",
|
|
".epub",
|
|
];
|
|
export const FILE_INPUTS = [".csv", ".json", ".xml", ".yaml", ".yml", ".zip"];
|
|
|
|
export function detectModalityFromMime(mime: string): Modality {
|
|
if (mime.startsWith("image/")) return "image";
|
|
if (mime.startsWith("video/")) return "video";
|
|
if (mime.startsWith("audio/")) return "audio";
|
|
if (
|
|
mime === "application/pdf" ||
|
|
mime.includes("officedocument") ||
|
|
mime.includes("msword") ||
|
|
mime.includes("ms-excel") ||
|
|
mime.includes("ms-powerpoint") ||
|
|
mime === "application/epub+zip" ||
|
|
mime.startsWith("text/html")
|
|
) {
|
|
return "document";
|
|
}
|
|
return "file";
|
|
}
|
|
|
|
const MODALITY_EXTENSION_SETS: [Modality, readonly string[]][] = [
|
|
["image", IMAGE_INPUTS],
|
|
["video", VIDEO_INPUTS],
|
|
["audio", AUDIO_INPUTS],
|
|
["document", DOCUMENT_INPUTS],
|
|
["file", FILE_INPUTS],
|
|
];
|
|
|
|
/** Owning modality for a file extension (with or without leading dot), or null. */
|
|
export function modalityForExtension(ext: string): Modality | null {
|
|
const norm = (ext.startsWith(".") ? ext : `.${ext}`).toLowerCase();
|
|
for (const [modality, set] of MODALITY_EXTENSION_SETS) {
|
|
if (set.includes(norm)) return modality;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The modality a tool consumes. Derived from acceptedInputs so tools whose `modality`
|
|
* differs from their real input (gif-to-video, images-to-video) are correct. */
|
|
export function toolInputModality(tool: {
|
|
modality: Modality;
|
|
acceptedInputs: string[];
|
|
}): Modality {
|
|
const first = tool.acceptedInputs[0];
|
|
if (first) {
|
|
const m = modalityForExtension(first);
|
|
if (m) return m;
|
|
}
|
|
return tool.modality;
|
|
}
|
|
|
|
/** The modality a tool produces. Uses the explicit override, else the tool modality. */
|
|
export function toolOutputModality(tool: {
|
|
modality: Modality;
|
|
outputModality?: Modality;
|
|
}): Modality {
|
|
return tool.outputModality ?? tool.modality;
|
|
}
|