Skip to content

Commit

Permalink
feat(output): initial support for video file
Browse files Browse the repository at this point in the history
  • Loading branch information
tctien342 committed Jan 15, 2025
1 parent e88b6fb commit 39da2a5
Show file tree
Hide file tree
Showing 20 changed files with 316 additions and 102 deletions.
2 changes: 1 addition & 1 deletion app/[locale]/main/WorkflowSidePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/main/workflow/TaskItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export const TaskItem: IComponent<{
/>
{runningTime >= 0 && <MiniBadge Icon={Hourglass} title='Take' count={`${runningTime}s`} />}
{task.repeatCount > 1 && <MiniBadge Icon={Repeat} title='Repeat' count={task.repeatCount} />}
{!!attachments?.length && <MiniBadge Icon={Image} title='Images' count={attachments.length} />}
{!!attachments?.length && <MiniBadge Icon={Image} title='Files' count={attachments.length} />}
{task.computedCost > 0 && (
<MiniBadge Icon={DollarSign} title='Credits' count={task.computedCost.toFixed(2)} />
)}
Expand Down
2 changes: 1 addition & 1 deletion components/AttachmentDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
74 changes: 58 additions & 16 deletions components/AttachmentReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -191,6 +195,20 @@ export const AttachmentReview: IComponent<{
'rounded-lg outline outline-white shadow cursor-pointer': isPop
})}
/>
{isPop && (
<m.div className='mt-2' onClick={(e) => e.stopPropagation()}>
{!!image && (
<div className='px-2 py-1 text-xs bg-background/50 border backdrop-blur w-min rounded flex justify-center gap-1'>
{image.type === EValueType.Video ? (
<PlayCircleIcon className='w-4 h-4' />
) : (
<ImageIcon className='w-4 h-4' />
)}
<code>{image?.type}</code>
</div>
)}
</m.div>
)}
</m.div>
{isPop && (
<m.div
Expand Down Expand Up @@ -220,21 +238,32 @@ export const AttachmentReview: IComponent<{
<AttachmentTooltipPopup taskId={taskId} active={hoverSync && isHovering} />
</div>
<div className='w-full mt-2 h-10 flex justify-end gap-2'>
<DropdownMenu>
<DropdownMenuTrigger asChild className='flex items-center'>
<Button variant='secondary' className='bg-background/50 backdrop-blur-lg'>
<code>DOWNLOAD</code> <Download width={16} height={16} className='ml-2' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side='left' className='bg-background/80 backdrop-blur-lg'>
<DropdownMenuItem onClick={() => downloadFn('jpg')} className='cursor-pointer text-sm'>
<span>Download compressed JPG</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => downloadFn()} className='cursor-pointer text-sm'>
<span>Download Raw</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{isVideo ? (
<Button
variant='secondary'
onClick={() => downloadFn()}
className='bg-background/50 backdrop-blur-lg'
>
<code>DOWNLOAD</code> <Download width={16} height={16} className='ml-2' />
</Button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild className='flex items-center'>
<Button variant='secondary' className='bg-background/50 backdrop-blur-lg'>
<code>DOWNLOAD</code> <Download width={16} height={16} className='ml-2' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side='left' className='bg-background/80 backdrop-blur-lg'>
<DropdownMenuItem onClick={() => downloadFn('jpg')} className='cursor-pointer text-sm'>
<span>Download compressed JPG</span>
</DropdownMenuItem>

<DropdownMenuItem onClick={() => downloadFn()} className='cursor-pointer text-sm'>
<span>Download Raw</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{!!onPressFavorite && (
<Button
onClick={() => onPressFavorite?.(data?.id!)}
Expand Down Expand Up @@ -304,6 +333,19 @@ export const AttachmentReview: IComponent<{
</div>
</div>
)}
{!!image && (
<div
className={cn(
'z-10 group-hover:block absolute bottom-2 left-2 bg-black/50 text-white backdrop-blur rounded p-1'
)}
>
{image.type === EValueType.Video ? (
<PlayCircleIcon className='w-4 h-4' />
) : (
<ImageIcon className='w-4 h-4' />
)}
</div>
)}
{!!onPressFavorite && (
<div
className={cn('z-10 group-hover:block absolute top-1 left-1', {
Expand Down
4 changes: 2 additions & 2 deletions components/ImageGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const ImageGallery: IComponent<{
left: `${(virtualRow.lane * 100) / imgPerRow}%`,
width: `calc(100%/${imgPerRow})`,
height: 'fit-content',
aspectRatio: 'loading' in item ? 1 : item.ratio,
aspectRatio: 'loading' in item ? 1 : item.ratio || 1,
transform: `translateY(${virtualRow.start}px)`
}}
>
Expand All @@ -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 && <div className='flex'>More data...</div>}
Expand Down
12 changes: 12 additions & 0 deletions components/dialogs/AddWorkflowDialog/steps/CreateInputNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
LanguageIcon,
ListBulletIcon,
PhotoIcon,
PlayCircleIcon,
PuzzlePieceIcon,
SparklesIcon,
VariableIcon
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -288,6 +294,12 @@ export const CreateInputNode: IComponent<{
Image
</div>
</SelectItem>
<SelectItem value={EValueType.Video}>
<div className='flex items-center'>
<PlayCircleIcon className='mr-2 h-4 w-4' />
Video
</div>
</SelectItem>
<SelectItem value={EValueType.File}>
<div className='flex items-center'>
<DocumentArrowUpIcon className='mr-2 h-4 w-4' />
Expand Down
11 changes: 10 additions & 1 deletion components/dialogs/AddWorkflowDialog/steps/CreateOutputNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
DocumentArrowUpIcon,
LanguageIcon,
PhotoIcon,
SparklesIcon,
PlayCircleIcon,
VariableIcon
} from '@heroicons/react/24/outline'
import { zodResolver } from '@hookform/resolvers/zod'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -192,6 +195,12 @@ export const CreateOutputNode: IComponent<{
Image
</div>
</SelectItem>
<SelectItem value={EValueType.Video}>
<div className='flex items-center'>
<PlayCircleIcon className='mr-2 h-4 w-4' />
Video
</div>
</SelectItem>
<SelectItem value={EValueType.File}>
<div className='flex items-center'>
<DocumentArrowUpIcon className='mr-2 h-4 w-4' />
Expand Down
5 changes: 3 additions & 2 deletions components/dialogs/AddWorkflowDialog/steps/Finalize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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) && (
<DropFileInput
disabled={loading}
defaultFiles={inputWorkflowTest.current[val]}
Expand Down Expand Up @@ -240,6 +240,7 @@ export const FinalizeStep: IComponent = () => {
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(
Expand Down
56 changes: 45 additions & 11 deletions components/ui-ext/download-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -58,11 +85,18 @@ const DownloadImagesButton: IComponent<{
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side='left' align='center'>
<DropdownMenuItem onClick={() => downloadFn('jpg')} className='cursor-pointer text-sm'>
<span>Download compressed JPG</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => downloadFn()} className='cursor-pointer text-sm'>
<span>Download Raw</span>
{haveVideo && (
<DropdownMenuItem onClick={downloadVideos} className='cursor-pointer text-sm'>
<span>Download video files</span>
</DropdownMenuItem>
)}
{haveImage && (
<DropdownMenuItem onClick={downloadCompressedJpg} className='cursor-pointer text-sm'>
<span>Download compressed JPG</span>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={downloadRawOutput} className='cursor-pointer text-sm'>
<span>Download raw output</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
2 changes: 1 addition & 1 deletion entities/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions entities/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export enum EValueType {
File = 'File',
String = 'String',
Number = 'Number',
Video = 'Video',
Image = 'Image',
Boolean = 'Boolean'
}
Expand Down
6 changes: 5 additions & 1 deletion server/handlers/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions server/routers/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion server/routers/snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export const snippetRouter = router({
const inputBody: Record<string, string | number | boolean> = {}
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] = '<attachment_id>'
needUpload = true
continue
Expand Down
Loading

0 comments on commit 39da2a5

Please sign in to comment.