import YAML from "yaml" import { v4 as uuidv4 } from "uuid" import { ClapHeader, ClapMeta, ClapModel, ClapProject, ClapScene, ClapSegment, ClapStreamType } from "./types" import { getValidNumber } from "@/lib/utils/getValidNumber" /** * import a Clap file (from a plain text string) * * note: it is not really async, because for some reason YAML.parse is a blocking call like for JSON, * they is no async version although we are now in the 20s not 90s */ export async function parseClap(inputStringOrBlob: string | Blob): Promise { // Decompress the input blob using gzip const decompressor = new DecompressionStream('gzip'); const inputBlob = typeof inputStringOrBlob === "string" ? new Blob([inputStringOrBlob], { type: "application/x-yaml" }) : inputStringOrBlob; const decompressedStream = inputBlob.stream().pipeThrough(decompressor); // Convert the stream to text using a Response object const text = await new Response(decompressedStream).text(); // Parse YAML string to raw data const rawData = YAML.parse(text); if (!Array.isArray(rawData) || rawData.length < 2) { throw new Error("invalid clap file (need a clap format header block and project metadata block)") } const maybeClapHeader = rawData[0] as ClapHeader if (maybeClapHeader.format !== "clap-0") { throw new Error("invalid clap file (sorry, but you can't make up version numbers like that)") } const maybeClapMeta = rawData[1] as ClapMeta const clapMeta: ClapMeta = { id: typeof maybeClapMeta.title === "string" ? maybeClapMeta.id : uuidv4(), title: typeof maybeClapMeta.title === "string" ? maybeClapMeta.title : "", description: typeof maybeClapMeta.description === "string" ? maybeClapMeta.description : "", licence: typeof maybeClapMeta.licence === "string" ? maybeClapMeta.licence : "", orientation: maybeClapMeta.orientation === "portrait" ? "portrait" : maybeClapMeta.orientation === "square" ? "square" : "landscape", width: getValidNumber(maybeClapMeta.width, 256, 8192, 1024), height: getValidNumber(maybeClapMeta.height, 256, 8192, 576), defaultVideoModel: typeof maybeClapMeta.defaultVideoModel === "string" ? maybeClapMeta.defaultVideoModel : "SVD", extraPositivePrompt: Array.isArray(maybeClapMeta.extraPositivePrompt) ? maybeClapMeta.extraPositivePrompt : [], screenplay: typeof maybeClapMeta.screenplay === "string" ? maybeClapMeta.screenplay : "", streamType: (typeof maybeClapMeta.streamType == "string" ? maybeClapMeta.streamType : "static") as ClapStreamType, } /* in case we want to support streaming (mix of models and segments etc), we could do it this way: const maybeModelsOrSegments = rawData.slice(2) maybeModelsOrSegments.forEach((unknownElement: any) => { if (isValidNumber(unknownElement?.track)) { maybeSegments.push(unknownElement as ClapSegment) } else { maybeModels.push(unknownElement as ClapModel) } }) */ const expectedNumberOfModels = maybeClapHeader.numberOfModels || 0 const expectedNumberOfScenes = maybeClapHeader.numberOfScenes || 0 const expectedNumberOfSegments = maybeClapHeader.numberOfSegments || 0 // note: we assume the order is strictly enforced! // if you implement streaming (mix of models and segments) you will have to rewrite this! const afterTheHeaders = 2 const afterTheModels = afterTheHeaders + expectedNumberOfModels const afterTheScenes = afterTheModels + expectedNumberOfScenes // note: if there are no expected models, maybeModels will be empty const maybeModels = rawData.slice(afterTheHeaders, afterTheModels) as ClapModel[] // note: if there are no expected scenes, maybeScenes will be empty const maybeScenes = rawData.slice(afterTheModels, afterTheScenes) as ClapScene[] const maybeSegments = rawData.slice(afterTheScenes) as ClapSegment[] const clapModels: ClapModel[] = maybeModels.map(({ id, category, triggerName, label, description, author, thumbnailUrl, seed, assetSourceType, assetUrl, age, gender, region, appearance, voiceVendor, voiceId, }) => ({ // TODO: we should verify each of those, probably id, category, triggerName, label, description, author, thumbnailUrl, seed, assetSourceType, assetUrl, age, gender, region, appearance, voiceVendor, voiceId, })) const clapScenes: ClapScene[] = maybeScenes.map(({ id, scene, line, rawLine, sequenceFullText, sequenceStartAtLine, sequenceEndAtLine, startAtLine, endAtLine, events, }) => ({ id, scene, line, rawLine, sequenceFullText, sequenceStartAtLine, sequenceEndAtLine, startAtLine, endAtLine, events: events.map(e => e) })) const clapSegments: ClapSegment[] = maybeSegments.map(({ id, track, startTimeInMs, endTimeInMs, category, modelId, sceneId, prompt, label, outputType, renderId, status, assetUrl, assetDurationInMs, createdBy, editedBy, outputGain, seed, }) => ({ // TODO: we should verify each of those, probably id, track, startTimeInMs, endTimeInMs, category, modelId, sceneId, prompt, label, outputType, renderId, status, assetUrl, assetDurationInMs, createdBy, editedBy, outputGain, seed, })) return { meta: clapMeta, models: clapModels, scenes: clapScenes, segments: clapSegments } }