jbilcke-hf HF staff commited on
Commit
40fde09
·
1 Parent(s): f1f03f6

working on the new game engine

Browse files
src/app/engines.ts CHANGED
@@ -1,28 +1,76 @@
1
 
2
- export type EngineType = "image" | "video"
 
 
 
 
 
 
3
 
4
  export interface Engine {
5
  type: EngineType
6
  label: string
7
  modelName: string
8
  modelUrl: string
 
 
9
  }
10
 
11
  export const engines: Record<string, Engine> = {
12
- image: {
13
- type: "image",
14
- label: "Image",
15
- modelName: "SDXL 1.0",
 
 
16
  modelUrl: "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
17
  },
18
- video: {
19
- type: "video",
20
- label: "Video",
21
- modelName: "Zeroscope V2 576w",
 
 
22
  modelUrl: "https://huggingface.co/cerspense/zeroscope_v2_576w",
23
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
- export const defaultEngine: EngineType = "image"
27
 
28
  export const getEngine = (type?: EngineType): Engine => engines[type || defaultEngine] || engines[defaultEngine]
 
1
 
2
+ export type EngineType =
3
+ | "cartesian_image"
4
+ | "cartesian_video"
5
+ | "spherical_image"
6
+ | "spherical_video"
7
+ | "spherical_stereogram"
8
+ | "spherical_stereovideo"
9
 
10
  export interface Engine {
11
  type: EngineType
12
  label: string
13
  modelName: string
14
  modelUrl: string
15
+ visible: boolean
16
+ enabled: boolean
17
  }
18
 
19
  export const engines: Record<string, Engine> = {
20
+ cartesian_image: {
21
+ type: "cartesian_image",
22
+ label: "Cartesian image",
23
+ visible: true,
24
+ enabled: true,
25
+ modelName: "SDXL",
26
  modelUrl: "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
27
  },
28
+ cartesian_video: {
29
+ type: "cartesian_video",
30
+ label: "Cartesian video",
31
+ visible: false,
32
+ enabled: false,
33
+ modelName: "Zeroscope",
34
  modelUrl: "https://huggingface.co/cerspense/zeroscope_v2_576w",
35
+ },
36
+ spherical_image: {
37
+ type: "spherical_image",
38
+ label: "Spherical image",
39
+ visible: true,
40
+ enabled: true,
41
+ modelName: "SDXL 360",
42
+ modelUrl: "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
43
+ },
44
+
45
+ spherical_video: {
46
+ type: "spherical_video",
47
+ label: "Spherical video",
48
+ visible: false,
49
+ enabled: false,
50
+ modelName: "",
51
+ modelUrl: "",
52
+ },
53
+
54
+ spherical_stereogram: {
55
+ type: "spherical_stereogram",
56
+ label: "Spherical stereogram",
57
+ visible: false,
58
+ enabled: false,
59
+ modelName: "SDXL 360",
60
+ modelUrl: "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
61
+ },
62
+
63
+ // A.K.A. the Holy Graal
64
+ spherical_stereovideo: {
65
+ type: "spherical_stereovideo",
66
+ label: "Spherical stereovideo",
67
+ visible: false,
68
+ enabled: false,
69
+ modelName: "",
70
+ modelUrl: "",
71
+ }
72
  }
73
 
74
+ export const defaultEngine: EngineType = "cartesian_image"
75
 
76
  export const getEngine = (type?: EngineType): Engine => engines[type || defaultEngine] || engines[defaultEngine]
src/app/games/city.ts CHANGED
@@ -44,6 +44,7 @@ const initialActionnables = [
44
  export const game: Game = {
45
  title: "City",
46
  type: "city",
 
47
  className: edu.className,
48
  initialSituation,
49
  initialActionnables,
 
44
  export const game: Game = {
45
  title: "City",
46
  type: "city",
47
+ engine: "cartesian_image",
48
  className: edu.className,
49
  initialSituation,
50
  initialActionnables,
src/app/games/doom.ts CHANGED
@@ -17,6 +17,7 @@ const initialActionnables = [
17
  export const game: Game = {
18
  title: "Doom",
19
  type: "doom",
 
20
  className: orbitron.className,
21
  initialSituation,
22
  initialActionnables,
 
17
  export const game: Game = {
18
  title: "Doom",
19
  type: "doom",
20
+ engine: "cartesian_image",
21
  className: orbitron.className,
22
  initialSituation,
23
  initialActionnables,
src/app/games/dungeon.ts CHANGED
@@ -48,6 +48,7 @@ const initialActionnables = [
48
  export const game: Game = {
49
  title: "Dungeon",
50
  type: "dungeon",
 
51
  className: amatic.className,
52
  initialSituation,
53
  initialActionnables,
 
48
  export const game: Game = {
49
  title: "Dungeon",
50
  type: "dungeon",
51
+ engine: "cartesian_image",
52
  className: amatic.className,
53
  initialSituation,
54
  initialActionnables,
src/app/games/enchanters.ts CHANGED
@@ -2,7 +2,7 @@ import { macondo } from "@/lib/fonts"
2
  import { Game } from "./types"
3
 
4
  const initialSituation = [
5
- `looking at a beautiful medieval castle on a lake, with a metallic gate, during golden house, surrounded by mountain, with a flying dragon visible afar`,
6
  ].join(", ")
7
 
8
  const initialActionnables = [
@@ -18,6 +18,7 @@ const initialActionnables = [
18
  export const game: Game = {
19
  title: "Enchanters",
20
  type: "enchanters",
 
21
  className: macondo.className,
22
  initialSituation,
23
  initialActionnables,
 
2
  import { Game } from "./types"
3
 
4
  const initialSituation = [
5
+ `looking at a beautiful medieval castle on a lake, with a metallic gate, during golden hour, surrounded by mountain, with a flying dragon visible afar`,
6
  ].join(", ")
7
 
8
  const initialActionnables = [
 
18
  export const game: Game = {
19
  title: "Enchanters",
20
  type: "enchanters",
21
+ engine: "spherical_image",
22
  className: macondo.className,
23
  initialSituation,
24
  initialActionnables,
src/app/games/pirates.ts CHANGED
@@ -55,6 +55,7 @@ const initialSituation = [
55
  export const game: Game = {
56
  title: "Pirates",
57
  type: "pirates",
 
58
  className: lugrasimo.className,
59
  initialSituation,
60
  initialActionnables,
 
55
  export const game: Game = {
56
  title: "Pirates",
57
  type: "pirates",
58
+ engine: "cartesian_image",
59
  className: lugrasimo.className,
60
  initialSituation,
61
  initialActionnables,
src/app/games/types.ts CHANGED
@@ -1,3 +1,4 @@
 
1
 
2
  export type GameType = "pirates" | "city" | "dungeon" | "doom" | "vernian" | "enchanters"
3
 
@@ -9,6 +10,7 @@ export interface Scene {
9
  export interface Game {
10
  title: string
11
  type: GameType
 
12
  className: string
13
  initialSituation: string
14
  initialActionnables: string[]
 
1
+ import { EngineType } from "../engines"
2
 
3
  export type GameType = "pirates" | "city" | "dungeon" | "doom" | "vernian" | "enchanters"
4
 
 
10
  export interface Game {
11
  title: string
12
  type: GameType
13
+ engine: EngineType
14
  className: string
15
  initialSituation: string
16
  initialActionnables: string[]
src/app/games/vernian.ts CHANGED
@@ -19,17 +19,18 @@ const initialActionnables = [
19
  export const game: Game = {
20
  title: "Vernian",
21
  type: "vernian",
 
22
  className: imfell.className,
23
  initialSituation,
24
  initialActionnables,
25
  getScenePrompt: (situation?: string) => [
26
  `Screenshot from a videogame`,
27
- `steam punk`,
28
  `jules verne architecture and design`,
29
  `mysterious machines and mechanisms`,
30
  `first person`,
31
- `unreal engine`,
32
  situation || initialSituation,
 
33
  ]
34
  }
35
 
 
19
  export const game: Game = {
20
  title: "Vernian",
21
  type: "vernian",
22
+ engine: "spherical_image",
23
  className: imfell.className,
24
  initialSituation,
25
  initialActionnables,
26
  getScenePrompt: (situation?: string) => [
27
  `Screenshot from a videogame`,
28
+ `steam punk decor`,
29
  `jules verne architecture and design`,
30
  `mysterious machines and mechanisms`,
31
  `first person`,
 
32
  situation || initialSituation,
33
+ `unreal engine`,
34
  ]
35
  }
36
 
src/app/main.tsx CHANGED
@@ -14,7 +14,7 @@ import {
14
  SelectValue,
15
  } from "@/components/ui/select"
16
 
17
- import { render } from "./render"
18
 
19
  import { RenderedScene } from "./types"
20
  import { Game, GameType } from "./games/types"
@@ -24,14 +24,18 @@ import { getDialogue } from "@/app/queries/getDialogue"
24
  import { getActionnables } from "@/app/queries/getActionnables"
25
  import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
26
 
 
 
 
 
 
 
 
 
27
  export default function Main() {
28
  const [isPending, startTransition] = useTransition()
29
- const [rendered, setRendered] = useState<RenderedScene>({
30
- assetUrl: "",
31
- error: "",
32
- maskBase64: "",
33
- segments:[]
34
- })
35
  const router = useRouter()
36
  const pathname = usePathname()
37
  const searchParams = useSearchParams()
@@ -41,24 +45,22 @@ export default function Main() {
41
  const [game, setGame] = useState<Game>(getGame(gameRef.current))
42
 
43
  const requestedEngine = (searchParams.get('engine') as EngineType) || defaultEngine
44
- // const engineRef = useRef<EngineType>(requestedEngine)
45
  const [engine, setEngine] = useState<Engine>(getEngine(requestedEngine))
46
 
47
  const [situation, setSituation] = useState("")
48
  const [scene, setScene] = useState("")
49
  const [dialogue, setDialogue] = useState("")
50
  const [hoveredActionnable, setHoveredActionnable] = useState("")
51
- const [isLoading, setLoading] = useState(true)
52
 
53
- const loadNextScene = async (nextSituation?: string, nextActionnables?: string[]) => {
54
- // console.log(`update view..`)
55
- setLoading(true)
56
 
 
 
57
  await startTransition(async () => {
58
  console.log("Rendering a scene for " + game.type)
59
 
60
  // console.log(`rendering scene..`)
61
- const newRendered = await render({
62
  engine,
63
 
64
  // SCENE PROMPT
@@ -71,30 +73,74 @@ export default function Main() {
71
  ).slice(0, 6) // too many can slow us down it seems
72
  })
73
 
 
 
74
  // detect if type game type changed while we were busy
75
  if (game?.type !== gameRef?.current) {
76
  console.log("game type changed! aborting..")
77
  return
78
- }
79
 
80
- if (newRendered.assetUrl) {
81
- // console.log(`got a new url: ${newRendered.assetUrl}`)
82
- setScene(scene)
83
 
84
- setRendered(newRendered)
85
- setLoading(false)
86
- }
87
  })
88
  }
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  useEffect(() => {
91
  loadNextScene()
 
92
  }, [])
93
 
94
  const handleUserAction = async (actionnable: string) => {
95
  console.log("user actionnable:", actionnable)
96
 
97
- setLoading(true)
98
 
99
  // TODO: ask Llama2 what to do about it
100
  // we need a frame and some actionnables,
@@ -128,7 +174,6 @@ export default function Main() {
128
  // todo we could also use useEffect
129
  } catch (err) {
130
  console.error(`failed to get one of the mandatory entites: ${err}`)
131
- setLoading(false)
132
  }
133
  })
134
  }
@@ -138,13 +183,16 @@ export default function Main() {
138
  const handleSelectGame = (newGameType: GameType) => {
139
  gameRef.current = newGameType
140
  setGame(getGame(newGameType))
 
141
  setRendered({
 
 
142
  assetUrl: "",
143
  error: "",
144
  maskBase64: "",
145
  segments:[]
146
  })
147
- setLoading(true)
148
 
149
  const current = new URLSearchParams(Array.from(searchParams.entries()))
150
  current.set("game", newGameType)
@@ -186,7 +234,7 @@ export default function Main() {
186
  <div
187
  className="flex flex-col w-full max-w-5xl"
188
  >
189
- <div className="flex flex-row w-full justify-between items-center px-2 py-2 border-b-1 border-gray-50 bg-gray-800">
190
  <div className="flex flex-row items-center space-x-3 font-mono">
191
  <label className="flex text-sm">Select a story:</label>
192
  <Select
@@ -211,8 +259,10 @@ export default function Main() {
211
  <SelectValue className="text-sm" placeholder="Type" />
212
  </SelectTrigger>
213
  <SelectContent>
214
- {Object.entries(engines).map(([key, engine]) =>
215
- <SelectItem key={key} value={key} disabled={key === "video"}>{engine.label} ({engine.modelName})</SelectItem>
 
 
216
  )}
217
  </SelectContent>
218
  </Select>
@@ -220,12 +270,17 @@ export default function Main() {
220
  </div>
221
 
222
  <div className={[
223
- "flex flex-col w-full pt-4 space-y-3 px-2",
224
  getGame(gameRef.current).className // apply the game theme
225
  ].join(" ")}>
226
  <p className="text-xl">A stable diffusion exploration game. Click on an item to explore a new scene!</p>
227
  <div className="flex flex-row">
228
- <div className="text-xl mr-2">🔎 Clickable items:</div>
 
 
 
 
 
229
  {clickables.map((clickable, i) =>
230
  <div key={i} className="flex flex-row text-xl mr-2">
231
  <div className="">{clickable}</div>
@@ -237,7 +292,7 @@ export default function Main() {
237
  rendered={rendered}
238
  onUserAction={handleUserAction}
239
  onUserHover={setHoveredActionnable}
240
- isLoading={isLoading}
241
  game={game}
242
  engine={engine}
243
  />
 
14
  SelectValue,
15
  } from "@/components/ui/select"
16
 
17
+ import { newRender, getRender } from "./render"
18
 
19
  import { RenderedScene } from "./types"
20
  import { Game, GameType } from "./games/types"
 
24
  import { getActionnables } from "@/app/queries/getActionnables"
25
  import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
26
 
27
+ const getInitialRenderedScene = (): RenderedScene => ({
28
+ renderId: "",
29
+ status: "pending",
30
+ assetUrl: "",
31
+ error: "",
32
+ maskBase64: "",
33
+ segments: []
34
+ })
35
  export default function Main() {
36
  const [isPending, startTransition] = useTransition()
37
+ const [rendered, setRendered] = useState<RenderedScene>(getInitialRenderedScene())
38
+ const renderedRef = useRef<RenderedScene>()
 
 
 
 
39
  const router = useRouter()
40
  const pathname = usePathname()
41
  const searchParams = useSearchParams()
 
45
  const [game, setGame] = useState<Game>(getGame(gameRef.current))
46
 
47
  const requestedEngine = (searchParams.get('engine') as EngineType) || defaultEngine
 
48
  const [engine, setEngine] = useState<Engine>(getEngine(requestedEngine))
49
 
50
  const [situation, setSituation] = useState("")
51
  const [scene, setScene] = useState("")
52
  const [dialogue, setDialogue] = useState("")
53
  const [hoveredActionnable, setHoveredActionnable] = useState("")
 
54
 
55
+ const loopRef = useRef<any>(null)
 
 
56
 
57
+ const loadNextScene = async (nextSituation?: string, nextActionnables?: string[]) => {
58
+
59
  await startTransition(async () => {
60
  console.log("Rendering a scene for " + game.type)
61
 
62
  // console.log(`rendering scene..`)
63
+ renderedRef.current = await newRender({
64
  engine,
65
 
66
  // SCENE PROMPT
 
73
  ).slice(0, 6) // too many can slow us down it seems
74
  })
75
 
76
+ console.log("got the first version of our scene!", renderedRef.current)
77
+
78
  // detect if type game type changed while we were busy
79
  if (game?.type !== gameRef?.current) {
80
  console.log("game type changed! aborting..")
81
  return
82
+ }
83
 
84
+ setScene(scene)
 
 
85
 
86
+ setRendered(renderedRef.current)
 
 
87
  })
88
  }
89
 
90
+ const checkRenderedLoop = async () => {
91
+ // console.log("checkRenderedLoop! rendered:", renderedRef.current)
92
+ clearTimeout(loopRef.current)
93
+ if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
94
+ // console.log("let's try again in a moments")
95
+ loopRef.current = setTimeout(() => checkRenderedLoop(), 200)
96
+ return
97
+ }
98
+
99
+ // console.log("checking rendering..")
100
+ await startTransition(async () => {
101
+ // console.log(`getting latest updated scene..`)
102
+ try {
103
+ if (!renderedRef.current?.renderId) {
104
+ throw new Error(`missing renderId`)
105
+ }
106
+
107
+
108
+ // console.log(`calling getRender(${renderedRef.current.renderId})`)
109
+ const newRendered = await getRender(renderedRef.current.renderId)
110
+ // console.log(`got latest updated scene:`, renderedRef.current)
111
+
112
+ // detect if type game type changed while we were busy
113
+ if (game?.type !== gameRef?.current) {
114
+ console.log("game type changed! aborting..")
115
+ return
116
+ }
117
+
118
+
119
+ const before = JSON.stringify(renderedRef.current)
120
+ const after = JSON.stringify(newRendered)
121
+
122
+ if (after !== before) {
123
+ console.log("updating scene..")
124
+ renderedRef.current = newRendered
125
+ setRendered(renderedRef.current)
126
+ }
127
+ } catch (err) {
128
+ console.error(err)
129
+ }
130
+
131
+ clearTimeout(loopRef.current)
132
+ loopRef.current = setTimeout(() => checkRenderedLoop(), 1000)
133
+ })
134
+ }
135
+
136
  useEffect(() => {
137
  loadNextScene()
138
+ checkRenderedLoop()
139
  }, [])
140
 
141
  const handleUserAction = async (actionnable: string) => {
142
  console.log("user actionnable:", actionnable)
143
 
 
144
 
145
  // TODO: ask Llama2 what to do about it
146
  // we need a frame and some actionnables,
 
174
  // todo we could also use useEffect
175
  } catch (err) {
176
  console.error(`failed to get one of the mandatory entites: ${err}`)
 
177
  }
178
  })
179
  }
 
183
  const handleSelectGame = (newGameType: GameType) => {
184
  gameRef.current = newGameType
185
  setGame(getGame(newGameType))
186
+ /*
187
  setRendered({
188
+ renderId: "",
189
+ status: "pending",
190
  assetUrl: "",
191
  error: "",
192
  maskBase64: "",
193
  segments:[]
194
  })
195
+ */
196
 
197
  const current = new URLSearchParams(Array.from(searchParams.entries()))
198
  current.set("game", newGameType)
 
234
  <div
235
  className="flex flex-col w-full max-w-5xl"
236
  >
237
+ <div className="flex flex-row w-full justify-between items-center px-2 py-2 border-b-1 border-gray-50 dark:border-gray-50 bg-gray-800 dark:bg-gray-800">
238
  <div className="flex flex-row items-center space-x-3 font-mono">
239
  <label className="flex text-sm">Select a story:</label>
240
  <Select
 
259
  <SelectValue className="text-sm" placeholder="Type" />
260
  </SelectTrigger>
261
  <SelectContent>
262
+ {Object.entries(engines)
263
+ .filter(([_, engine]) => engine.visible)
264
+ .map(([key, engine]) =>
265
+ <SelectItem key={key} value={key} disabled={!engine.enabled}>{engine.label} ({engine.modelName})</SelectItem>
266
  )}
267
  </SelectContent>
268
  </Select>
 
270
  </div>
271
 
272
  <div className={[
273
+ "flex flex-col w-full pt-4 space-y-3 px-2 text-gray-50 dark:text-gray-50",
274
  getGame(gameRef.current).className // apply the game theme
275
  ].join(" ")}>
276
  <p className="text-xl">A stable diffusion exploration game. Click on an item to explore a new scene!</p>
277
  <div className="flex flex-row">
278
+ <div className="text-xl mr-2">
279
+ {rendered.segments.length
280
+ ? <span>🔎 Clickable items:</span>
281
+ : <span>⌛ Loading clickable items..</span>
282
+ }
283
+ </div>
284
  {clickables.map((clickable, i) =>
285
  <div key={i} className="flex flex-row text-xl mr-2">
286
  <div className="">{clickable}</div>
 
292
  rendered={rendered}
293
  onUserAction={handleUserAction}
294
  onUserHover={setHoveredActionnable}
295
+ isLoading={rendered.status === "pending"}
296
  game={game}
297
  engine={engine}
298
  />
src/app/render.ts CHANGED
@@ -9,10 +9,9 @@ import { Engine, EngineType } from "./engines"
9
  // so we have to add it ourselves if needed
10
  const apiUrl = process.env.RENDERING_ENGINE_API
11
 
12
-
13
  const cacheDurationInSec = 30 * 60 // 30 minutes
14
 
15
- export async function render({
16
  prompt,
17
  actionnables = [],
18
  engine,
@@ -30,20 +29,39 @@ export async function render({
30
  throw new Error(`cannot call the rendering API without actionnables, aborting..`)
31
  }
32
 
33
- const nbFrames = engine.type === "video" ? 8 : 1
34
 
35
  const cacheKey = `render/${JSON.stringify({ prompt, actionnables, nbFrames, type: engine.type })}`
36
 
37
- return await Gorgon.get(cacheKey, async () => {
 
38
  let defaulResult: RenderedScene = {
 
 
39
  assetUrl: "",
40
  maskBase64: "",
41
- error: "",
42
  segments: []
43
  }
44
 
45
  try {
46
- console.log(`calling ${apiUrl}/render with prompt: ${prompt}`)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  const res = await fetch(`${apiUrl}/render`, {
48
  method: "POST",
49
  headers: {
@@ -55,9 +73,11 @@ export async function render({
55
  prompt,
56
  // nbFrames: 8 and nbSteps: 15 --> ~10 sec generation
57
  nbFrames, // when nbFrames is 1, we will only generate static images
58
- nbSteps: 20,
59
  actionnables,
60
  segmentation: "firstframe", // one day we will remove this param, to make it automatic
 
 
61
  }),
62
  cache: 'no-store',
63
  // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
@@ -79,8 +99,60 @@ export async function render({
79
  return response
80
  } catch (err) {
81
  console.error(err)
82
- Gorgon.clear(cacheKey)
83
  return defaulResult
84
  }
85
- }, cacheDurationInSec * 1000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
 
9
  // so we have to add it ourselves if needed
10
  const apiUrl = process.env.RENDERING_ENGINE_API
11
 
 
12
  const cacheDurationInSec = 30 * 60 // 30 minutes
13
 
14
+ export async function newRender({
15
  prompt,
16
  actionnables = [],
17
  engine,
 
29
  throw new Error(`cannot call the rendering API without actionnables, aborting..`)
30
  }
31
 
32
+ const nbFrames = engine.type === "cartesian_video" ? 8 : 1
33
 
34
  const cacheKey = `render/${JSON.stringify({ prompt, actionnables, nbFrames, type: engine.type })}`
35
 
36
+ // return await Gorgon.get(cacheKey, async () => {
37
+
38
  let defaulResult: RenderedScene = {
39
+ renderId: "",
40
+ status: "error",
41
  assetUrl: "",
42
  maskBase64: "",
43
+ error: "failed to fetch the data",
44
  segments: []
45
  }
46
 
47
  try {
48
+ // console.log(`calling POST ${apiUrl}/render with prompt: ${prompt}`)
49
+
50
+ const isForVideo = nbFrames > 1
51
+
52
+
53
+ console.log("REQUEST:", JSON.stringify({
54
+ prompt,
55
+ // nbFrames: 8 and nbSteps: 15 --> ~10 sec generation
56
+ nbFrames, // when nbFrames is 1, we will only generate static images
57
+ nbSteps: isForVideo ? 20 : 30, // 20 = fast, 30 = better, 50 = best
58
+ actionnables,
59
+ segmentation: "firstframe", // one day we will remove this param, to make it automatic
60
+ width: isForVideo ? 576 : 1024,
61
+ height: isForVideo ? 320 : 512,
62
+ }))
63
+
64
+
65
  const res = await fetch(`${apiUrl}/render`, {
66
  method: "POST",
67
  headers: {
 
73
  prompt,
74
  // nbFrames: 8 and nbSteps: 15 --> ~10 sec generation
75
  nbFrames, // when nbFrames is 1, we will only generate static images
76
+ nbSteps: isForVideo ? 12 : 30, // 20 = fast, 30 = better, 50 = best
77
  actionnables,
78
  segmentation: "firstframe", // one day we will remove this param, to make it automatic
79
+ width: isForVideo ? 576 : 1024,
80
+ height: isForVideo ? 320 : 512,
81
  }),
82
  cache: 'no-store',
83
  // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
 
99
  return response
100
  } catch (err) {
101
  console.error(err)
102
+ // Gorgon.clear(cacheKey)
103
  return defaulResult
104
  }
105
+
106
+ // }, cacheDurationInSec * 1000)
107
+ }
108
+
109
+ export async function getRender(renderId: string) {
110
+ if (!renderId) {
111
+ console.error(`cannot call the rendering API without a renderId, aborting..`)
112
+ throw new Error(`cannot call the rendering API without a renderId, aborting..`)
113
+ }
114
+
115
+ let defaulResult: RenderedScene = {
116
+ renderId: "",
117
+ status: "error",
118
+ assetUrl: "",
119
+ maskBase64: "",
120
+ error: "failed to fetch the data",
121
+ segments: []
122
+ }
123
+
124
+ try {
125
+ console.log(`calling GET ${apiUrl}/render with renderId: ${renderId}`)
126
+ const res = await fetch(`${apiUrl}/render/${renderId}`, {
127
+ method: "GET",
128
+ headers: {
129
+ Accept: "application/json",
130
+ "Content-Type": "application/json",
131
+ // Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
132
+ },
133
+ cache: 'no-store',
134
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
135
+ // next: { revalidate: 1 }
136
+ })
137
+
138
+ // console.log("res:", res)
139
+ // The return value is *not* serialized
140
+ // You can return Date, Map, Set, etc.
141
+
142
+ // Recommendation: handle errors
143
+ if (res.status !== 200) {
144
+ // This will activate the closest `error.js` Error Boundary
145
+ throw new Error('Failed to fetch data')
146
+ }
147
+
148
+ const response = (await res.json()) as RenderedScene
149
+ // console.log("response:", response)
150
+ return response
151
+ } catch (err) {
152
+ console.error(err)
153
+ // Gorgon.clear(cacheKey)
154
+ return defaulResult
155
+ }
156
+
157
+ // }, cacheDurationInSec * 1000)
158
  }
src/app/types.ts CHANGED
@@ -1,3 +1,5 @@
 
 
1
  export interface RenderRequest {
2
  prompt: string
3
 
@@ -21,6 +23,11 @@ export interface RenderRequest {
21
  nbSteps: number // min: 1, max: 50
22
 
23
  seed: number
 
 
 
 
 
24
  }
25
 
26
  export interface ImageSegment {
@@ -31,7 +38,10 @@ export interface ImageSegment {
31
  score: number
32
  }
33
 
 
34
  export interface RenderedScene {
 
 
35
  assetUrl: string
36
  error: string
37
  maskBase64: string
 
1
+ export type ProjectionMode = 'cartesian' | 'spherical'
2
+
3
  export interface RenderRequest {
4
  prompt: string
5
 
 
23
  nbSteps: number // min: 1, max: 50
24
 
25
  seed: number
26
+
27
+ width: number // fixed at 1024 for now
28
+ height: number // fixed at 512 for now
29
+
30
+ projection: ProjectionMode
31
  }
32
 
33
  export interface ImageSegment {
 
38
  score: number
39
  }
40
 
41
+ export type RenderedSceneStatus = 'pending' | 'completed' | 'error'
42
  export interface RenderedScene {
43
+ renderId: string
44
+ status: RenderedSceneStatus
45
  assetUrl: string
46
  error: string
47
  maskBase64: string
src/components/business/cartesian-image.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ForwardedRef, forwardRef } from "react"
2
+
3
+ import { SceneEventHandler } from "./types"
4
+
5
+ export const CartesianImage = forwardRef(({
6
+ src,
7
+ width,
8
+ height,
9
+ onEvent,
10
+ className,
11
+ }: {
12
+ src: string
13
+ width: number | string
14
+ height: number | string
15
+ onEvent: SceneEventHandler
16
+ className?: string
17
+ }, ref: ForwardedRef<HTMLImageElement>) => {
18
+
19
+ const handleEvent = (event: React.MouseEvent<HTMLImageElement, MouseEvent>, isClick: boolean) => {
20
+
21
+ const element = ((ref as any)?.current) as HTMLImageElement
22
+
23
+ if (!element) {
24
+ console.log("element isn't ready")
25
+ return
26
+ }
27
+
28
+ const boundingRect = element.getBoundingClientRect()
29
+ const x = event.clientX - boundingRect.left
30
+ const y = event.clientY - boundingRect.top
31
+
32
+ const eventType = isClick ? "click" : "hover"
33
+ onEvent(eventType, x, y)
34
+ }
35
+
36
+ return (
37
+ <img
38
+ src={src}
39
+ ref={ref}
40
+ width={width}
41
+ height={height}
42
+ className={className}
43
+ onMouseUp={(event) => handleEvent(event, true)}
44
+ onMouseMove={(event) => handleEvent(event, false)}
45
+ />
46
+ )
47
+ })
src/components/business/cartesian-video.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ForwardedRef, forwardRef } from "react"
2
+
3
+ import { SceneEventHandler } from "./types"
4
+
5
+ export const CartesianVideo = forwardRef(({
6
+ src,
7
+ width,
8
+ height,
9
+ onEvent,
10
+ className,
11
+ }: {
12
+ src: string
13
+ width: number | string
14
+ height: number | string
15
+ onEvent: SceneEventHandler
16
+ className?: string
17
+ }, ref: ForwardedRef<HTMLVideoElement>) => {
18
+
19
+ const handleEvent = (event: React.MouseEvent<HTMLVideoElement, MouseEvent>, isClick: boolean) => {
20
+
21
+ const element = ((ref as any)?.current) as HTMLVideoElement
22
+
23
+ if (!element) {
24
+ console.log("element isn't ready")
25
+ return
26
+ }
27
+
28
+ const boundingRect = element.getBoundingClientRect()
29
+ const x = event.clientX - boundingRect.left
30
+ const y = event.clientY - boundingRect.top
31
+
32
+ const eventType = isClick ? "click" : "hover"
33
+ onEvent(eventType, x, y)
34
+ }
35
+
36
+ return (
37
+ <video
38
+ src={src}
39
+ ref={ref}
40
+ muted
41
+ autoPlay
42
+ loop
43
+ width={width}
44
+ height={height}
45
+ className={className}
46
+ onMouseUp={(event) => handleEvent(event, true)}
47
+ onMouseMove={(event) => handleEvent(event, false)}
48
+ />
49
+ )
50
+ })
src/components/business/renderer.tsx CHANGED
@@ -4,6 +4,10 @@ import { ImageSegment, RenderedScene } from "@/app/types"
4
  import { ProgressBar } from "../misc/progress"
5
  import { Game } from "@/app/games/types"
6
  import { Engine, EngineType } from "@/app/engines"
 
 
 
 
7
 
8
  export const Renderer = ({
9
  rendered: {
@@ -106,26 +110,22 @@ export const Renderer = ({
106
  return closestSegment;
107
  }
108
 
109
- const handleMouseEvent = async (event: React.MouseEvent, isClickEvent: boolean = false) => {
110
  if (!contextRef.current) return; // Return early if mask image has not been loaded yet
111
 
112
  if (isLoading) {
113
  // we ignore all user interactions
114
- return false
115
  }
116
 
117
  // sometimes we generate an image, but the segmentation fails
118
  // so if we click anywhere bug there are no segments,
119
  // we inform the rest of the app by passing nothing
120
- if (isClickEvent && segments.length == 0) {
121
  onUserAction("nothing, to trigger a scene reload")
122
  return
123
  }
124
 
125
- const boundingRect = imgRef.current!.getBoundingClientRect();
126
- const x = event.clientX - boundingRect.left;
127
- const y = event.clientY - boundingRect.top;
128
-
129
  const newSegment = getSegmentAt(x, y)
130
 
131
  if (actionnable !== newSegment.label) {
@@ -139,7 +139,7 @@ export const Renderer = ({
139
  setActionnable(newSegment.label)
140
  }
141
 
142
- if (isClickEvent) {
143
  if (!newSegment.label) {
144
  return
145
  }
@@ -212,49 +212,43 @@ export const Renderer = ({
212
  */
213
 
214
  return (
215
- <div className={[
216
- "w-full py-8",
217
- // isLoading ? "animate-pulse" : ""
218
- ].join(" ")
219
- }>
220
- <div className="relative w-[1024px] h-[512px] border-2 border-gray-50 rounded-xl overflow-hidden">
 
 
 
 
221
  {!assetUrl ?
222
  null
223
- : engine.type === "video"
224
- ? <video
225
- src={assetUrl}
226
- ref={imgRef as any}
227
- muted
228
- autoPlay
229
- loop
230
- width="1024px"
231
- height="512px"
232
-
233
- className={
234
- [
235
- // "border-1 border-gray-50",
236
- // "absolute top-0 left-0",
237
- actionnable && !isLoading ? "cursor-pointer" : ""
238
- ].join(" ")
239
- }
240
- onMouseDown={(event) => handleMouseEvent(event, true)}
241
- onMouseMove={handleMouseEvent}
242
- />
243
- : <img
244
- src={assetUrl}
245
- // src={"data:image/png;base64," + maskBase64}
246
- ref={imgRef as any}
247
- width="1024px"
248
- height="512px"
249
- className={
250
- [
251
- // "absolute top-0 left-0",
252
- actionnable && !isLoading ? "cursor-pointer" : ""
253
- ].join(" ")
254
- }
255
- onMouseDown={(event) => handleMouseEvent(event, true)}
256
- onMouseMove={handleMouseEvent}
257
- />}
258
  </div>
259
 
260
  {isLoading
 
4
  import { ProgressBar } from "../misc/progress"
5
  import { Game } from "@/app/games/types"
6
  import { Engine, EngineType } from "@/app/engines"
7
+ import { CartesianImage } from "./cartesian-image"
8
+ import { SceneEventHandler, SceneEventType } from "./types"
9
+ import { CartesianVideo } from "./cartesian-video"
10
+ import { SphericalImage } from "./spherical-image"
11
 
12
  export const Renderer = ({
13
  rendered: {
 
110
  return closestSegment;
111
  }
112
 
113
+ const handleMouseEvent: SceneEventHandler = async (type: SceneEventType, x: number, y: number) => {
114
  if (!contextRef.current) return; // Return early if mask image has not been loaded yet
115
 
116
  if (isLoading) {
117
  // we ignore all user interactions
118
+ return
119
  }
120
 
121
  // sometimes we generate an image, but the segmentation fails
122
  // so if we click anywhere bug there are no segments,
123
  // we inform the rest of the app by passing nothing
124
+ if (type === "click" && segments.length == 0) {
125
  onUserAction("nothing, to trigger a scene reload")
126
  return
127
  }
128
 
 
 
 
 
129
  const newSegment = getSegmentAt(x, y)
130
 
131
  if (actionnable !== newSegment.label) {
 
139
  setActionnable(newSegment.label)
140
  }
141
 
142
+ if (type === "click") {
143
  if (!newSegment.label) {
144
  return
145
  }
 
212
  */
213
 
214
  return (
215
+ <div className="w-full py-8">
216
+ <div
217
+ className={[
218
+ "relative w-full h-[800px] border-2 border-gray-50 rounded-xl overflow-hidden",
219
+ isLoading
220
+ ? "cursor-wait"
221
+ : actionnable
222
+ ? "cursor-pointer"
223
+ : ""
224
+ ].join(" ")}>
225
  {!assetUrl ?
226
  null
227
+ : engine.type === "cartesian_video"
228
+ ? <CartesianVideo
229
+ src={assetUrl}
230
+ ref={imgRef as any}
231
+ width="1024px"
232
+ height="512px"
233
+ onEvent={handleMouseEvent}
234
+ />
235
+ : engine.type === "spherical_image"
236
+ ? <SphericalImage
237
+ src={assetUrl}
238
+ ref={imgRef as any}
239
+ width="1024px"
240
+ height="512px"
241
+ onEvent={handleMouseEvent}
242
+ />
243
+ : <CartesianImage
244
+ src={assetUrl}
245
+ ref={imgRef as any}
246
+ width="1024px"
247
+ height="512px"
248
+ onEvent={handleMouseEvent}
249
+ />
250
+ }
251
+
 
 
 
 
 
 
 
 
 
 
252
  </div>
253
 
254
  {isLoading
src/components/business/spherical-image.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ForwardedRef, forwardRef } from "react"
2
+ import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
3
+ // import { CubemapAdapter } from "react-photo-sphere-viewer"
4
+
5
+ import { SceneEventHandler } from "./types"
6
+
7
+ export const SphericalImage = forwardRef(({
8
+ src,
9
+ width,
10
+ height,
11
+ onEvent,
12
+ className,
13
+ }: {
14
+ src: string
15
+ width: number | string
16
+ height: number | string
17
+ onEvent: SceneEventHandler
18
+ className?: string
19
+ }, ref: ForwardedRef<HTMLImageElement>) => {
20
+
21
+
22
+ return (
23
+ <ReactPhotoSphereViewer
24
+ src={src}
25
+ // ref={ref}
26
+ container=""
27
+ containerClass={className}
28
+ //
29
+ height="800px"
30
+ // height={'100vh'}
31
+ width="100%"
32
+
33
+ defaultZoomLvl={1}
34
+
35
+ onClick={(data, instance) => {
36
+ console.log("on click:")
37
+ const position = data.target.getPosition()
38
+ console.log("position:", position)
39
+ }}
40
+
41
+ onReady={(instance) => {
42
+ console.log("spherical image display is ready")
43
+ /*
44
+ const markersPlugs = instance.getPlugin(MarkersPlugin);
45
+ if (!markersPlugs)
46
+ return;
47
+ markersPlugs.addMarker({
48
+ id: "imageLayer2",
49
+ imageLayer: "drone.png",
50
+ size: { width: 220, height: 220 },
51
+ position: { yaw: '130.5deg', pitch: '-0.1deg' },
52
+ tooltip: "Image embedded in the scene"
53
+ });
54
+ markersPlugs.addEventListener("select-marker", () => {
55
+ console.log("asd");
56
+ });
57
+ */
58
+ }}
59
+
60
+ />
61
+ )
62
+ })
src/components/business/types.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export type SceneEventType = 'hover' | 'click' | 'back'
2
+
3
+ export type SceneEventHandler = (type: SceneEventType, x: number, y: number) => Promise<void>