jbilcke-hf HF staff commited on
Commit
f4af987
β€’
1 Parent(s): 4f64da5

work in progress

Browse files
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
src/app/agents/{ant.ts β†’ city.ts} RENAMED
@@ -2,40 +2,62 @@ import { pick } from "./pick"
2
  import { Agent, Scene } from "./types"
3
 
4
  const actions = [
5
- "working on lavae",
6
- "slicing leaves",
7
- "attacking a beetle",
8
- "foraging",
9
- "cutting a sugar cube",
10
- "collecting sugar",
11
- "collecting aphids"
12
  ]
13
 
14
  const positions = [
15
- "on a leave",
16
- "on a tree branch",
17
- "on sand",
18
- "on the ground"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  ]
20
 
21
  export const agent: Agent = {
22
- title: "Ant",
23
- type: "ant",
24
  simulate: (): Scene => {
25
  const action = pick(actions)
26
  const position = pick(positions)
 
27
 
28
  const prompt = [
29
- `close-up shot of a couple of ants`,
30
  action,
31
  position,
 
 
 
32
  `high res`,
33
- `documentary`,
34
  ].join(", ")
35
 
36
  return {
37
  action,
38
  position,
 
 
39
  prompt
40
  }
41
  }
 
2
  import { Agent, Scene } from "./types"
3
 
4
  const actions = [
5
+ "busy pedestrians",
6
+ "busy traffic",
7
+ "typical street life",
8
+ "skyscrapper being constructed",
9
+ "a building is on fire",
 
 
10
  ]
11
 
12
  const positions = [
13
+ "city center with skyscrappers",
14
+ "city center with a hospital",
15
+ "market area",
16
+ "residential area with small houses",
17
+ "residential area and houses with pools",
18
+ "industrial area with a smoking factory",
19
+ "beachfront area with villas",
20
+ "theme park with one big rollercoaster"
21
+ ]
22
+
23
+ const lights = [
24
+ "during the day",
25
+ // "during the night",
26
+ ]
27
+
28
+ const actionnables = [
29
+ "building",
30
+ "road",
31
+ "car",
32
+ "tower",
33
+ "tree",
34
+ "river",
35
+ "sea"
36
  ]
37
 
38
  export const agent: Agent = {
39
+ title: "City",
40
+ type: "city",
41
  simulate: (): Scene => {
42
  const action = pick(actions)
43
  const position = pick(positions)
44
+ const light = pick(lights)
45
 
46
  const prompt = [
47
+ `static isometrical view of 3D rendered city`,
48
  action,
49
  position,
50
+ light,
51
+ `isometric`,
52
+ `game`,
53
  `high res`,
 
54
  ].join(", ")
55
 
56
  return {
57
  action,
58
  position,
59
+ light,
60
+ actionnables,
61
  prompt
62
  }
63
  }
src/app/agents/{fox.ts β†’ dungeon.ts} RENAMED
@@ -2,42 +2,67 @@ import { pick } from "./pick"
2
  import { Agent, Scene } from "./types"
3
 
4
  const actions = [
5
- "waiting",
6
- "jumping",
7
- "eating a mouse",
8
- "looking at camera",
9
- "touch a rock",
10
- "touching grass",
11
- "drinking from a water hole"
12
  ]
13
 
14
  const positions = [
15
- "in the forest",
16
- "in a plain",
17
- "in front of a fox hole",
18
- "in front of a bush"
 
 
 
 
19
  ]
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  export const agent: Agent = {
23
- title: "Fox",
24
- type: "fox",
25
  simulate: (): Scene => {
26
  const action = pick(actions)
27
  const position = pick(positions)
 
28
 
29
  const prompt = [
30
- `medium shot of a fox`,
31
  action,
32
  position,
33
- `high res`,
 
 
34
  `documentary`,
 
35
  ].join(", ")
36
 
37
  return {
38
  action,
39
  position,
 
 
40
  prompt
41
  }
42
  }
43
- }
 
2
  import { Agent, Scene } from "./types"
3
 
4
  const actions = [
5
+ "not moving",
6
+ "walking in",
7
+ "looking up",
8
+ "looking down",
9
+ "looking left",
10
+ "looking right",
11
+ "looking around"
12
  ]
13
 
14
  const positions = [
15
+ "corridor with a beautiful wooden door at the end, wooden floor and stone walls",
16
+ "a beautiful wooden door",
17
+ "beautiful room with stone walls and wooden floor",
18
+ "large ball room with stone pillars, stone floor and red carpet",
19
+ "a cosy room with a fireplace, stone walls and wooden floor",
20
+ "a fireplace with stone walls",
21
+ "a cold dungeon with stone walls",
22
+ "a damp medieval jail cell with stone walls and wooden floor"
23
  ]
24
 
25
+ const lights = [
26
+ "lit through windows",
27
+ "lit through wall-mounted torchs"
28
+ // "poorly lit"
29
+ ]
30
+
31
+ const actionnables = [
32
+ "floor",
33
+ "fireplace",
34
+ "door",
35
+ "window",
36
+ "chair",
37
+ "table",
38
+ "torch"
39
+ ]
40
 
41
  export const agent: Agent = {
42
+ title: "Dungeon",
43
+ type: "dungeon",
44
  simulate: (): Scene => {
45
  const action = pick(actions)
46
  const position = pick(positions)
47
+ const light = pick(lights)
48
 
49
  const prompt = [
50
+ `first-person footage`,
51
  action,
52
  position,
53
+ light,
54
+ `medieval`,
55
+ `photography`,
56
  `documentary`,
57
+ `high res`,
58
  ].join(", ")
59
 
60
  return {
61
  action,
62
  position,
63
+ light,
64
+ actionnables,
65
  prompt
66
  }
67
  }
68
+ }
src/app/agents/index.ts CHANGED
@@ -1,11 +1,11 @@
1
  import { Agent, AgentType } from "./types"
2
 
3
- import { agent as ant } from "./ant"
4
- import { agent as fish } from "./fish"
5
- import { agent as fox } from "./fox"
6
 
7
- export const agents = { ant, fish, fox }
8
 
9
- export const defaultAgent: AgentType = "fox"
10
 
11
  export const getAgent = (type?: AgentType) => agents[type || defaultAgent] || agents[defaultAgent]
 
1
  import { Agent, AgentType } from "./types"
2
 
3
+ import { agent as pirates } from "./pirates"
4
+ import { agent as city } from "./city"
5
+ import { agent as dungeon } from "./dungeon"
6
 
7
+ export const agents = { pirates, city, dungeon }
8
 
9
+ export const defaultAgent: AgentType = "pirates"
10
 
11
  export const getAgent = (type?: AgentType) => agents[type || defaultAgent] || agents[defaultAgent]
src/app/agents/{fish.ts β†’ pirates.ts} RENAMED
@@ -21,24 +21,51 @@ const positions = [
21
  "hiding in the coral"
22
  ]
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  export const agent: Agent = {
25
- title: "Fish",
26
- type: "fish",
27
  simulate: (): Scene => {
28
  const action = pick(actions)
29
  const position = pick(positions)
 
30
 
 
 
31
  const prompt = [
32
- `medium shot of a clownfish`,
33
- action,
34
- position,
35
- `in front of yellow coral`,
36
- `high res underwater footage`,
 
 
37
  ].join(", ")
38
 
39
  return {
40
  action,
41
  position,
 
 
42
  prompt
43
  }
44
  }
 
21
  "hiding in the coral"
22
  ]
23
 
24
+ const lights = [
25
+ "during the day",
26
+ ]
27
+
28
+ const actionnables = [
29
+ "chest",
30
+ // "door",
31
+ // "window",
32
+ // "sail",
33
+ // "capstan",
34
+ // "ship's wheel",
35
+ // "hat",
36
+ // "barrel",
37
+ // "cannon",
38
+ // "rope",
39
+ // "bucket",
40
+ "parrot",
41
+ // "wooden leg"
42
+ ]
43
+
44
  export const agent: Agent = {
45
+ title: "Pirates",
46
+ type: "pirates",
47
  simulate: (): Scene => {
48
  const action = pick(actions)
49
  const position = pick(positions)
50
+ const light = pick(lights)
51
 
52
+ // this prompt is beautiful:
53
+ // screenshot from an adventure videogame, inside the hold of a pirate ship, with a pirate chest in the center, at sunset, beautiful, award winning, unreal engine, intricate details
54
  const prompt = [
55
+ `screenshot from an adventure videogame`,
56
+ `inside the hold of a pirate ship`,
57
+ `with a pirate chest in the center`,
58
+ `a parrot`,
59
+ `and a wooden leg`,
60
+ `at sunset`,
61
+ `unreal engine`,
62
  ].join(", ")
63
 
64
  return {
65
  action,
66
  position,
67
+ light,
68
+ actionnables,
69
  prompt
70
  }
71
  }
src/app/agents/types.ts CHANGED
@@ -1,8 +1,10 @@
1
- export type AgentType = 'ant' | 'fish' | 'fox'
2
 
3
  export interface Scene {
4
  action: string
5
  position: string
 
 
6
  prompt: string
7
  }
8
 
 
1
+ export type AgentType = 'pirates' | 'city' | 'dungeon'
2
 
3
  export interface Scene {
4
  action: string
5
  position: string
6
+ light: string
7
+ actionnables: string[]
8
  prompt: string
9
  }
10
 
src/app/layout.tsx CHANGED
@@ -5,8 +5,8 @@ import { Inter } from 'next/font/google'
5
  const inter = Inter({ subsets: ['latin'] })
6
 
7
  export const metadata: Metadata = {
8
- title: 'VideoQuest πŸŽžπŸ›‘',
9
- description: 'VideoQuest',
10
  }
11
 
12
  export default function RootLayout({
 
5
  const inter = Inter({ subsets: ['latin'] })
6
 
7
  export const metadata: Metadata = {
8
+ title: 'Generative AI Adventure',
9
+ description: 'Generative AI Adventure',
10
  }
11
 
12
  export default function RootLayout({
src/app/main.tsx CHANGED
@@ -2,7 +2,7 @@
2
 
3
  import { useEffect, useRef, useState, useTransition } from "react"
4
 
5
- import { VideoPlayer } from "@/components/business/video-player"
6
 
7
  import {
8
  Select,
@@ -15,20 +15,25 @@ import {
15
  import { render } from "./render"
16
  import { Agent, AgentType, Scene } from "./agents/types"
17
  import { agents, defaultAgent, getAgent } from "./agents"
 
18
 
19
  export default function Main() {
20
- const [url, setUrl] = useState<string>()
21
  const [isPending, startTransition] = useTransition()
22
- const [agent, setAgent] = useState<Agent>()
23
  const [scene, setScene] = useState<Scene>()
 
 
 
 
 
 
24
  const ref = useRef<AgentType>(defaultAgent)
25
 
26
  useEffect(() => {
27
 
28
  const updateView = async () => {
29
- console.log(`update view..`)
30
 
31
- startTransition(async () => {
32
 
33
  // console.log(`getting agent..`)
34
  const type = ref?.current
@@ -38,19 +43,27 @@ export default function Main() {
38
  const scene = agent.simulate()
39
 
40
  // console.log(`rendering scene..`)
41
- const newUrl = await render(scene.prompt)
 
 
 
42
 
43
  if (type !== ref?.current) {
44
- console.log("agent type changed while we were rendering")
45
  setTimeout(() => { updateView() }, 0)
46
  return
47
  }
48
 
49
- // console.log(`newUrl: ${newUrl}`)
50
- setUrl(newUrl)
51
- setAgent(agent)
52
- setScene(scene)
53
- setTimeout(() => { updateView()}, 2000)
 
 
 
 
 
54
  })
55
  }
56
 
@@ -62,12 +75,17 @@ export default function Main() {
62
  <div className="flex flex-col w-full pt-4">
63
  <div className="flex flex-col space-y-3 px-2">
64
  <div className="flex flex-row items-center space-x-3">
65
- <label className="flex">Agent model:</label>
66
  <Select
67
  defaultValue={defaultAgent}
68
  onValueChange={(value) => {
69
  ref.current = value as AgentType
70
- setUrl("")
 
 
 
 
 
71
  }}>
72
  <SelectTrigger className="w-[180px]">
73
  <SelectValue placeholder="Type" />
@@ -79,12 +97,21 @@ export default function Main() {
79
  </SelectContent>
80
  </Select>
81
  </div>
82
- {(url && scene) ? <div>
 
 
83
  <p>Action: {scene.action}</p>
84
  <p>Position: {scene.position}</p>
 
85
  </div> : null}
 
 
 
 
 
 
86
  </div>
87
- <VideoPlayer url={url} />
88
  </div>
89
  )
90
  }
 
2
 
3
  import { useEffect, useRef, useState, useTransition } from "react"
4
 
5
+ import { ImageRenderer } from "@/components/business/image-renderer"
6
 
7
  import {
8
  Select,
 
15
  import { render } from "./render"
16
  import { Agent, AgentType, Scene } from "./agents/types"
17
  import { agents, defaultAgent, getAgent } from "./agents"
18
+ import { ImageSegment, RenderedScene } from "./types"
19
 
20
  export default function Main() {
 
21
  const [isPending, startTransition] = useTransition()
 
22
  const [scene, setScene] = useState<Scene>()
23
+ const [rendered, setRendered] = useState<RenderedScene>({
24
+ assetUrl: "",
25
+ error: "",
26
+ maskBase64: "",
27
+ segments:[]
28
+ })
29
  const ref = useRef<AgentType>(defaultAgent)
30
 
31
  useEffect(() => {
32
 
33
  const updateView = async () => {
34
+ // console.log(`update view..`)
35
 
36
+ await startTransition(async () => {
37
 
38
  // console.log(`getting agent..`)
39
  const type = ref?.current
 
43
  const scene = agent.simulate()
44
 
45
  // console.log(`rendering scene..`)
46
+ const newRendered = await render(
47
+ scene.prompt,
48
+ scene.actionnables.slice(0, 5) // too many can slow us down it seems
49
+ )
50
 
51
  if (type !== ref?.current) {
52
+ console.log("agent type changed! reloading scene")
53
  setTimeout(() => { updateView() }, 0)
54
  return
55
  }
56
 
57
+
58
+ if (newRendered.assetUrl) {
59
+ setRendered(newRendered)
60
+ // console.log(`got a new url: ${newUrl}`)
61
+ setScene(scene)
62
+ setTimeout(() => { updateView()}, 1000)
63
+ } else {
64
+ // console.log(`going to wait a bit more: ${newUrl}`)
65
+ setTimeout(() => { updateView()}, newRendered.error ? 6000 : 3000)
66
+ }
67
  })
68
  }
69
 
 
75
  <div className="flex flex-col w-full pt-4">
76
  <div className="flex flex-col space-y-3 px-2">
77
  <div className="flex flex-row items-center space-x-3">
78
+ <label className="flex">Select a game:</label>
79
  <Select
80
  defaultValue={defaultAgent}
81
  onValueChange={(value) => {
82
  ref.current = value as AgentType
83
+ setRendered({
84
+ assetUrl: "",
85
+ error: "",
86
+ maskBase64: "",
87
+ segments:[]
88
+ })
89
  }}>
90
  <SelectTrigger className="w-[180px]">
91
  <SelectValue placeholder="Type" />
 
97
  </SelectContent>
98
  </Select>
99
  </div>
100
+ <p>Note: changing the model might take up to 1 minute</p>
101
+
102
+ {(scene) ? <div>
103
  <p>Action: {scene.action}</p>
104
  <p>Position: {scene.position}</p>
105
+ <p>Light: {scene.light}</p>
106
  </div> : null}
107
+ <div className="flex flex-col">
108
+ {rendered.segments.map((segment, i) =>
109
+ <div key={i}>
110
+ {segment.label} ({segment.score})
111
+ </div>)}
112
+ </div>
113
  </div>
114
+ <ImageRenderer rendered={rendered} />
115
  </div>
116
  )
117
  }
src/app/render.ts CHANGED
@@ -1,10 +1,19 @@
1
  "use server"
2
 
 
 
3
  // note: there is no / at the end in the variable
4
  // so we have to add it ourselves if needed
5
  const apiUrl = process.env.RENDERING_ENGINE_API
6
 
7
- export async function render(prompt: string) {
 
 
 
 
 
 
 
8
  try {
9
  console.log(`calling ${apiUrl}/render with prompt: ${prompt}`)
10
  const res = await fetch(`${apiUrl}/render`, {
@@ -14,12 +23,20 @@ export async function render(prompt: string) {
14
  "Content-Type": "application/json",
15
  // Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
16
  },
17
- body: JSON.stringify({ prompt }),
 
 
 
 
 
 
 
18
  cache: 'no-store',
19
  // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
20
  // next: { revalidate: 1 }
21
  })
22
 
 
23
  // The return value is *not* serialized
24
  // You can return Date, Map, Set, etc.
25
 
@@ -29,11 +46,11 @@ export async function render(prompt: string) {
29
  throw new Error('Failed to fetch data')
30
  }
31
 
32
- const { url } = await res.json()
33
-
34
- return url
35
  } catch (err) {
36
  console.error(err)
37
- return ""
38
  }
39
  }
 
1
  "use server"
2
 
3
+ import { RenderedScene } from "./types"
4
+
5
  // note: there is no / at the end in the variable
6
  // so we have to add it ourselves if needed
7
  const apiUrl = process.env.RENDERING_ENGINE_API
8
 
9
+ export async function render(prompt: string, actionnables: string[] = []) {
10
+ let defaulResult: RenderedScene = {
11
+ assetUrl: "",
12
+ maskBase64: "",
13
+ error: "",
14
+ segments: []
15
+ }
16
+
17
  try {
18
  console.log(`calling ${apiUrl}/render with prompt: ${prompt}`)
19
  const res = await fetch(`${apiUrl}/render`, {
 
23
  "Content-Type": "application/json",
24
  // Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
25
  },
26
+ body: JSON.stringify({
27
+ prompt,
28
+ // nbFrames: 8 and nbSteps: 15 --> ~10 sec generation
29
+ nbFrames: 1, // when nbFrames is 1, we will only generate static images
30
+ nbSteps: 20,
31
+ actionnables,
32
+ segmentation: "firstframe", // one day we will remove this param, to make it automatic
33
+ }),
34
  cache: 'no-store',
35
  // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
36
  // next: { revalidate: 1 }
37
  })
38
 
39
+ // console.log("res:", res)
40
  // The return value is *not* serialized
41
  // You can return Date, Map, Set, etc.
42
 
 
46
  throw new Error('Failed to fetch data')
47
  }
48
 
49
+ const response = (await res.json()) as RenderedScene
50
+ // console.log("response:", response)
51
+ return response
52
  } catch (err) {
53
  console.error(err)
54
+ return defaulResult
55
  }
56
  }
src/app/types.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ export interface RenderRequest {
3
+ prompt: string
4
+
5
+ // whether to use video segmentation
6
+ // disabled (default)
7
+ // firstframe: we only analyze the first frame
8
+ // allframes: we analyze all the frames
9
+ segmentation: 'disabled' | 'firstframe' | 'allframes'
10
+
11
+ // segmentation will only be executed if we have a non-empty list of actionnables
12
+ // actionnables are names of things like "chest", "key", "tree", "chair" etc
13
+ actionnables: string[]
14
+
15
+ // note: this is the number of frames for Zeroscope,
16
+ // which is currently configured to only output 3 seconds, so:
17
+ // nbFrames=8 -> 1 sec
18
+ // nbFrames=16 -> 2 sec
19
+ // nbFrames=24 -> 3 sec
20
+ nbFrames: number // min: 1, max: 24
21
+
22
+ nbSteps: number // min: 1, max: 50
23
+
24
+ seed: number
25
+ }
26
+
27
+ export interface ImageSegment {
28
+ id: number
29
+ box: number[]
30
+ color: number[]
31
+ label: string
32
+ score: number
33
+ }
34
+
35
+ export interface RenderedScene {
36
+ assetUrl: string
37
+ error: string
38
+ maskBase64: string
39
+ segments: ImageSegment[]
40
+ }
src/components/business/image-renderer.tsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef } from "react"
2
+
3
+ import { ImageSegment, RenderedScene } from "@/app/types"
4
+
5
+ export const ImageRenderer = ({
6
+ assetUrl = "",
7
+ maskBase64 = "",
8
+ segments = []
9
+ }: RenderedScene) => {
10
+ const imgRef = useRef<HTMLImageElement | null>(null)
11
+ const canvas = document.createElement('canvas')
12
+ const context = canvas.getContext('2d')
13
+
14
+ const getPixelColor = (x: number, y: number) => {
15
+ console.log("getting pixel color")
16
+ if (!context) {
17
+ throw new Error("Unable to get context from canvas")
18
+ }
19
+
20
+ const imgData = context.getImageData(x, y, 1, 1).data
21
+
22
+ return `[${imgData[0]},${imgData[1]},${imgData[2]},${imgData[3]/255}]`
23
+ }
24
+
25
+ const handleMouseDown = (event: React.MouseEvent) => {
26
+ const boundingRect = imgRef.current!.getBoundingClientRect()
27
+
28
+ const x = event.clientX - boundingRect.left
29
+ const y = event.clientY - boundingRect.top
30
+
31
+ if (maskBase64) {
32
+ const image = new Image()
33
+
34
+ image.onload = function () {
35
+ if (context) {
36
+ context.drawImage(image, 0, 0)
37
+
38
+ const clickedColor = getPixelColor(x, y) as any
39
+
40
+ let closestSegment: ImageSegment | null = null
41
+
42
+ let minDistance = Infinity
43
+
44
+ segments.forEach(segment => {
45
+ const segmentColor = segment.color.slice(0,3) // get the RGB part only
46
+
47
+ const distance = Math.sqrt(
48
+ Math.pow(clickedColor[0] - segmentColor[0], 2) +
49
+ Math.pow(clickedColor[1] - segmentColor[1], 2) +
50
+ Math.pow(clickedColor[2] - segmentColor[2], 2)
51
+ )
52
+
53
+ if(distance < minDistance) {
54
+ minDistance = distance;
55
+ closestSegment = segment;
56
+ }
57
+ })
58
+
59
+ console.log("closestSegment:", closestSegment)
60
+ console.log(closestSegment) // Here is the closest matching segment
61
+ }
62
+ }
63
+
64
+ image.src = maskBase64
65
+ // image.src = "data:image/png;base64," + maskBase64;
66
+ } else {
67
+ console.log("No mask available, aborting..")
68
+ }
69
+ }
70
+
71
+
72
+ if (!assetUrl) {
73
+ return <div className="flex w-full h-screen items-center justify-center text-center">
74
+ <div>Rendering first frame.. (might take around 30s)</div>
75
+ </div>
76
+ }
77
+
78
+ return (
79
+ <div className="w-full py-8 px-2">
80
+ <img
81
+ src={assetUrl}
82
+ ref={imgRef}
83
+ className="w-full rounded-md overflow-hidden"
84
+ onMouseDown={handleMouseDown}
85
+ />
86
+ </div>
87
+ )
88
+ }
src/components/business/{video-player.tsx β†’ video-renderer.tsx} RENAMED
@@ -1,10 +1,10 @@
1
  "use client"
2
 
3
- export const VideoPlayer = ({ url }: { url?: string }) => {
4
 
5
  if (!url) {
6
  return <div className="flex w-full h-screen items-center justify-center text-center">
7
- <div>Generating first frames.. (might take around 30s)</div>
8
  </div>
9
  }
10
 
 
1
  "use client"
2
 
3
+ export const VideoRenderer = ({ url }: { url?: string }) => {
4
 
5
  if (!url) {
6
  return <div className="flex w-full h-screen items-center justify-center text-center">
7
+ <div>Rendering first frames.. (might take around 30s)</div>
8
  </div>
9
  }
10