Files
SnapOtter/packages/shared/src/modality.ts
T
SnapOtterandGitHub a53038ed96 feat(automate): make the pipeline builder fully multi-modal (#335)
* 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
2026-06-24 00:21:41 +08:00

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;
}