diff --git a/app/[locale]/main/WorkflowSidePicker.tsx b/app/[locale]/main/WorkflowSidePicker.tsx index 5c283d7..300cf49 100644 --- a/app/[locale]/main/WorkflowSidePicker.tsx +++ b/app/[locale]/main/WorkflowSidePicker.tsx @@ -83,7 +83,7 @@ export const WorkflowSidePicker: IComponent = () => { if (crrInput.type === EValueType.Number) { inputRecord[key] = Number(inputData[key] || crrInput.default) } - if ([EValueType.File, EValueType.Image].includes(crrInput.type as EValueType)) { + if ([EValueType.File, EValueType.Video, EValueType.Image].includes(crrInput.type as EValueType)) { const files = inputData[key] as File[] if (!files || files.length === 0) { toast({ diff --git a/app/[locale]/main/workflow/TaskItem.tsx b/app/[locale]/main/workflow/TaskItem.tsx index e9a85f1..e49e9b6 100644 --- a/app/[locale]/main/workflow/TaskItem.tsx +++ b/app/[locale]/main/workflow/TaskItem.tsx @@ -241,7 +241,7 @@ export const TaskItem: IComponent<{ /> {runningTime >= 0 && } {task.repeatCount > 1 && } - {!!attachments?.length && } + {!!attachments?.length && } {task.computedCost > 0 && ( )} diff --git a/components/AttachmentDetail.tsx b/components/AttachmentDetail.tsx index fc76ac2..7576b2a 100644 --- a/components/AttachmentDetail.tsx +++ b/components/AttachmentDetail.tsx @@ -31,7 +31,7 @@ export const AttachmentDetail: IComponent<{ const renderMapperInput = useCallback((config: IMapperInput, inputVal: any) => { const value = inputVal || config.default - if (config.type === EValueType.Image) { + if ([EValueType.Image, EValueType.Video].includes(config.type as EValueType)) { let src = value if (isArray(src) && src.length === 1) { src = src[0] diff --git a/components/AttachmentReview.tsx b/components/AttachmentReview.tsx index f74de1a..30a849e 100644 --- a/components/AttachmentReview.tsx +++ b/components/AttachmentReview.tsx @@ -5,7 +5,7 @@ import { Attachment } from '@/entities/attachment' import { cn } from '@/lib/utils' import { PhotoView } from 'react-photo-view' import { Button } from './ui/button' -import { Download, MoreHorizontal, Star } from 'lucide-react' +import { Download, ImageIcon, MoreHorizontal, Star } from 'lucide-react' import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu' import LoadableImage from './LoadableImage' @@ -24,6 +24,8 @@ import useCurrentMousePosRef from '@/hooks/useCurrentMousePos' import { useScrollingStatusRef } from '@/hooks/useScrollingStatus' import useCopyAction from '@/hooks/useCopyAction' import { useActionDebounce } from '@/hooks/useAction' +import { EValueType } from '@/entities/enum' +import { PlayCircleIcon } from '@heroicons/react/24/outline' const AttachmentTooltipPopup: IComponent<{ taskId?: string @@ -108,6 +110,8 @@ export const AttachmentReview: IComponent<{ } ) + const isVideo = image?.type === EValueType.Video + const downloadFn = (mode: 'jpg' | 'raw' = 'raw') => { if (mode === 'raw') { window.open(image?.raw?.url, '_blank') @@ -191,6 +195,20 @@ export const AttachmentReview: IComponent<{ 'rounded-lg outline outline-white shadow cursor-pointer': isPop })} /> + {isPop && ( + e.stopPropagation()}> + {!!image && ( +
+ {image.type === EValueType.Video ? ( + + ) : ( + + )} + {image?.type} +
+ )} +
+ )} {isPop && (
- - - - - - downloadFn('jpg')} className='cursor-pointer text-sm'> - Download compressed JPG - - downloadFn()} className='cursor-pointer text-sm'> - Download Raw - - - + {isVideo ? ( + + ) : ( + + + + + + downloadFn('jpg')} className='cursor-pointer text-sm'> + Download compressed JPG + + + downloadFn()} className='cursor-pointer text-sm'> + Download Raw + + + + )} {!!onPressFavorite && (
)} + {!!image && ( +
+ {image.type === EValueType.Video ? ( + + ) : ( + + )} +
+ )} {!!onPressFavorite && (
@@ -129,7 +129,7 @@ export const ImageGallery: IComponent<{ id='bottom' ref={bottomRef} className={cn('w-full flex items-center justify-center mt-4 pt-4 pb-24 text-gray-400', { - 'hidden': items.length === 0 + hidden: items.length === 0 })} > {hasNextPage &&
More data...
} diff --git a/components/dialogs/AddWorkflowDialog/steps/CreateInputNode.tsx b/components/dialogs/AddWorkflowDialog/steps/CreateInputNode.tsx index 1c031bc..f68c1dd 100644 --- a/components/dialogs/AddWorkflowDialog/steps/CreateInputNode.tsx +++ b/components/dialogs/AddWorkflowDialog/steps/CreateInputNode.tsx @@ -24,6 +24,7 @@ import { LanguageIcon, ListBulletIcon, PhotoIcon, + PlayCircleIcon, PuzzlePieceIcon, SparklesIcon, VariableIcon @@ -106,6 +107,9 @@ export const CreateInputNode: IComponent<{ case EValueType.Image: form.setValue('icon', 'PhotoIcon') break + case EValueType.Video: + form.setValue('icon', 'PlayCircleIcon') + break case EValueType.File: form.setValue('icon', 'DocumentArrowUpIcon') break @@ -146,6 +150,8 @@ export const CreateInputNode: IComponent<{ return 'Use for string input like positive, negative, caption,...' case EValueType.Image: return 'Use for image input like load_image, load_mask,...' + case EValueType.Video: + return 'Use for video input like load_video,...' case EValueType.File: return 'Use for file input like load_audio, load_text,...' case EValueType.Boolean: @@ -288,6 +294,12 @@ export const CreateInputNode: IComponent<{ Image
+ +
+ + Video +
+
diff --git a/components/dialogs/AddWorkflowDialog/steps/CreateOutputNode.tsx b/components/dialogs/AddWorkflowDialog/steps/CreateOutputNode.tsx index 05684f0..28b63dc 100644 --- a/components/dialogs/AddWorkflowDialog/steps/CreateOutputNode.tsx +++ b/components/dialogs/AddWorkflowDialog/steps/CreateOutputNode.tsx @@ -19,7 +19,7 @@ import { DocumentArrowUpIcon, LanguageIcon, PhotoIcon, - SparklesIcon, + PlayCircleIcon, VariableIcon } from '@heroicons/react/24/outline' import { zodResolver } from '@hookform/resolvers/zod' @@ -74,6 +74,9 @@ export const CreateOutputNode: IComponent<{ case EValueType.Image: form.setValue('icon', 'PhotoIcon') break + case EValueType.Video: + form.setValue('icon', 'PlayCircleIcon') + break case EValueType.File: form.setValue('icon', 'DocumentArrowUpIcon') break @@ -192,6 +195,12 @@ export const CreateOutputNode: IComponent<{ Image
+ +
+ + Video +
+
diff --git a/components/dialogs/AddWorkflowDialog/steps/Finalize.tsx b/components/dialogs/AddWorkflowDialog/steps/Finalize.tsx index fbab2da..1d8b409 100644 --- a/components/dialogs/AddWorkflowDialog/steps/Finalize.tsx +++ b/components/dialogs/AddWorkflowDialog/steps/Finalize.tsx @@ -75,7 +75,7 @@ export const FinalizeStep: IComponent = () => { for (const key of inputKeys) { const input = workflow?.mapInput?.[key as keyof typeof workflow.mapInput] if (!input) continue - if (input.type === EValueType.File || input.type === EValueType.Image) { + if (input.type === EValueType.File || input.type === EValueType.Image || input.type === EValueType.Video) { const files = wfObj[key] as File[] if (files.length > 0) { const file = files[0] @@ -140,7 +140,7 @@ export const FinalizeStep: IComponent = () => { defaultValue={String(input.default ?? '')} /> )} - {[EValueType.File, EValueType.Image].includes(input.type as EValueType) && ( + {[EValueType.File, EValueType.Image, EValueType.Video].includes(input.type as EValueType) && ( { const data = progressEv.data.output[key as keyof typeof progressEv.data] let items: ReactNode[] = [] switch (data.info.type) { + case EValueType.Video: case EValueType.Image: const imageURLs = data.data as { url: string }[] items.push( diff --git a/components/ui-ext/download-button.tsx b/components/ui-ext/download-button.tsx index a5a9f15..6dcdba2 100644 --- a/components/ui-ext/download-button.tsx +++ b/components/ui-ext/download-button.tsx @@ -7,6 +7,7 @@ import { Button } from '../ui/button' import { Download } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu' import { trpc } from '@/utils/trpc' +import { EValueType } from '@/entities/enum' const DownloadImagesButton: IComponent<{ workflowTaskId?: string @@ -19,19 +20,45 @@ const DownloadImagesButton: IComponent<{ refetchOnWindowFocus: false } ) + const haveVideo = !!attachments?.some((a) => a?.type === EValueType.Video) + const haveImage = !!attachments?.some((a) => a?.type === EValueType.Image) + const { bundleImages, isLoading, progress, error } = useImageBundler() - const downloadFn = async (mode: 'jpg' | 'raw' = 'jpg') => { + const downloadRawOutput = () => { if (isLoading) return try { const images = attachments ?.filter((a) => !!a) .filter((a) => !!a.raw?.url) - .map((v) => { - if (mode === 'jpg') return v.high!.url - return v.raw!.url - }) as string[] - await bundleImages(images) + .map((v) => v.raw!.url) as string[] + bundleImages(images) + } catch (e) { + console.error(e) + } + } + + const downloadCompressedJpg = () => { + if (isLoading) return + try { + const images = attachments + ?.filter((a) => !!a && a.type === EValueType.Image) + .filter((a) => !!a.high?.url) + .map((v) => v.high!.url) as string[] + bundleImages(images) + } catch (e) { + console.error(e) + } + } + + const downloadVideos = () => { + if (isLoading) return + try { + const videos = attachments + ?.filter((a) => !!a && a.type === EValueType.Video) + .filter((a) => !!a.raw?.url) + .map((v) => v.raw!.url) as string[] + bundleImages(videos) } catch (e) { console.error(e) } @@ -58,11 +85,18 @@ const DownloadImagesButton: IComponent<{ - downloadFn('jpg')} className='cursor-pointer text-sm'> - Download compressed JPG - - downloadFn()} className='cursor-pointer text-sm'> - Download Raw + {haveVideo && ( + + Download video files + + )} + {haveImage && ( + + Download compressed JPG + + )} + + Download raw output diff --git a/entities/attachment.ts b/entities/attachment.ts index c7398a6..193c6d7 100644 --- a/entities/attachment.ts +++ b/entities/attachment.ts @@ -24,7 +24,7 @@ export class Attachment { ratio?: number // width / height, only for image @Property({ type: 'varchar', default: EValueType.Image, nullable: true }) - type?: EValueType.Image | EValueType.File + type?: EValueType.Image | EValueType.Video | EValueType.File @Property({ type: 'varchar', default: EAttachmentStatus.PENDING, index: true }) status!: EAttachmentStatus diff --git a/entities/enum.ts b/entities/enum.ts index 2a128f1..c6e6708 100644 --- a/entities/enum.ts +++ b/entities/enum.ts @@ -51,6 +51,7 @@ export enum EValueType { File = 'File', String = 'String', Number = 'Number', + Video = 'Video', Image = 'Image', Boolean = 'Boolean' } diff --git a/server/handlers/workflow.ts b/server/handlers/workflow.ts index 0f67b33..58c4dbd 100644 --- a/server/handlers/workflow.ts +++ b/server/handlers/workflow.ts @@ -126,7 +126,11 @@ export const WorkflowPlugin = new Elysia({ prefix: '/workflow', detail: { tags: throw new Error(`Value of <${key}> is greater than max value (max ${keyConfig.max})`) } } - if (keyConfig.type === EValueType.File || keyConfig.type === EValueType.Image) { + if ( + keyConfig.type === EValueType.File || + keyConfig.type === EValueType.Image || + keyConfig.type === EValueType.Video + ) { const temp = input[key] if (Array.isArray(temp) && temp.length === 0) { set.status = 400 diff --git a/server/routers/attachment.ts b/server/routers/attachment.ts index 3dd9099..2521dd1 100644 --- a/server/routers/attachment.ts +++ b/server/routers/attachment.ts @@ -16,6 +16,7 @@ const getAttachmentURL = async (attachment: Attachment, baseUrl = 'http://localh AttachmentService.getInstance().getFileURL(highName, 3600 * 24, baseUrl) ]) return { + type: attachment.type, raw: imageInfo, preview: imagePreviewInfo || imageInfo, high: imageHighInfo || imageInfo diff --git a/server/routers/snippet.ts b/server/routers/snippet.ts index b94d47e..e68516a 100644 --- a/server/routers/snippet.ts +++ b/server/routers/snippet.ts @@ -22,7 +22,9 @@ export const snippetRouter = router({ const inputBody: Record = {} for (const inputKey in workflow.mapInput) { if (workflow.mapInput[inputKey].type === EValueUtilityType.Prefixer) continue - if ([EValueType.File, EValueType.Image].includes(workflow.mapInput[inputKey].type as EValueType)) { + if ( + [EValueType.File, EValueType.Image, EValueType.Video].includes(workflow.mapInput[inputKey].type as EValueType) + ) { inputBody[inputKey] = '' needUpload = true continue diff --git a/server/routers/workflow.ts b/server/routers/workflow.ts index fb76a48..309760c 100644 --- a/server/routers/workflow.ts +++ b/server/routers/workflow.ts @@ -257,15 +257,16 @@ export const workflowRouter = router({ builder.input(key, String(inputData)) break case EValueType.File: + case EValueType.Video: case EValueType.Image: const file = inputData as Attachment const fileBlob = await AttachmentService.getInstance().getFileBlob(file.fileName) if (!fileBlob) { - return subscriber.next({ key: 'failed', detail: 'missing image' }) + return subscriber.next({ key: 'failed', detail: 'missing file' }) } const uploadedImg = await api.uploadImage(fileBlob, file.fileName) if (!uploadedImg) { - subscriber.next({ key: 'failed', detail: 'failed to upload image' }) + subscriber.next({ key: 'failed', detail: 'failed to upload file' }) return } builder.input(key, uploadedImg.info.filename) diff --git a/server/routers/workflow_task.ts b/server/routers/workflow_task.ts index de11725..27a55b5 100644 --- a/server/routers/workflow_task.ts +++ b/server/routers/workflow_task.ts @@ -148,13 +148,13 @@ export const workflowTaskRouter = router({ }), getOutputAttachmentUrls: privateProcedure.input(z.string()).query(async ({ input, ctx }) => { const task = await ctx.em.findOneOrFail(WorkflowTask, { id: input }, { populate: ['subTasks.events'] }) - let fileNames: string[] = [] + let fileNames: { filename: string; type?: EValueType }[] = [] if (task.status !== ETaskStatus.Parent) { const attachments = await ctx.em.find(Attachment, { task }) - fileNames = attachments.map((a) => a.fileName) + fileNames = attachments.map((a) => ({ filename: a.fileName, type: a.type })) } else { const subTaskIds = task.subTasks.map((t) => t.id) const attachments = await ctx.em.find(Attachment, { @@ -164,18 +164,20 @@ export const workflowTaskRouter = router({ } } }) - fileNames = attachments.map((a) => a.fileName) + fileNames = attachments.map((a) => ({ filename: a.fileName, type: a.type })) } return Promise.all( - fileNames.map(async (fileName) => { - const prevName = `${fileName}_preview.jpg` - const highName = `${fileName}_high.jpg` + fileNames.map(async (info) => { + const { filename, type } = info + const prevName = `${filename}_preview.jpg` + const highName = `${filename}_high.jpg` const [imageInfo, imagePreviewInfo, imageHighInfo] = await Promise.all([ - AttachmentService.getInstance().getFileURL(fileName, 3600 * 24, ctx.baseUrl), + AttachmentService.getInstance().getFileURL(filename, 3600 * 24, ctx.baseUrl), AttachmentService.getInstance().getFileURL(prevName, 3600 * 24, ctx.baseUrl), AttachmentService.getInstance().getFileURL(highName, 3600 * 24, ctx.baseUrl) ]) return { + type, raw: imageInfo, preview: imagePreviewInfo || imageInfo, high: imageHighInfo || imageInfo diff --git a/server/schemas/attachment.ts b/server/schemas/attachment.ts index 9352c04..41b6a17 100644 --- a/server/schemas/attachment.ts +++ b/server/schemas/attachment.ts @@ -7,7 +7,7 @@ export const AttachmentSchema = t.Object({ fileName: t.String(), size: t.Number(), ratio: t.Number(), - type: t.UnionEnum([EValueType.File, EValueType.Image]), + type: t.UnionEnum([EValueType.File, EValueType.Image, EValueType.Video]), status: t.Enum(EAttachmentStatus), storageType: t.String(), taskEvent: t.Optional(t.Null()), diff --git a/server/utils/file.ts b/server/utils/file.ts new file mode 100644 index 0000000..d314f75 --- /dev/null +++ b/server/utils/file.ts @@ -0,0 +1,16 @@ +/** + * Determine the type of a Blob: Image, Video, or Other. + * @param blob - The Blob to classify. + * @returns A string indicating the type: 'image', 'video', or 'other'. + */ +export function classifyBlob(blob: Blob): 'image' | 'video' | 'other' { + const mimeType = blob.type // Get the MIME type from the Blob + + if (mimeType.startsWith('image/')) { + return 'image' // It's an image + } else if (mimeType.startsWith('video/')) { + return 'video' // It's a video + } else { + return 'other' // Something else + } +} diff --git a/services/comfyui.service.ts b/services/comfyui.service.ts index 55d2cf4..66a0364 100644 --- a/services/comfyui.service.ts +++ b/services/comfyui.service.ts @@ -27,6 +27,9 @@ import AttachmentService, { EAttachmentType } from './attachment.service' import { ImageUtil } from '@/server/utils/ImageUtil' import { delay } from '@/utils/tools' import { User } from '@/entities/user' +import { classifyBlob } from '@/server/utils/file' +import { Workflow } from '@/entities/workflow' +import mine from 'mime' const MONITOR_INTERVAL = 5000 @@ -144,6 +147,104 @@ export class ComfyPoolInstance { await em.flush() } + private handleImageOutput = async ( + imgBlob: Blob, + info: { + key: string + idx: number + task: WorkflowTask + workflow: Workflow + }, + attachment: AttachmentService, + em: Awaited['getEM']>>> + ) => { + const { key, idx, task, workflow } = info + const imgUtil = new ImageUtil(Buffer.from(await imgBlob.arrayBuffer())) + const [preview, high, raw] = await Promise.all([ + // For thumbnail + imgUtil + .clone() + .resizeMax(1024) + .intoPreviewJPG() + .catch((e) => { + this.logger.w('Error while converting to preview', e) + return null + }), + // For on click into thumbnail preview + imgUtil + .clone() + .intoPreviewJPG() + .catch((e) => { + this.logger.w('Error while converting to preview', e) + return null + }), + // Raw image, use for download + imgUtil.intoPNG() + ]) + const tmpName = `${task.id}_${key}_${idx}.png` + const [uploaded] = await Promise.all([ + attachment.uploadFile(raw, `${tmpName}`), + preview ? attachment.uploadFile(preview, `${tmpName}_preview.jpg`) : Promise.resolve(false), + high ? attachment.uploadFile(high, `${tmpName}_high.jpg`) : Promise.resolve(false) + ]) + if (uploaded) { + const fileInfo = await attachment.getFileURL(tmpName) + const ratio = await imgUtil.getRatio() + const outputAttachment = em.create( + Attachment, + { + fileName: tmpName, + size: raw.byteLength, + storageType: fileInfo?.type === EAttachmentType.LOCAL ? EStorageType.LOCAL : EStorageType.S3, + status: EAttachmentStatus.UPLOADED, + ratio, + task, + workflow + }, + { partial: true } + ) + em.persist(outputAttachment) + return outputAttachment.id + } + } + + private handleVideoOutput = async ( + videoBlob: Blob, + info: { + key: string + idx: number + task: WorkflowTask + workflow: Workflow + }, + attachment: AttachmentService, + em: Awaited['getEM']>>> + ) => { + const { key, idx, task, workflow } = info + const buff = Buffer.from(await videoBlob.arrayBuffer()) + const extension = mine.getExtension(videoBlob.type) || 'mp4' + const tmpName = `${task.id}_${key}_${idx}.${extension}` + + const uploaded = await attachment.uploadFile(buff, `${tmpName}`) + if (uploaded) { + const fileInfo = await attachment.getFileURL(tmpName) + const outputAttachment = em.create( + Attachment, + { + fileName: tmpName, + size: buff.byteLength, + type: EValueType.Video, + storageType: fileInfo?.type === EAttachmentType.LOCAL ? EStorageType.LOCAL : EStorageType.S3, + status: EAttachmentStatus.UPLOADED, + task, + workflow + }, + { partial: true } + ) + em.persist(outputAttachment) + return outputAttachment.id + } + } + private async pickingJob() { const pool = this.pool const em = await MikroORMInstance.getInstance().getEM() @@ -215,6 +316,7 @@ export class ComfyPoolInstance { builder.input(key, String(inputData)) break case EValueType.File: + case EValueType.Video: case EValueType.Image: const attachmentId = inputData as string const file = await em.findOneOrFail(Attachment, { id: attachmentId }) @@ -243,7 +345,7 @@ export class ComfyPoolInstance { break } } - + console.log(JSON.stringify(builder.workflow)) return new CallWrapper(api, builder) .onPending(async () => { await this.updateTaskEventFn(task, ETaskStatus.Running, { @@ -289,56 +391,21 @@ export class ComfyPoolInstance { if (Array.isArray(tmpOutput[key])) { tmpOutput[key] = (await Promise.all( tmpOutput[key].map(async (v, idx) => { + console.log('out', key, v) if (v instanceof Blob) { - const imgUtil = new ImageUtil(Buffer.from(await v.arrayBuffer())) - const [preview, high, raw] = await Promise.all([ - // For thumbnail - imgUtil - .clone() - .resizeMax(1024) - .intoPreviewJPG() - .catch((e) => { - this.logger.w('Error while converting to preview', e) - return null - }), - // For on click into thumbnail preview - imgUtil - .clone() - .intoPreviewJPG() - .catch((e) => { - this.logger.w('Error while converting to preview', e) - return null - }), - // Raw image, use for download - imgUtil.intoPNG() - ]) - const tmpName = `${task.id}_${key}_${idx}.png` - const [uploaded] = await Promise.all([ - attachment.uploadFile(raw, `${tmpName}`), - preview - ? attachment.uploadFile(preview, `${tmpName}_preview.jpg`) - : Promise.resolve(false), - high ? attachment.uploadFile(high, `${tmpName}_high.jpg`) : Promise.resolve(false) - ]) - if (uploaded) { - const fileInfo = await attachment.getFileURL(tmpName) - const ratio = await imgUtil.getRatio() - const outputAttachment = em.create( - Attachment, - { - fileName: tmpName, - size: raw.byteLength, - storageType: - fileInfo?.type === EAttachmentType.LOCAL ? EStorageType.LOCAL : EStorageType.S3, - status: EAttachmentStatus.UPLOADED, - ratio, - task, - workflow - }, - { partial: true } - ) - em.persist(outputAttachment) - return outputAttachment.id + // Check if v is Video, Image or others + const blobType = classifyBlob(v) + switch (blobType) { + case 'image': { + await this.handleImageOutput(v, { key, idx, task, workflow }, attachment, em) + break + } + case 'video': { + await this.handleVideoOutput(v, { key, idx, task, workflow }, attachment, em) + break + } + default: { + } } } return v diff --git a/utils/workflow.ts b/utils/workflow.ts index a1cff9c..92b9980 100644 --- a/utils/workflow.ts +++ b/utils/workflow.ts @@ -92,10 +92,32 @@ export const parseOutput = async (api: ComfyApi, workflow: Workflow, data: any) } break case EValueType.Image: - if (!tmp && 'images' in output) { - const { images } = output - tmp = await Promise.all(images.map((img: any) => api.getImage(img))) - break + if (tmp) { + if (Array.isArray(tmp)) { + tmp = await Promise.all(tmp.map((img: any) => api.getImage(img))) + } else { + tmp = await api.getImage(tmp) + } + } else { + if ('images' in output) { + const { images } = output + tmp = await Promise.all(images.map((img: any) => api.getImage(img))) + break + } + } + case EValueType.Video: + if (tmp) { + if (Array.isArray(tmp)) { + tmp = await Promise.all(tmp.map((img: any) => api.getImage(img))) + } else { + tmp = await api.getImage(tmp) + } + } else { + if ('gifs' in output) { + const { gifs } = output + tmp = await Promise.all(gifs.map((img: any) => api.getImage(img))) + break + } } case EValueType.File: if (tmp) {