|
"use client" |
|
|
|
import React, { useState, useTransition } from "react" |
|
import { ClapProject, ClapSegmentCategory, getClapAssetSourceType, newEntity, updateClap } from "@aitube/clap" |
|
|
|
import { logImage } from "@/lib/utils" |
|
import { useIsBusy, useStoryPromptDraft } from "@/lib/hooks" |
|
import { isRateLimitError } from "@/lib/utils/isRateLimitError" |
|
import { useToast } from "@/components/ui/use-toast" |
|
|
|
import { createClap } from "@/app/server/aitube/createClap" |
|
import { editClapEntities } from "@/app/server/aitube/editClapEntities" |
|
import { editClapDialogues } from "@/app/server/aitube/editClapDialogues" |
|
import { editClapStoryboards } from "@/app/server/aitube/editClapStoryboards" |
|
import { editClapSounds } from "@/app/server/aitube/editClapSounds" |
|
import { editClapMusic } from "@/app/server/aitube/editClapMusic" |
|
import { editClapVideos } from "@/app/server/aitube/editClapVideos" |
|
import { exportClapToVideo } from "@/app/server/aitube/exportClapToVideo" |
|
|
|
import { useStore } from "../../app/store" |
|
import { useOAuth } from "../oauth/useOAuth" |
|
|
|
export function useProcessors() { |
|
const [isLocked, setLocked] = useState(false) |
|
|
|
const { storyPromptDraft, setStoryPromptDraft, promptDraftRef } = useStoryPromptDraft() |
|
|
|
const [_isPending, startTransition] = useTransition() |
|
|
|
const mainCharacterImage = useStore(s => s.mainCharacterImage) |
|
const mainCharacterVoice = useStore(s => s.mainCharacterVoice) |
|
|
|
const currentClap = useStore(s => s.currentClap) |
|
const setStoryPrompt = useStore(s => s.setStoryPrompt) |
|
const setMainCharacterImage = useStore(s => s.setMainCharacterImage) |
|
const setMainCharacterVoice = useStore(s => s.setMainCharacterVoice) |
|
const setStatus = useStore(s => s.setStatus) |
|
const setShowAuthWall = useStore(s => s.setShowAuthWall) |
|
|
|
const error = useStore(s => s.error) |
|
const setError = useStore(s => s.setError) |
|
const setParseGenerationStatus = useStore(s => s.setParseGenerationStatus) |
|
const setStoryGenerationStatus = useStore(s => s.setStoryGenerationStatus) |
|
const setAssetGenerationStatus = useStore(s => s.setAssetGenerationStatus) |
|
const setSoundGenerationStatus = useStore(s => s.setSoundGenerationStatus) |
|
const setMusicGenerationStatus = useStore(s => s.setMusicGenerationStatus) |
|
const setVoiceGenerationStatus = useStore(s => s.setVoiceGenerationStatus) |
|
const setImageGenerationStatus = useStore(s => s.setImageGenerationStatus) |
|
const setVideoGenerationStatus = useStore(s => s.setVideoGenerationStatus) |
|
const setFinalGenerationStatus = useStore(s => s.setFinalGenerationStatus) |
|
const setCurrentClap = useStore(s => s.setCurrentClap) |
|
const setCurrentVideo = useStore(s => s.setCurrentVideo) |
|
const setProgress = useStore(s => s.setProgress) |
|
|
|
const { isLoggedIn, enableOAuthWall } = useOAuth() |
|
|
|
const { isBusy, busyRef } = useIsBusy() |
|
|
|
const { toast } = useToast() |
|
|
|
const generateStory = async (): Promise<ClapProject> => { |
|
|
|
let clap: ClapProject | undefined = undefined |
|
try { |
|
setProgress(0) |
|
|
|
setStatus("generating") |
|
setStoryGenerationStatus("generating") |
|
setStoryPrompt(promptDraftRef.current) |
|
|
|
clap = await createClap({ |
|
prompt: promptDraftRef.current, |
|
orientation: useStore.getState().orientation, |
|
|
|
turbo: false, |
|
}) |
|
|
|
if (!clap) { throw new Error(`failed to create the clap`) } |
|
|
|
if (clap.segments.length <= 1) { throw new Error(`failed to generate more than one segments`) } |
|
|
|
console.log(`generateStory(): received a clap = `, clap) |
|
|
|
console.log(`generateStory(): copying over entities from the previous clap`) |
|
|
|
console.log(`generateStory(): later we can add button(s) to clear the project and/or the character(s)`) |
|
const { currentClap } = useStore.getState() |
|
|
|
clap.entities = Array.isArray(currentClap?.entities) ? currentClap.entities : [] |
|
|
|
setCurrentClap(clap) |
|
setStoryGenerationStatus("finished") |
|
|
|
console.log("---------------- GENERATED STORY ----------------") |
|
console.table(clap.segments, [ |
|
|
|
'endTimeInMs', |
|
|
|
'category', |
|
'prompt' |
|
]) |
|
return clap |
|
} catch (err) { |
|
setStoryGenerationStatus("error") |
|
throw err |
|
} |
|
} |
|
|
|
const generateEntities = async (clap: ClapProject): Promise<ClapProject> => { |
|
try { |
|
|
|
setAssetGenerationStatus("generating") |
|
clap = await editClapEntities({ |
|
clap, |
|
|
|
|
|
turbo: false, |
|
|
|
}).then(r => r.promise) |
|
|
|
if (!clap) { throw new Error(`failed to edit the entities`) } |
|
|
|
console.log(`generateEntities(): received entities = `, clap) |
|
|
|
if (mainCharacterImage) { |
|
|
|
let mainEntity = clap.entities.at(0) |
|
|
|
if (mainEntity) { |
|
console.log(`generateEntities(): replacing the main character's picture..`) |
|
mainEntity.thumbnailUrl = mainCharacterImage |
|
mainEntity.imagePrompt = "" |
|
mainEntity.imageSourceType = getClapAssetSourceType(mainCharacterImage) |
|
mainEntity.imageEngine = "" |
|
mainEntity.imageId = mainCharacterImage |
|
} |
|
} |
|
|
|
setAssetGenerationStatus("finished") |
|
console.log("---------------- GENERATED ENTITIES ----------------") |
|
console.table(clap.entities, [ |
|
'category', |
|
'label', |
|
'imagePrompt', |
|
'appearance' |
|
]) |
|
return clap |
|
} catch (err) { |
|
setAssetGenerationStatus("error") |
|
throw err |
|
} |
|
} |
|
|
|
const generateSounds = async (clap: ClapProject): Promise<ClapProject> => { |
|
try { |
|
|
|
setSoundGenerationStatus("generating") |
|
|
|
clap = await editClapSounds({ |
|
clap, |
|
turbo: false, |
|
}).then(r => r.promise) |
|
|
|
if (!clap) { throw new Error(`failed to edit the sound`) } |
|
|
|
console.log(`generateSounds(): received a clap with sound = `, clap) |
|
setSoundGenerationStatus("finished") |
|
console.log("---------------- GENERATED SOUND ----------------") |
|
console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.SOUND), [ |
|
'endTimeInMs', |
|
'prompt', |
|
'entityId', |
|
]) |
|
return clap |
|
} catch (err) { |
|
setSoundGenerationStatus("error") |
|
throw err |
|
} |
|
} |
|
|
|
const generateMusic = async (clap: ClapProject): Promise<ClapProject> => { |
|
try { |
|
|
|
setMusicGenerationStatus("generating") |
|
|
|
clap = await editClapMusic({ |
|
clap, |
|
turbo: false, |
|
}).then(r => r.promise) |
|
|
|
if (!clap) { throw new Error(`failed to edit the music`) } |
|
|
|
console.log(`generateMusic(): received a clap with music = `, clap) |
|
setMusicGenerationStatus("finished") |
|
console.log("---------------- GENERATED MUSIC ----------------") |
|
console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.MUSIC), [ |
|
'endTimeInMs', |
|
'prompt', |
|
'entityId', |
|
]) |
|
return clap |
|
} catch (err) { |
|
setMusicGenerationStatus("error") |
|
throw err |
|
} |
|
} |
|
|
|
const generateStoryboards = async (clap: ClapProject): Promise<ClapProject> => { |
|
try { |
|
|
|
setImageGenerationStatus("generating") |
|
clap = await editClapStoryboards({ |
|
clap, |
|
|
|
|
|
|
|
turbo: false, |
|
}).then(r => r.promise) |
|
|
|
if (!clap) { throw new Error(`failed to edit the storyboards`) } |
|
|
|
|
|
console.log(`generateStoryboards(): received storyboards = `, clap) |
|
|
|
setImageGenerationStatus("finished") |
|
console.log("---------------- GENERATED STORYBOARDS ----------------") |
|
clap.segments |
|
.filter(s => s.category === ClapSegmentCategory.STORYBOARD) |
|
.forEach((s, i) => { |
|
if (s.status === "completed" && s.assetUrl) { |
|
|
|
logImage(s.assetUrl, 0.35) |
|
} else { |
|
console.log(` [${i}] failed to generate storyboard`) |
|
} |
|
|
|
}) |
|
console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.STORYBOARD), [ |
|
'endTimeInMs', |
|
'prompt', |
|
'assetUrl' |
|
]) |
|
return clap |
|
} catch (err) { |
|
setImageGenerationStatus("error") |
|
throw err |
|
} |
|
} |
|
|
|
const generateVideos = async (clap: ClapProject): Promise<ClapProject> => { |
|
try { |
|
|
|
setVideoGenerationStatus("generating") |
|
|
|
clap = await editClapVideos({ |
|
clap, |
|
turbo: false |
|
}).then(r => r.promise) |
|
|
|
if (!clap) { throw new Error(`failed to edit the videos`) } |
|
|
|
console.log(`handleSubmit(): received individual video clips = `, clap) |
|
setVideoGenerationStatus("finished") |
|
console.log("---------------- GENERATED VIDEOS ----------------") |
|
console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.VIDEO), [ |
|
'endTimeInMs', |
|
'prompt', |
|
'entityId', |
|
]) |
|
return clap |
|
} catch (err) { |
|
setVideoGenerationStatus("error") |
|
throw err |
|
} |
|
} |
|
|
|
const generateStoryboardsThenVideos = async (clap: ClapProject): Promise<ClapProject> => { |
|
clap = await generateStoryboards(clap) |
|
clap = await generateVideos(clap) |
|
return clap |
|
} |
|
|
|
|
|
const generateDialogues = async (clap: ClapProject): Promise<ClapProject> => { |
|
try { |
|
|
|
setVoiceGenerationStatus("generating") |
|
clap = await editClapDialogues({ |
|
clap, |
|
turbo: false, |
|
}).then(r => r.promise) |
|
|
|
if (!clap) { throw new Error(`failed to edit the dialogues`) } |
|
|
|
console.log(`generateDialogues(): received dialogues = `, clap) |
|
setVoiceGenerationStatus("finished") |
|
console.log("---------------- GENERATED DIALOGUES ----------------") |
|
console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.DIALOGUE), [ |
|
'endTimeInMs', |
|
'prompt', |
|
'entityId', |
|
]) |
|
return clap |
|
} catch (err) { |
|
setVoiceGenerationStatus("error") |
|
throw err |
|
} |
|
} |
|
|
|
const generateFinalVideo = async (clap: ClapProject): Promise<string> => { |
|
|
|
let assetUrl = "" |
|
try { |
|
|
|
setFinalGenerationStatus("generating") |
|
assetUrl = await exportClapToVideo({ |
|
clap, |
|
turbo: false, |
|
}) |
|
|
|
setCurrentVideo(assetUrl) |
|
|
|
if (assetUrl.length < 128) { throw new Error(`generateFinalVideo(): the generated video is too small, so we failed`) } |
|
|
|
console.log(`generateFinalVideo(): received a video: ${assetUrl.slice(0, 120)}...`) |
|
setFinalGenerationStatus("finished") |
|
return assetUrl |
|
} catch (err) { |
|
setFinalGenerationStatus("error") |
|
throw err |
|
} |
|
} |
|
|
|
const handleSubmit = async () => { |
|
|
|
if (busyRef.current) { return } |
|
|
|
if (enableOAuthWall && !isLoggedIn) { |
|
setShowAuthWall(true) |
|
return |
|
} |
|
|
|
setStatus("generating") |
|
busyRef.current = true |
|
|
|
startTransition(async () => { |
|
setStatus("generating") |
|
busyRef.current = true |
|
|
|
console.log(`handleSubmit(): generating a clap using prompt = "${promptDraftRef.current}" `) |
|
|
|
try { |
|
let clap = await generateStory() |
|
setCurrentClap(clap) |
|
|
|
const storyboards = clap.segments.filter(s => s.category === ClapSegmentCategory.STORYBOARD) |
|
|
|
let mainCharacter = clap.entities.at(0) |
|
|
|
|
|
|
|
if (mainCharacter) { |
|
console.log(`handleSubmit(): we use the clap's main character's face on all storyboards`) |
|
storyboards.forEach(storyboard => { storyboard.entityId = mainCharacter!.id }) |
|
logImage(mainCharacter.imageId, 0.35) |
|
} else if (mainCharacterImage) { |
|
console.log(`handleSubmit(): declaring a new entity for our main character`) |
|
const entityName = "person" |
|
mainCharacter = newEntity({ |
|
category: ClapSegmentCategory.CHARACTER, |
|
triggerName: entityName, |
|
label: entityName, |
|
description: entityName, |
|
author: "auto", |
|
thumbnailUrl: mainCharacterImage, |
|
|
|
imagePrompt: "", |
|
imageSourceType: getClapAssetSourceType(mainCharacterImage), |
|
imageEngine: "", |
|
imageId: mainCharacterImage, |
|
audioPrompt: "", |
|
}) |
|
|
|
clap.entities.push(mainCharacter!) |
|
console.log(`handleSubmit(): we use the main character's face on all storyboards`) |
|
|
|
storyboards.forEach(storyboard => { storyboard.entityId = mainCharacter!.id }) |
|
logImage(mainCharacterImage, 0.35) |
|
} |
|
|
|
const tasks = [ |
|
generateMusic(clap), |
|
generateStoryboardsThenVideos(clap) |
|
] |
|
|
|
const claps = await Promise.all(tasks) |
|
|
|
console.log(`finished processing ${tasks.length} tasks in parallel`) |
|
|
|
for (const newerClap of claps) { |
|
clap = await updateClap(clap, newerClap, { |
|
overwriteMeta: false, |
|
inlineReplace: true, |
|
}) |
|
setCurrentClap(clap) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log("final clap: ", clap) |
|
setCurrentClap(clap) |
|
await generateFinalVideo(clap) |
|
|
|
setStatus("finished") |
|
setError("") |
|
} catch (err) { |
|
|
|
if (isRateLimitError(err)) { |
|
console.error("Critical error: you are doing too many requests!") |
|
toast({ |
|
title: "You can generate only one video per minute π", |
|
description: "Don't send too many requests at once π€", |
|
}) |
|
return |
|
} else { |
|
console.error(err) |
|
toast({ |
|
title: "We couldn't generate this video π", |
|
description: "We are currently experiencing a surge in traffic, please try later in the day π€", |
|
}) |
|
} |
|
|
|
} |
|
}) |
|
} |
|
|
|
return { |
|
generateDialogues, |
|
generateEntities, |
|
generateFinalVideo, |
|
generateMusic, |
|
generateSounds, |
|
generateStory, |
|
generateStoryboards, |
|
generateStoryboardsThenVideos, |
|
generateVideos, |
|
handleSubmit, |
|
} |
|
} |