Spaces:
Sleeping
Sleeping
| import YAML from "yaml" | |
| import { v4 as uuidv4 } from "uuid" | |
| import { ClapHeader, ClapMeta, ClapModel, ClapProject, ClapScene, ClapSegment } from "./types" | |
| import { getValidNumber } from "@/lib/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<ClapProject> { | |
| // 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 : "", | |
| } | |
| /* | |
| 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 | |
| } | |
| } | |