Spaces:
Running
Running
Commit
β’
f42fa3f
1
Parent(s):
888548a
exhumation of this old project π§
Browse files- .env +2 -0
- .nvmrc +1 -1
- next.config.js +0 -5
- package-lock.json +0 -0
- package.json +20 -26
- src/app/engine/getPanoramaFlux.ts +58 -0
- src/app/engine/getPanoramaSDXL.ts +20 -0
- src/app/engine/render.ts +0 -245
- src/app/engine/upscaleImage.ts +77 -0
- src/app/firehose/{page.tsx β page.txt} +17 -8
- src/app/generate/page.tsx +28 -105
- src/app/interface/display/index.tsx +2 -3
- src/app/interface/spherical-image/index.tsx +11 -180
- src/app/interface/top-menu/index.tsx +2 -10
- src/app/page.tsx +3 -1
- src/app/store/index.ts +5 -15
- src/lib/getInitialRenderedScene.ts +0 -11
- src/types.ts +0 -88
.env
CHANGED
@@ -1,4 +1,6 @@
|
|
1 |
|
|
|
|
|
2 |
# ----------- CENSORSHIP -------
|
3 |
# Due to abuse by users, I've had to add a censorship/fingerprinting mechanism
|
4 |
ENABLE_CENSORSHIP="false"
|
|
|
1 |
|
2 |
+
HF_API_KEY=""
|
3 |
+
|
4 |
# ----------- CENSORSHIP -------
|
5 |
# Due to abuse by users, I've had to add a censorship/fingerprinting mechanism
|
6 |
ENABLE_CENSORSHIP="false"
|
.nvmrc
CHANGED
@@ -1 +1 @@
|
|
1 |
-
v20.
|
|
|
1 |
+
v20.15.1
|
next.config.js
CHANGED
@@ -1,11 +1,6 @@
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
const nextConfig = {
|
3 |
output: 'standalone',
|
4 |
-
|
5 |
-
experimental: {
|
6 |
-
serverActions: true,
|
7 |
-
serverActionsBodySizeLimit: '8mb',
|
8 |
-
},
|
9 |
}
|
10 |
|
11 |
module.exports = nextConfig
|
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
const nextConfig = {
|
3 |
output: 'standalone',
|
|
|
|
|
|
|
|
|
|
|
4 |
}
|
5 |
|
6 |
module.exports = nextConfig
|
package-lock.json
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
package.json
CHANGED
@@ -9,7 +9,8 @@
|
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
12 |
-
"@gradio/client": "1.
|
|
|
13 |
"@photo-sphere-viewer/core": "5.1.7",
|
14 |
"@photo-sphere-viewer/markers-plugin": "5.1.7",
|
15 |
"@photo-sphere-viewer/video-plugin": "5.1.7",
|
@@ -31,46 +32,39 @@
|
|
31 |
"@radix-ui/react-switch": "1.0.3",
|
32 |
"@radix-ui/react-toast": "1.1.4",
|
33 |
"@radix-ui/react-tooltip": "1.0.6",
|
34 |
-
"@react-pdf/renderer": "3.1.12",
|
35 |
"@types/node": "20.4.2",
|
36 |
"@types/react": "18.2.15",
|
37 |
"@types/react-dom": "18.2.7",
|
38 |
"@types/uuid": "9.0.2",
|
39 |
-
"autoprefixer": "10.4.
|
40 |
-
"class-variance-authority": "0.
|
41 |
-
"clsx": "2.
|
42 |
-
"cmdk": "0.
|
43 |
"cookies-next": "2.1.2",
|
44 |
-
"
|
45 |
-
"eslint": "
|
46 |
-
"
|
47 |
-
"
|
48 |
-
"lucide-react": "0.260.0",
|
49 |
-
"next": "13.4.10",
|
50 |
"photo-sphere-viewer-lensflare-plugin": "1.1.1",
|
51 |
"pick": "0.0.1",
|
52 |
-
"postcss": "8.4.
|
53 |
"react": "18.2.0",
|
54 |
"react-circular-progressbar": "2.1.0",
|
55 |
"react-dom": "18.2.0",
|
56 |
"react-photo-sphere-viewer": "3.3.5-psv5.1.4",
|
57 |
-
"
|
58 |
-
"
|
59 |
-
"
|
60 |
-
"
|
61 |
-
"
|
62 |
-
"
|
63 |
-
"
|
64 |
-
"tesseract.js": "4.1.2",
|
65 |
-
"ts-node": "10.9.1",
|
66 |
-
"typescript": "5.1.6",
|
67 |
"usehooks-ts": ".9.1",
|
68 |
"uuid": "9.0.0",
|
69 |
-
"zustand": "4.4
|
70 |
},
|
71 |
"devDependencies": {
|
72 |
"@types/qs": "6.9.7",
|
73 |
-
"@types/react-virtualized": "9.21.22"
|
74 |
-
"@types/sbd": "1.0.3"
|
75 |
}
|
76 |
}
|
|
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
12 |
+
"@gradio/client": "1.5.0",
|
13 |
+
"@huggingface/inference": "^2.8.0",
|
14 |
"@photo-sphere-viewer/core": "5.1.7",
|
15 |
"@photo-sphere-viewer/markers-plugin": "5.1.7",
|
16 |
"@photo-sphere-viewer/video-plugin": "5.1.7",
|
|
|
32 |
"@radix-ui/react-switch": "1.0.3",
|
33 |
"@radix-ui/react-toast": "1.1.4",
|
34 |
"@radix-ui/react-tooltip": "1.0.6",
|
|
|
35 |
"@types/node": "20.4.2",
|
36 |
"@types/react": "18.2.15",
|
37 |
"@types/react-dom": "18.2.7",
|
38 |
"@types/uuid": "9.0.2",
|
39 |
+
"autoprefixer": "10.4.20",
|
40 |
+
"class-variance-authority": "0.7.0",
|
41 |
+
"clsx": "2.1.1",
|
42 |
+
"cmdk": "^1.0.0",
|
43 |
"cookies-next": "2.1.2",
|
44 |
+
"eslint": "^8.57.0",
|
45 |
+
"eslint-config-next": "^14.2.5",
|
46 |
+
"lucide-react": "0.427.0",
|
47 |
+
"next": "14.2.5",
|
|
|
|
|
48 |
"photo-sphere-viewer-lensflare-plugin": "1.1.1",
|
49 |
"pick": "0.0.1",
|
50 |
+
"postcss": "8.4.41",
|
51 |
"react": "18.2.0",
|
52 |
"react-circular-progressbar": "2.1.0",
|
53 |
"react-dom": "18.2.0",
|
54 |
"react-photo-sphere-viewer": "3.3.5-psv5.1.4",
|
55 |
+
"sharp": "0.33.4",
|
56 |
+
"styled-components": "6.1.12",
|
57 |
+
"tailwind-merge": "2.5.2",
|
58 |
+
"tailwindcss": "3.4.9",
|
59 |
+
"tailwindcss-animate": "1.0.7",
|
60 |
+
"ts-node": "^10.9.2",
|
61 |
+
"typescript": "5.5.4",
|
|
|
|
|
|
|
62 |
"usehooks-ts": ".9.1",
|
63 |
"uuid": "9.0.0",
|
64 |
+
"zustand": "4.5.4"
|
65 |
},
|
66 |
"devDependencies": {
|
67 |
"@types/qs": "6.9.7",
|
68 |
+
"@types/react-virtualized": "9.21.22"
|
|
|
69 |
}
|
70 |
}
|
src/app/engine/getPanoramaFlux.ts
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { HfInference, HfInferenceEndpoint } from '@huggingface/inference'
|
4 |
+
|
5 |
+
import { filterOutBadWords } from "./censorship"
|
6 |
+
|
7 |
+
export async function getPanoramaFlux({
|
8 |
+
prompt,
|
9 |
+
}: {
|
10 |
+
prompt: string
|
11 |
+
}): Promise<string> {
|
12 |
+
if (!prompt) {
|
13 |
+
console.error(`cannot call the rendering API without a prompt, aborting..`)
|
14 |
+
throw new Error(`cannot call the rendering API without a prompt, aborting..`)
|
15 |
+
}
|
16 |
+
|
17 |
+
prompt = [
|
18 |
+
`hdri view`,
|
19 |
+
`highly detailed`,
|
20 |
+
`intricate details`,
|
21 |
+
filterOutBadWords(prompt)
|
22 |
+
].join(', ')
|
23 |
+
|
24 |
+
|
25 |
+
console.log(`calling API with prompt: ${prompt}`)
|
26 |
+
|
27 |
+
const hf: HfInferenceEndpoint = new HfInference(
|
28 |
+
`${process.env.HF_API_KEY}`
|
29 |
+
)
|
30 |
+
|
31 |
+
const blob: Blob = await hf.textToImage({
|
32 |
+
model: "<put a 360Β° flux model here>",
|
33 |
+
inputs: prompt,
|
34 |
+
parameters: {
|
35 |
+
height: 1024,
|
36 |
+
width: 2048,
|
37 |
+
|
38 |
+
// this triggers the following exception:
|
39 |
+
// Error: __call__() got an unexpected keyword argument 'negative_prompt'
|
40 |
+
// negative_prompt: request.prompts.image.negative || '',
|
41 |
+
|
42 |
+
/**
|
43 |
+
* The number of denoising steps. More denoising steps usually lead to a higher quality image at the expense of slower inference.
|
44 |
+
*/
|
45 |
+
// num_inference_steps?: number;
|
46 |
+
/**
|
47 |
+
* Guidance scale: Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, usually at the expense of lower image quality.
|
48 |
+
*/
|
49 |
+
// guidance_scale?: number;
|
50 |
+
},
|
51 |
+
})
|
52 |
+
|
53 |
+
// console.log('output from Hugging Face Inference API:', blob)
|
54 |
+
|
55 |
+
const buffer = Buffer.from(await blob.arrayBuffer())
|
56 |
+
|
57 |
+
return `data:${blob.type || 'image/jpeg'};base64,${buffer.toString('base64')}`
|
58 |
+
}
|
src/app/engine/getPanoramaSDXL.ts
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { Client } from "@gradio/client"
|
4 |
+
|
5 |
+
export async function getPanoramaSDXL({
|
6 |
+
prompt
|
7 |
+
}: {
|
8 |
+
prompt: string
|
9 |
+
}): Promise<string> {
|
10 |
+
const app = await Client.connect("jbilcke-hf/panorama-api")
|
11 |
+
|
12 |
+
const result = await app.predict("/predict", [
|
13 |
+
process.env.MICRO_SERVICE_SECRET_TOKEN || "",
|
14 |
+
prompt,
|
15 |
+
undefined
|
16 |
+
])
|
17 |
+
console.log(`result:`, result)
|
18 |
+
|
19 |
+
return ""
|
20 |
+
}
|
src/app/engine/render.ts
DELETED
@@ -1,245 +0,0 @@
|
|
1 |
-
"use server"
|
2 |
-
|
3 |
-
|
4 |
-
import { RenderRequest, RenderedScene } from "@/types"
|
5 |
-
import { generateSeed } from "@/lib/generateSeed"
|
6 |
-
import { sleep } from "@/lib/sleep"
|
7 |
-
import { filterOutBadWords } from "./censorship"
|
8 |
-
|
9 |
-
export async function newRender({
|
10 |
-
prompt,
|
11 |
-
clearCache,
|
12 |
-
}: {
|
13 |
-
prompt: string
|
14 |
-
clearCache: boolean
|
15 |
-
}) {
|
16 |
-
if (!prompt) {
|
17 |
-
console.error(`cannot call the rendering API without a prompt, aborting..`)
|
18 |
-
throw new Error(`cannot call the rendering API without a prompt, aborting..`)
|
19 |
-
}
|
20 |
-
|
21 |
-
prompt = [
|
22 |
-
`hdri view`,
|
23 |
-
`highly detailed`,
|
24 |
-
`intricate details`,
|
25 |
-
filterOutBadWords(prompt)
|
26 |
-
].join(', ')
|
27 |
-
|
28 |
-
// return await Gorgon.get(cacheKey, async () => {
|
29 |
-
|
30 |
-
let defaulResult: RenderedScene = {
|
31 |
-
renderId: "",
|
32 |
-
status: "error",
|
33 |
-
assetUrl: "",
|
34 |
-
alt: prompt || "",
|
35 |
-
maskUrl: "",
|
36 |
-
error: "failed to fetch the data",
|
37 |
-
segments: []
|
38 |
-
}
|
39 |
-
|
40 |
-
try {
|
41 |
-
console.log(`calling Gradio space with prompt: ${prompt}`)
|
42 |
-
|
43 |
-
const request = {
|
44 |
-
prompt,
|
45 |
-
nbFrames: 1, // when nbFrames is 1, we will only generate static images
|
46 |
-
nbSteps: 35, // 20 = fast, 30 = better, 50 = best
|
47 |
-
actionnables: [],
|
48 |
-
segmentation: "disabled", // one day we will remove this param, to make it automatic
|
49 |
-
width: 1024,
|
50 |
-
height: 768,
|
51 |
-
|
52 |
-
// on VideoQuest we use an aggressive setting: 4X upscaling
|
53 |
-
// this generates images that can be slow to load, but that's
|
54 |
-
// not too much of an issue since we use async loading
|
55 |
-
upscalingFactor: 1,
|
56 |
-
|
57 |
-
// note that we never disable the cache completely for VideoQuest
|
58 |
-
// that's because in the feedbacks people prefer speed to avoid frustration
|
59 |
-
cache: clearCache ? "renew" : "use",
|
60 |
-
|
61 |
-
} as Partial<RenderRequest>
|
62 |
-
|
63 |
-
console.table(request)
|
64 |
-
|
65 |
-
if (renderingEngine === "REPLICATE") {
|
66 |
-
if (!replicateToken) {
|
67 |
-
throw new Error(`you need to configure your REPLICATE_API_TOKEN in order to use the REPLICATE rendering engine`)
|
68 |
-
}
|
69 |
-
if (!replicateModel) {
|
70 |
-
throw new Error(`you need to configure your REPLICATE_API_MODEL in order to use the REPLICATE rendering engine`)
|
71 |
-
}
|
72 |
-
if (!replicateModelVersion) {
|
73 |
-
throw new Error(`you need to configure your REPLICATE_API_MODEL_VERSION in order to use the REPLICATE rendering engine`)
|
74 |
-
}
|
75 |
-
const replicate = new Replicate({ auth: replicateToken })
|
76 |
-
|
77 |
-
// console.log("Calling replicate..")
|
78 |
-
const seed = generateSeed()
|
79 |
-
const prediction = await replicate.predictions.create({
|
80 |
-
version: replicateModelVersion,
|
81 |
-
input: { prompt, seed }
|
82 |
-
})
|
83 |
-
|
84 |
-
// console.log("prediction:", prediction)
|
85 |
-
|
86 |
-
// no need to reply straight away: good things take time
|
87 |
-
// also our friends at Replicate won't like it if we spam them with requests
|
88 |
-
await sleep(12000)
|
89 |
-
|
90 |
-
return {
|
91 |
-
renderId: prediction.id,
|
92 |
-
status: "pending",
|
93 |
-
assetUrl: "",
|
94 |
-
alt: prompt,
|
95 |
-
error: prediction.error,
|
96 |
-
maskUrl: "",
|
97 |
-
segments: []
|
98 |
-
} as RenderedScene
|
99 |
-
} else {
|
100 |
-
|
101 |
-
const res = await fetch(`${apiUrl}/render`, {
|
102 |
-
method: "POST",
|
103 |
-
headers: {
|
104 |
-
Accept: "application/json",
|
105 |
-
"Content-Type": "application/json",
|
106 |
-
// Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
107 |
-
},
|
108 |
-
body: JSON.stringify(request),
|
109 |
-
cache: 'no-store',
|
110 |
-
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
111 |
-
// next: { revalidate: 1 }
|
112 |
-
})
|
113 |
-
|
114 |
-
// console.log("res:", res)
|
115 |
-
// The return value is *not* serialized
|
116 |
-
// You can return Date, Map, Set, etc.
|
117 |
-
|
118 |
-
// Recommendation: handle errors
|
119 |
-
if (res.status !== 200) {
|
120 |
-
// This will activate the closest `error.js` Error Boundary
|
121 |
-
throw new Error('Failed to fetch data')
|
122 |
-
}
|
123 |
-
|
124 |
-
const response = (await res.json()) as RenderedScene
|
125 |
-
// console.log("response:", response)
|
126 |
-
return response
|
127 |
-
}
|
128 |
-
} catch (err) {
|
129 |
-
console.log("request failed:", err)
|
130 |
-
console.error(err)
|
131 |
-
// Gorgon.clear(cacheKey)
|
132 |
-
return defaulResult
|
133 |
-
}
|
134 |
-
|
135 |
-
// }, cacheDurationInSec * 1000)
|
136 |
-
}
|
137 |
-
|
138 |
-
export async function getRender(renderId: string) {
|
139 |
-
if (!renderId) {
|
140 |
-
console.error(`cannot call the rendering API without a renderId, aborting..`)
|
141 |
-
throw new Error(`cannot call the rendering API without a renderId, aborting..`)
|
142 |
-
}
|
143 |
-
|
144 |
-
let defaulResult: RenderedScene = {
|
145 |
-
renderId: "",
|
146 |
-
status: "pending",
|
147 |
-
assetUrl: "",
|
148 |
-
alt: "",
|
149 |
-
maskUrl: "",
|
150 |
-
error: "",
|
151 |
-
segments: []
|
152 |
-
}
|
153 |
-
|
154 |
-
try {
|
155 |
-
|
156 |
-
|
157 |
-
if (renderingEngine === "REPLICATE") {
|
158 |
-
if (!replicateToken) {
|
159 |
-
throw new Error(`you need to configure your REPLICATE_API_TOKEN in order to use the REPLICATE rendering engine`)
|
160 |
-
}
|
161 |
-
if (!replicateModel) {
|
162 |
-
throw new Error(`you need to configure your REPLICATE_API_MODEL in order to use the REPLICATE rendering engine`)
|
163 |
-
}
|
164 |
-
|
165 |
-
// const replicate = new Replicate({ auth: replicateToken })
|
166 |
-
|
167 |
-
// console.log("Calling replicate..")
|
168 |
-
// const prediction = await replicate.predictions.get(renderId)
|
169 |
-
// console.log("Prediction:", prediction)
|
170 |
-
|
171 |
-
// console.log(`calling GET https://api.replicate.com/v1/predictions/${renderId}`)
|
172 |
-
const res = await fetch(`https://api.replicate.com/v1/predictions/${renderId}`, {
|
173 |
-
method: "GET",
|
174 |
-
headers: {
|
175 |
-
// Accept: "application/json",
|
176 |
-
// "Content-Type": "application/json",
|
177 |
-
Authorization: `Token ${replicateToken}`,
|
178 |
-
},
|
179 |
-
cache: 'no-store',
|
180 |
-
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
181 |
-
// next: { revalidate: 1 }
|
182 |
-
})
|
183 |
-
|
184 |
-
// console.log("res:", res)
|
185 |
-
// The return value is *not* serialized
|
186 |
-
// You can return Date, Map, Set, etc.
|
187 |
-
|
188 |
-
// Recommendation: handle errors
|
189 |
-
if (res.status !== 200) {
|
190 |
-
// This will activate the closest `error.js` Error Boundary
|
191 |
-
throw new Error('Failed to fetch data')
|
192 |
-
}
|
193 |
-
|
194 |
-
const response = (await res.json()) as any
|
195 |
-
// console.log("response:", response)
|
196 |
-
|
197 |
-
return {
|
198 |
-
renderId,
|
199 |
-
status: response?.error ? "error" : response?.status === "succeeded" ? "completed" : "pending",
|
200 |
-
assetUrl: `${response?.output || ""}`,
|
201 |
-
alt: `${response?.input?.prompt || ""}`,
|
202 |
-
error: `${response?.error || ""}`,
|
203 |
-
maskUrl: "",
|
204 |
-
segments: []
|
205 |
-
} as RenderedScene
|
206 |
-
} else {
|
207 |
-
|
208 |
-
// console.log(`calling GET ${apiUrl}/render with renderId: ${renderId}`)
|
209 |
-
const res = await fetch(`${apiUrl}/render/${renderId}`, {
|
210 |
-
method: "GET",
|
211 |
-
headers: {
|
212 |
-
Accept: "application/json",
|
213 |
-
"Content-Type": "application/json",
|
214 |
-
Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
215 |
-
},
|
216 |
-
cache: 'no-store',
|
217 |
-
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
218 |
-
// next: { revalidate: 1 }
|
219 |
-
})
|
220 |
-
|
221 |
-
// console.log("res:", res)
|
222 |
-
// The return value is *not* serialized
|
223 |
-
// You can return Date, Map, Set, etc.
|
224 |
-
|
225 |
-
// Recommendation: handle errors
|
226 |
-
if (res.status !== 200) {
|
227 |
-
// This will activate the closest `error.js` Error Boundary
|
228 |
-
throw new Error('Failed to fetch data')
|
229 |
-
}
|
230 |
-
|
231 |
-
const response = (await res.json()) as RenderedScene
|
232 |
-
// console.log("response:", response)
|
233 |
-
|
234 |
-
return response
|
235 |
-
}
|
236 |
-
} catch (err) {
|
237 |
-
console.error(err)
|
238 |
-
defaulResult.status = "error"
|
239 |
-
defaulResult.error = `${err}`
|
240 |
-
// Gorgon.clear(cacheKey)
|
241 |
-
return defaulResult
|
242 |
-
}
|
243 |
-
|
244 |
-
// }, cacheDurationInSec * 1000)
|
245 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/engine/upscaleImage.ts
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { Client } from "@gradio/client"
|
4 |
+
|
5 |
+
export async function upscalePanorama({
|
6 |
+
image,
|
7 |
+
prompt
|
8 |
+
}: {
|
9 |
+
image: string
|
10 |
+
prompt: string
|
11 |
+
}): Promise<string> {
|
12 |
+
const app = await Client.connect("jbilcke-hf/clarity-upscaler-api")
|
13 |
+
|
14 |
+
// const dataUri = await fetch(`data:image/jpeg;base64,${base64Data}`)
|
15 |
+
const dataUri = await fetch(image)
|
16 |
+
|
17 |
+
const imageBlob = await dataUri.blob()
|
18 |
+
|
19 |
+
const result = await app.predict("/predict", {
|
20 |
+
"Secret Token": process.env.MICRO_SERVICE_SECRET_TOKEN || "",
|
21 |
+
|
22 |
+
// convert the base64 image to blob
|
23 |
+
"Image": imageBlob,
|
24 |
+
|
25 |
+
Prompt: `360Β° HDRI panorama photo, ${prompt}`,
|
26 |
+
|
27 |
+
"Negative Prompt": "blurry, cropped",
|
28 |
+
|
29 |
+
"Scalue Factor": 2,
|
30 |
+
|
31 |
+
"Dynamic": 6,
|
32 |
+
|
33 |
+
"Creativity": 0.35,
|
34 |
+
|
35 |
+
"Resemblance": 0.6,
|
36 |
+
|
37 |
+
"tiling_width": 112,
|
38 |
+
|
39 |
+
"tiling_height": 144,
|
40 |
+
|
41 |
+
// epicrealism_naturalSinRC1VAE.safetensors [84d76a0328]', 'juggernaut_reborn.safetensors [338b85bc4f]', 'flat2DAnimerge_v45Sharp.safetensors'],
|
42 |
+
"sd_model": "juggernaut_reborn.safetensors [338b85bc4f]",
|
43 |
+
"scheduler": "DPM++ 3M SDE Karras",
|
44 |
+
})
|
45 |
+
/*
|
46 |
+
|
47 |
+
inputs.append(gr.Slider(
|
48 |
+
label="Num Inference Steps", info='''Number of denoising steps''', value=18,
|
49 |
+
minimum=1, maximum=100, step=1,
|
50 |
+
))
|
51 |
+
|
52 |
+
inputs.append(gr.Number(
|
53 |
+
label="Seed", info='''Random seed. Leave blank to randomize the seed''', value=1337
|
54 |
+
))
|
55 |
+
|
56 |
+
inputs.append(gr.Checkbox(
|
57 |
+
label="Downscaling", info='''Downscale the image before upscaling. Can improve quality and speed for images with high resolution but lower quality''', value=False
|
58 |
+
))
|
59 |
+
|
60 |
+
inputs.append(gr.Number(
|
61 |
+
label="Downscaling Resolution", info='''Downscaling resolution''', value=768
|
62 |
+
))
|
63 |
+
|
64 |
+
inputs.append(gr.Textbox(
|
65 |
+
label="Lora Links", info='''Link to a lora file you want to use in your upscaling. Multiple links possible, seperated by comma'''
|
66 |
+
))
|
67 |
+
|
68 |
+
inputs.append(gr.Textbox(
|
69 |
+
label="Custom Sd Model", info='''Link to a custom safetensors checkpoint file you want to use in your upscaling. Will overwrite sd_model checkpoint.'''
|
70 |
+
))
|
71 |
+
|
72 |
+
])
|
73 |
+
*/
|
74 |
+
console.log(`result:`, result)
|
75 |
+
|
76 |
+
return ""
|
77 |
+
}
|
src/app/firehose/{page.tsx β page.txt}
RENAMED
@@ -1,6 +1,6 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { useEffect, useState, useTransition } from "react"
|
4 |
|
5 |
import { Post } from "@/types"
|
6 |
import { cn } from "@/lib/utils"
|
@@ -13,7 +13,7 @@ import { Delete } from "./delete"
|
|
13 |
import Link from "next/link"
|
14 |
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
15 |
|
16 |
-
|
17 |
const searchParams = useSearchParams()
|
18 |
const [_isPending, startTransition] = useTransition()
|
19 |
const [posts, setPosts] = useState<Post[]>([])
|
@@ -35,12 +35,7 @@ export default function FirehosePage() {
|
|
35 |
}
|
36 |
|
37 |
return (
|
38 |
-
|
39 |
-
<div className={cn(
|
40 |
-
`light fixed w-full h-full flex flex-col items-center bg-slate-300 text-slate-800`,
|
41 |
-
``,
|
42 |
-
actionman.className
|
43 |
-
)}>
|
44 |
<div className="w-full flex flex-col items-center overflow-y-scroll">
|
45 |
<div className="flex flex-col space-y-2 pt-18 mb-6">
|
46 |
<h1 className="text-4xl md:text-6xl lg:text-[70px] xl:text-[100px] text-cyan-700">π Text-to-panorama</h1>
|
@@ -96,6 +91,20 @@ export default function FirehosePage() {
|
|
96 |
</div>
|
97 |
</div>
|
98 |
<Delete post={toDelete} moderationKey={moderationKey} onDelete={handleOnDelete} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
</div>
|
100 |
</TooltipProvider>
|
101 |
)
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { Suspense, useEffect, useState, useTransition } from "react"
|
4 |
|
5 |
import { Post } from "@/types"
|
6 |
import { cn } from "@/lib/utils"
|
|
|
13 |
import Link from "next/link"
|
14 |
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
15 |
|
16 |
+
function PageContent() {
|
17 |
const searchParams = useSearchParams()
|
18 |
const [_isPending, startTransition] = useTransition()
|
19 |
const [posts, setPosts] = useState<Post[]>([])
|
|
|
35 |
}
|
36 |
|
37 |
return (
|
38 |
+
<>
|
|
|
|
|
|
|
|
|
|
|
39 |
<div className="w-full flex flex-col items-center overflow-y-scroll">
|
40 |
<div className="flex flex-col space-y-2 pt-18 mb-6">
|
41 |
<h1 className="text-4xl md:text-6xl lg:text-[70px] xl:text-[100px] text-cyan-700">π Text-to-panorama</h1>
|
|
|
91 |
</div>
|
92 |
</div>
|
93 |
<Delete post={toDelete} moderationKey={moderationKey} onDelete={handleOnDelete} />
|
94 |
+
</>
|
95 |
+
)
|
96 |
+
}
|
97 |
+
|
98 |
+
|
99 |
+
export default function FirehosePage() {
|
100 |
+
return (
|
101 |
+
<TooltipProvider delayDuration={100}>
|
102 |
+
<div className={cn(
|
103 |
+
`light fixed w-full h-full flex flex-col items-center bg-slate-300 text-slate-800`,
|
104 |
+
``,
|
105 |
+
actionman.className
|
106 |
+
)}>
|
107 |
+
<Suspense><PageContent /></Suspense>
|
108 |
</div>
|
109 |
</TooltipProvider>
|
110 |
)
|
src/app/generate/page.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import {
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
import { TopMenu } from "../interface/top-menu"
|
@@ -9,31 +9,22 @@ import { fonts } from "@/lib/fonts"
|
|
9 |
import { useStore } from "../store"
|
10 |
import { BottomBar } from "../interface/bottom-bar"
|
11 |
import { SphericalImage } from "../interface/spherical-image"
|
12 |
-
import {
|
13 |
-
import {
|
14 |
-
import { getPost, postToCommunity } from "../engine/community"
|
15 |
import { useSearchParams } from "next/navigation"
|
16 |
|
17 |
-
|
18 |
const searchParams = useSearchParams()
|
19 |
const [_isPending, startTransition] = useTransition()
|
20 |
const postId = (searchParams.get("postId") as string) || ""
|
21 |
|
22 |
const prompt = useStore(s => s.prompt)
|
23 |
const setPrompt = useStore(s => s.setPrompt)
|
24 |
-
const
|
25 |
-
const
|
26 |
const isLoading = useStore(s => s.isLoading)
|
27 |
const setLoading = useStore(s => s.setLoading)
|
28 |
|
29 |
-
// keep a ref in sync
|
30 |
-
const renderedRef = useRef<RenderedScene>()
|
31 |
-
const renderedKey = JSON.stringify(renderedScene)
|
32 |
-
useEffect(() => { renderedRef.current = renderedScene }, [renderedKey])
|
33 |
-
|
34 |
-
const timeoutRef = useRef<any>(null)
|
35 |
-
|
36 |
-
const delay = 3000
|
37 |
|
38 |
// react to prompt changes
|
39 |
useEffect(() => {
|
@@ -44,89 +35,21 @@ export default function GeneratePage() {
|
|
44 |
// if (isLoading) { return }
|
45 |
|
46 |
startTransition(async () => {
|
47 |
-
|
48 |
try {
|
49 |
-
const
|
50 |
-
|
51 |
-
|
52 |
-
console.error(err)
|
53 |
-
} finally {
|
54 |
-
}
|
55 |
-
})
|
56 |
-
}, [prompt]) // important: we need to react to preset changes too
|
57 |
-
|
58 |
-
|
59 |
-
const checkStatus = () => {
|
60 |
-
startTransition(async () => {
|
61 |
-
clearTimeout(timeoutRef.current)
|
62 |
-
|
63 |
-
if (renderedRef.current?.status === "completed") {
|
64 |
-
console.log("rendering job is already completed")
|
65 |
-
return
|
66 |
-
}
|
67 |
-
|
68 |
-
if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
|
69 |
-
timeoutRef.current = setTimeout(checkStatus, delay)
|
70 |
-
return
|
71 |
-
}
|
72 |
-
|
73 |
-
try {
|
74 |
-
// console.log(`Checking job status API for job ${renderedRef.current?.renderId}`)
|
75 |
-
const newRendered = await getRender(renderedRef.current.renderId)
|
76 |
-
if (!newRendered) {
|
77 |
-
throw new Error(`getRender failed`)
|
78 |
-
}
|
79 |
-
// console.log("got a response!", newRendered)
|
80 |
-
|
81 |
-
if (JSON.stringify(renderedRef.current) !== JSON.stringify(newRendered)) {
|
82 |
-
// console.log("updated panel:", newRendered)
|
83 |
-
setRendered(renderedRef.current = newRendered)
|
84 |
-
}
|
85 |
-
// console.log("status:", newRendered.status)
|
86 |
-
|
87 |
-
if (newRendered.status === "pending") {
|
88 |
-
console.log("job not finished")
|
89 |
-
timeoutRef.current = setTimeout(checkStatus, delay)
|
90 |
-
} else if (newRendered.status === "error" ||
|
91 |
-
(newRendered.status === "completed" && !newRendered.assetUrl?.length)) {
|
92 |
-
console.log(`panorama got an error and/or an empty asset url :/ "${newRendered.error}", but let's try to recover..`)
|
93 |
setLoading(false)
|
94 |
} else {
|
95 |
-
console.log(
|
96 |
-
/*
|
97 |
-
let's disable the community for now
|
98 |
-
|
99 |
-
try {
|
100 |
-
await postToCommunity({
|
101 |
-
prompt,
|
102 |
-
model: "jbilcke-hf/sdxl-panorama",
|
103 |
-
assetUrl: newRendered.assetUrl,
|
104 |
-
})
|
105 |
-
} catch (err) {
|
106 |
-
console.log("failed to post to community, but it's no big deal")
|
107 |
-
}
|
108 |
-
*/
|
109 |
-
setRendered(newRendered)
|
110 |
-
setLoading(false)
|
111 |
}
|
112 |
} catch (err) {
|
113 |
console.error(err)
|
114 |
-
|
|
|
115 |
}
|
116 |
})
|
117 |
-
}
|
118 |
-
|
119 |
-
useEffect(() => {
|
120 |
-
// console.log("starting timeout")
|
121 |
-
clearTimeout(timeoutRef.current)
|
122 |
-
|
123 |
-
// normally it should reply in < 1sec, but we could also use an interval
|
124 |
-
timeoutRef.current = setTimeout(checkStatus, delay)
|
125 |
-
|
126 |
-
return () => {
|
127 |
-
clearTimeout(timeoutRef.current)
|
128 |
-
}
|
129 |
-
}, [prompt])
|
130 |
|
131 |
useEffect(() => {
|
132 |
if (!postId) {
|
@@ -144,15 +67,7 @@ export default function GeneratePage() {
|
|
144 |
// because we are set the app to "is loading"
|
145 |
// setPrompt(post.prompt)
|
146 |
|
147 |
-
|
148 |
-
renderId: postId,
|
149 |
-
status: "completed",
|
150 |
-
assetUrl: post.assetUrl,
|
151 |
-
alt: post.prompt,
|
152 |
-
error: "",
|
153 |
-
maskUrl: "",
|
154 |
-
segments: []
|
155 |
-
})
|
156 |
setLoading(false)
|
157 |
} catch (err) {
|
158 |
console.error("failed to get post: ", err)
|
@@ -162,19 +77,16 @@ export default function GeneratePage() {
|
|
162 |
}, [postId])
|
163 |
|
164 |
return (
|
165 |
-
|
166 |
-
<TopMenu />
|
167 |
<div className={cn(
|
168 |
`fixed inset-0 w-screen h-screen overflow-y-scroll`,
|
169 |
fonts.actionman.className
|
170 |
)}>
|
171 |
-
{
|
172 |
-
|
173 |
-
onEvent={(() => {}) as any}
|
174 |
debug={true}
|
175 |
/> : null}
|
176 |
</div>
|
177 |
-
<BottomBar />
|
178 |
<div className={cn(
|
179 |
`print:hidden`,
|
180 |
`z-20 fixed inset-0`,
|
@@ -193,6 +105,17 @@ export default function GeneratePage() {
|
|
193 |
{isLoading ? 'Generating metaverse location in the latent space..' : ''}
|
194 |
</div>
|
195 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
</div>
|
197 |
)
|
198 |
}
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { Suspense, useEffect, useTransition } from "react"
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
import { TopMenu } from "../interface/top-menu"
|
|
|
9 |
import { useStore } from "../store"
|
10 |
import { BottomBar } from "../interface/bottom-bar"
|
11 |
import { SphericalImage } from "../interface/spherical-image"
|
12 |
+
import { getPanoramaSDXL } from "../engine/getPanoramaSDXL"
|
13 |
+
import { getPost } from "../engine/community"
|
|
|
14 |
import { useSearchParams } from "next/navigation"
|
15 |
|
16 |
+
function PageContent() {
|
17 |
const searchParams = useSearchParams()
|
18 |
const [_isPending, startTransition] = useTransition()
|
19 |
const postId = (searchParams.get("postId") as string) || ""
|
20 |
|
21 |
const prompt = useStore(s => s.prompt)
|
22 |
const setPrompt = useStore(s => s.setPrompt)
|
23 |
+
const assetUrl = useStore(s => s.assetUrl)
|
24 |
+
const setAssetUrl = useStore(s => s.setAssetUrl)
|
25 |
const isLoading = useStore(s => s.isLoading)
|
26 |
const setLoading = useStore(s => s.setLoading)
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
// react to prompt changes
|
30 |
useEffect(() => {
|
|
|
35 |
// if (isLoading) { return }
|
36 |
|
37 |
startTransition(async () => {
|
|
|
38 |
try {
|
39 |
+
const assetUrl = await getPanoramaSDXL({ prompt })
|
40 |
+
if (assetUrl) {
|
41 |
+
setAssetUrl(assetUrl)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
setLoading(false)
|
43 |
} else {
|
44 |
+
console.log(`panorama got an error and/or an empty asset url`)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
}
|
46 |
} catch (err) {
|
47 |
console.error(err)
|
48 |
+
} finally {
|
49 |
+
setLoading(false)
|
50 |
}
|
51 |
})
|
52 |
+
}, [prompt]) // important: we need to react to preset changes too
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
|
54 |
useEffect(() => {
|
55 |
if (!postId) {
|
|
|
67 |
// because we are set the app to "is loading"
|
68 |
// setPrompt(post.prompt)
|
69 |
|
70 |
+
setAssetUrl(post.assetUrl)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
setLoading(false)
|
72 |
} catch (err) {
|
73 |
console.error("failed to get post: ", err)
|
|
|
77 |
}, [postId])
|
78 |
|
79 |
return (
|
80 |
+
<>
|
|
|
81 |
<div className={cn(
|
82 |
`fixed inset-0 w-screen h-screen overflow-y-scroll`,
|
83 |
fonts.actionman.className
|
84 |
)}>
|
85 |
+
{assetUrl ? <SphericalImage
|
86 |
+
assetUrl={assetUrl}
|
|
|
87 |
debug={true}
|
88 |
/> : null}
|
89 |
</div>
|
|
|
90 |
<div className={cn(
|
91 |
`print:hidden`,
|
92 |
`z-20 fixed inset-0`,
|
|
|
105 |
{isLoading ? 'Generating metaverse location in the latent space..' : ''}
|
106 |
</div>
|
107 |
</div>
|
108 |
+
</>
|
109 |
+
)
|
110 |
+
}
|
111 |
+
|
112 |
+
|
113 |
+
export default function GeneratePage() {
|
114 |
+
return (
|
115 |
+
<div className="">
|
116 |
+
<Suspense><TopMenu /></Suspense>
|
117 |
+
<Suspense><PageContent /></Suspense>
|
118 |
+
<BottomBar />
|
119 |
</div>
|
120 |
)
|
121 |
}
|
src/app/interface/display/index.tsx
CHANGED
@@ -1,10 +1,9 @@
|
|
1 |
-
import { RenderedScene } from "@/types"
|
2 |
|
3 |
-
export function Display ({
|
4 |
return (
|
5 |
<>
|
6 |
<img
|
7 |
-
src={
|
8 |
className="fixed w-screen top-0 left-0 right-0"
|
9 |
/>
|
10 |
</>
|
|
|
|
|
1 |
|
2 |
+
export function Display ({ assetUrl }: { assetUrl: string }) {
|
3 |
return (
|
4 |
<>
|
5 |
<img
|
6 |
+
src={assetUrl || undefined}
|
7 |
className="fixed w-screen top-0 left-0 right-0"
|
8 |
/>
|
9 |
</>
|
src/app/interface/spherical-image/index.tsx
CHANGED
@@ -1,33 +1,27 @@
|
|
1 |
"use client"
|
2 |
|
3 |
import { useEffect, useRef, useState } from "react"
|
4 |
-
import {
|
5 |
-
import {
|
6 |
-
|
7 |
-
import { MouseEventHandler, RenderedScene } from "@/types"
|
8 |
|
9 |
import { useImageDimension } from "@/lib/useImageDimension"
|
10 |
-
import { lightSourceNames } from "@/lib/lightSourceNames"
|
11 |
|
12 |
type PhotoSpherePlugin = (PluginConstructor | [PluginConstructor, any])
|
13 |
|
14 |
export function SphericalImage({
|
15 |
-
|
16 |
-
onEvent,
|
17 |
className,
|
18 |
debug,
|
19 |
}: {
|
20 |
-
|
21 |
-
onEvent: MouseEventHandler
|
22 |
className?: string
|
23 |
debug?: boolean
|
24 |
}) {
|
25 |
|
26 |
|
27 |
-
const imageDimension = useImageDimension(
|
28 |
-
const maskDimension = useImageDimension(rendered.maskUrl)
|
29 |
|
30 |
-
const sceneConfig = JSON.stringify({
|
31 |
const [lastSceneConfig, setLastSceneConfig] = useState<string>(sceneConfig)
|
32 |
const rootContainerRef = useRef<HTMLDivElement>(null)
|
33 |
const viewerContainerRef = useRef<HTMLElement>()
|
@@ -39,7 +33,7 @@ export function SphericalImage({
|
|
39 |
const options = {
|
40 |
defaultZoomLvl,
|
41 |
fisheye: false, // ..no!
|
42 |
-
overlay:
|
43 |
overlayOpacity: debug ? 0.5 : 0,
|
44 |
/*
|
45 |
panoData: {
|
@@ -56,47 +50,6 @@ export function SphericalImage({
|
|
56 |
*/
|
57 |
}
|
58 |
|
59 |
-
|
60 |
-
const cacheRef = useRef("")
|
61 |
-
useEffect(() => {
|
62 |
-
const listener = (e: DragEvent) => {
|
63 |
-
if (!rootContainerRef.current) { return }
|
64 |
-
|
65 |
-
// TODO: check if we are currently dragging an object
|
66 |
-
// if yes, then we should check if clientX and clientY are matching the
|
67 |
-
const boundingRect = rootContainerRef.current.getBoundingClientRect()
|
68 |
-
|
69 |
-
// abort if we are not currently dragging over our display area
|
70 |
-
if (e.clientX < boundingRect.left) { return }
|
71 |
-
if (e.clientX > (boundingRect.left + boundingRect.width)) { return }
|
72 |
-
if (e.clientY < boundingRect.top) { return }
|
73 |
-
if (e.clientY > (boundingRect.top + boundingRect.height)) { return }
|
74 |
-
|
75 |
-
const containerX = e.clientX - boundingRect.left
|
76 |
-
const containerY = e.clientY - boundingRect.top
|
77 |
-
|
78 |
-
const relativeX = containerX / boundingRect.width
|
79 |
-
const relativeY = containerY / boundingRect.height
|
80 |
-
|
81 |
-
const key = `${relativeX},${relativeY}`
|
82 |
-
|
83 |
-
// to avoid use
|
84 |
-
if (cacheRef.current === key) {
|
85 |
-
return
|
86 |
-
}
|
87 |
-
// console.log(`DRAG: calling onEvent("hover", ${relativeX}, ${relativeY})`)
|
88 |
-
|
89 |
-
cacheRef.current = key
|
90 |
-
onEvent("hover", relativeX, relativeY)
|
91 |
-
}
|
92 |
-
|
93 |
-
document.addEventListener('drag', listener)
|
94 |
-
|
95 |
-
return () => {
|
96 |
-
document.removeEventListener('drag', listener)
|
97 |
-
}
|
98 |
-
}, [onEvent])
|
99 |
-
|
100 |
useEffect(() => {
|
101 |
const task = async () => {
|
102 |
// console.log("SphericalImage: useEffect")
|
@@ -114,50 +67,7 @@ export function SphericalImage({
|
|
114 |
...options,
|
115 |
}
|
116 |
|
117 |
-
|
118 |
-
|
119 |
-
if (maskDimension.width && imageDimension.width) {
|
120 |
-
|
121 |
-
// console.log("rendered.segments:", rendered.segments)
|
122 |
-
|
123 |
-
rendered.segments
|
124 |
-
.filter(segment => lightSourceNames.includes(segment.label))
|
125 |
-
.forEach(light => {
|
126 |
-
// console.log("light detected", light)
|
127 |
-
const [x1, y1, x2, y2] = light.box
|
128 |
-
const [centerX, centerY] = [(x1 + x2) / 2, (y1 + y2) / 2]
|
129 |
-
// console.log("center:", { centerX, centerY })
|
130 |
-
const [relativeX, relativeY] = [centerX / maskDimension.width, centerY/ maskDimension.height]
|
131 |
-
// console.log("relative:", { relativeX, relativeY})
|
132 |
-
|
133 |
-
const panoramaPosition: PanoramaPosition = {
|
134 |
-
textureX: relativeX * imageDimension.width,
|
135 |
-
textureY: relativeY * imageDimension.height
|
136 |
-
}
|
137 |
-
// console.log("panoramaPosition:", panoramaPosition)
|
138 |
-
|
139 |
-
const position = viewer.dataHelper.textureCoordsToSphericalCoords(panoramaPosition)
|
140 |
-
// console.log("sphericalPosition:", position)
|
141 |
-
if ( // make sure coordinates are valid
|
142 |
-
!isNaN(position.pitch) && isFinite(position.pitch) &&
|
143 |
-
!isNaN(position.yaw) && isFinite(position.yaw)) {
|
144 |
-
lensflares.push({
|
145 |
-
id: `flare_${lensflares.length}`,
|
146 |
-
position,
|
147 |
-
type: 0,
|
148 |
-
})
|
149 |
-
}
|
150 |
-
})
|
151 |
-
}
|
152 |
-
|
153 |
-
// console.log("lensflares:", lensflares)
|
154 |
-
const lensFlarePlugin = viewer.getPlugin<LensflarePlugin>("lensflare")
|
155 |
-
lensFlarePlugin.setLensflares(lensflares)
|
156 |
-
|
157 |
-
// console.log("SphericalImage: calling setOptions")
|
158 |
-
// console.log("SphericalImage: changing the panorama to: " + rendered.assetUrl.slice(0, 120))
|
159 |
-
|
160 |
-
await viewer.setPanorama(rendered.assetUrl, {
|
161 |
...newOptions,
|
162 |
showLoader: false,
|
163 |
})
|
@@ -173,86 +83,24 @@ export function SphericalImage({
|
|
173 |
}
|
174 |
}
|
175 |
task()
|
176 |
-
}, [sceneConfig,
|
177 |
-
|
178 |
-
const handleEvent = async (event: React.MouseEvent<HTMLDivElement, MouseEvent>, isClick: boolean) => {
|
179 |
-
const rootContainer = rootContainerRef.current
|
180 |
-
const viewer = viewerRef.current
|
181 |
-
const viewerContainer = viewerContainerRef.current
|
182 |
-
|
183 |
-
/*
|
184 |
-
if (isClick) console.log(`handleEvent(${isClick})`, {
|
185 |
-
" imageDimension.width": imageDimension.width,
|
186 |
-
"rendered.maskUrl": rendered.maskUrl
|
187 |
-
})
|
188 |
-
*/
|
189 |
-
|
190 |
-
if (!viewer || !rootContainer || !viewerContainer || !imageDimension.width || !rendered.maskUrl) {
|
191 |
-
return
|
192 |
-
}
|
193 |
-
|
194 |
-
const containerRect = viewerContainer.getBoundingClientRect()
|
195 |
-
// if (isClick) console.log("containerRect:", containerRect)
|
196 |
-
|
197 |
-
const containerY = event.clientY - containerRect.top
|
198 |
-
// console.log("containerY:", containerY)
|
199 |
-
|
200 |
-
const position: Position = viewer.getPosition()
|
201 |
-
|
202 |
-
const viewerPosition: Point = viewer.dataHelper.sphericalCoordsToViewerCoords(position)
|
203 |
-
// if (isClick) console.log("viewerPosition:", viewerPosition)
|
204 |
|
205 |
-
// we want to ignore events that are happening in the toolbar
|
206 |
-
// note that we will probably hide this toolbar at some point,
|
207 |
-
// to implement our own UI
|
208 |
-
if (isClick && containerY > (containerRect.height - 40)) {
|
209 |
-
// console.log("we are in the toolbar.. ignoring the click")
|
210 |
-
return
|
211 |
-
}
|
212 |
-
|
213 |
-
const panoramaPosition: PanoramaPosition = viewer.dataHelper.sphericalCoordsToTextureCoords(position)
|
214 |
-
|
215 |
-
if (typeof panoramaPosition.textureX !== "number" || typeof panoramaPosition.textureY !== "number") {
|
216 |
-
return
|
217 |
-
}
|
218 |
-
|
219 |
-
const relativeX = panoramaPosition.textureX / imageDimension.width
|
220 |
-
const relativeY = panoramaPosition.textureY / imageDimension.height
|
221 |
|
222 |
-
|
223 |
-
}
|
224 |
-
|
225 |
-
if (!rendered.assetUrl) {
|
226 |
return null
|
227 |
}
|
228 |
|
229 |
return (
|
230 |
<div
|
231 |
ref={rootContainerRef}
|
232 |
-
onMouseMove={(event) => {
|
233 |
-
handleEvent(event, false)
|
234 |
-
setMouseMoved(true)
|
235 |
-
}}
|
236 |
-
onMouseUp={(event) => {
|
237 |
-
if (!mouseMoved) {
|
238 |
-
handleEvent(event, true)
|
239 |
-
}
|
240 |
-
setMouseMoved(false)
|
241 |
-
}}
|
242 |
-
onMouseDown={() => {
|
243 |
-
setMouseMoved(false)
|
244 |
-
}}
|
245 |
>
|
246 |
<ReactPhotoSphereViewer
|
247 |
-
src={
|
248 |
container=""
|
249 |
containerClass={className}
|
250 |
|
251 |
height="100vh"
|
252 |
width="100%"
|
253 |
-
|
254 |
-
// to access a plugin we must use viewer.getPlugin()
|
255 |
-
plugins={[[LensflarePlugin, { lensflares: [] }]]}
|
256 |
|
257 |
{...options}
|
258 |
|
@@ -263,24 +111,7 @@ export function SphericalImage({
|
|
263 |
}}
|
264 |
|
265 |
onReady={(instance) => {
|
266 |
-
viewerRef.current = instance
|
267 |
viewerContainerRef.current = instance.container
|
268 |
-
|
269 |
-
/*
|
270 |
-
const markersPlugs = instance.getPlugin(MarkersPlugin);
|
271 |
-
if (!markersPlugs)
|
272 |
-
return;
|
273 |
-
markersPlugs.addMarker({
|
274 |
-
id: "imageLayer2",
|
275 |
-
imageLayer: "drone.png",
|
276 |
-
size: { width: 220, height: 220 },
|
277 |
-
position: { yaw: '130.5deg', pitch: '-0.1deg' },
|
278 |
-
tooltip: "Image embedded in the scene"
|
279 |
-
});
|
280 |
-
markersPlugs.addEventListener("select-marker", () => {
|
281 |
-
console.log("asd");
|
282 |
-
});
|
283 |
-
*/
|
284 |
}}
|
285 |
|
286 |
/>
|
|
|
1 |
"use client"
|
2 |
|
3 |
import { useEffect, useRef, useState } from "react"
|
4 |
+
import { PluginConstructor, Viewer } from "@photo-sphere-viewer/core"
|
5 |
+
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
|
|
|
|
|
6 |
|
7 |
import { useImageDimension } from "@/lib/useImageDimension"
|
|
|
8 |
|
9 |
type PhotoSpherePlugin = (PluginConstructor | [PluginConstructor, any])
|
10 |
|
11 |
export function SphericalImage({
|
12 |
+
assetUrl,
|
|
|
13 |
className,
|
14 |
debug,
|
15 |
}: {
|
16 |
+
assetUrl: string
|
|
|
17 |
className?: string
|
18 |
debug?: boolean
|
19 |
}) {
|
20 |
|
21 |
|
22 |
+
const imageDimension = useImageDimension(assetUrl)
|
|
|
23 |
|
24 |
+
const sceneConfig = JSON.stringify({ assetUrl, debug, imageDimension })
|
25 |
const [lastSceneConfig, setLastSceneConfig] = useState<string>(sceneConfig)
|
26 |
const rootContainerRef = useRef<HTMLDivElement>(null)
|
27 |
const viewerContainerRef = useRef<HTMLElement>()
|
|
|
33 |
const options = {
|
34 |
defaultZoomLvl,
|
35 |
fisheye: false, // ..no!
|
36 |
+
overlay: undefined,
|
37 |
overlayOpacity: debug ? 0.5 : 0,
|
38 |
/*
|
39 |
panoData: {
|
|
|
50 |
*/
|
51 |
}
|
52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
useEffect(() => {
|
54 |
const task = async () => {
|
55 |
// console.log("SphericalImage: useEffect")
|
|
|
67 |
...options,
|
68 |
}
|
69 |
|
70 |
+
await viewer.setPanorama(assetUrl, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
...newOptions,
|
72 |
showLoader: false,
|
73 |
})
|
|
|
83 |
}
|
84 |
}
|
85 |
task()
|
86 |
+
}, [sceneConfig, assetUrl, viewerRef.current, imageDimension])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
|
89 |
+
if (!assetUrl) {
|
|
|
|
|
|
|
90 |
return null
|
91 |
}
|
92 |
|
93 |
return (
|
94 |
<div
|
95 |
ref={rootContainerRef}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
>
|
97 |
<ReactPhotoSphereViewer
|
98 |
+
src={assetUrl}
|
99 |
container=""
|
100 |
containerClass={className}
|
101 |
|
102 |
height="100vh"
|
103 |
width="100%"
|
|
|
|
|
|
|
104 |
|
105 |
{...options}
|
106 |
|
|
|
111 |
}}
|
112 |
|
113 |
onReady={(instance) => {
|
|
|
114 |
viewerContainerRef.current = instance.container
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
}}
|
116 |
|
117 |
/>
|
src/app/interface/top-menu/index.tsx
CHANGED
@@ -11,7 +11,7 @@ export function TopMenu() {
|
|
11 |
const prompt = useStore(s => s.prompt)
|
12 |
|
13 |
const setPrompt = useStore(s => s.setPrompt)
|
14 |
-
const
|
15 |
|
16 |
const isLoading = useStore(s => s.isLoading)
|
17 |
const setLoading = useStore(s => s.setLoading)
|
@@ -26,15 +26,7 @@ export function TopMenu() {
|
|
26 |
const promptChanged = draftPrompt.trim() !== prompt.trim()
|
27 |
if (!isLoading && (promptChanged)) {
|
28 |
// important: we reset!
|
29 |
-
|
30 |
-
renderId: "",
|
31 |
-
status: "pending",
|
32 |
-
assetUrl: "",
|
33 |
-
alt: "",
|
34 |
-
error: "",
|
35 |
-
maskUrl: "",
|
36 |
-
segments: []
|
37 |
-
})
|
38 |
setPrompt(draftPrompt)
|
39 |
}
|
40 |
}
|
|
|
11 |
const prompt = useStore(s => s.prompt)
|
12 |
|
13 |
const setPrompt = useStore(s => s.setPrompt)
|
14 |
+
const setAssetUrl = useStore(s => s.setAssetUrl)
|
15 |
|
16 |
const isLoading = useStore(s => s.isLoading)
|
17 |
const setLoading = useStore(s => s.setLoading)
|
|
|
26 |
const promptChanged = draftPrompt.trim() !== prompt.trim()
|
27 |
if (!isLoading && (promptChanged)) {
|
28 |
// important: we reset!
|
29 |
+
setAssetUrl("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
setPrompt(draftPrompt)
|
31 |
}
|
32 |
}
|
src/app/page.tsx
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
"use server"
|
2 |
|
3 |
import Head from "next/head"
|
|
|
4 |
|
5 |
// import Firehose from "./firehose/page"
|
6 |
import Generate from "./generate/page"
|
7 |
|
|
|
8 |
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
9 |
|
10 |
export default async function Page() {
|
@@ -18,7 +20,7 @@ export default async function Page() {
|
|
18 |
<main className={
|
19 |
`light bg-zinc-50 text-stone-900
|
20 |
`}>
|
21 |
-
<Generate
|
22 |
</main>
|
23 |
</>
|
24 |
)
|
|
|
1 |
"use server"
|
2 |
|
3 |
import Head from "next/head"
|
4 |
+
import { Suspense } from "react"
|
5 |
|
6 |
// import Firehose from "./firehose/page"
|
7 |
import Generate from "./generate/page"
|
8 |
|
9 |
+
|
10 |
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
11 |
|
12 |
export default async function Page() {
|
|
|
20 |
<main className={
|
21 |
`light bg-zinc-50 text-stone-900
|
22 |
`}>
|
23 |
+
<Suspense><Generate /></Suspense>
|
24 |
</main>
|
25 |
</>
|
26 |
)
|
src/app/store/index.ts
CHANGED
@@ -2,33 +2,23 @@
|
|
2 |
|
3 |
import { create } from "zustand"
|
4 |
|
5 |
-
import { RenderedScene } from "@/types"
|
6 |
-
|
7 |
export const useStore = create<{
|
8 |
prompt: string
|
9 |
-
|
10 |
isLoading: boolean
|
11 |
setLoading: (isLoading: boolean) => void
|
12 |
-
|
13 |
setPrompt: (prompt: string) => void
|
14 |
}>((set, get) => ({
|
15 |
prompt: "",
|
16 |
-
|
17 |
-
renderId: "",
|
18 |
-
status: "pending",
|
19 |
-
assetUrl: "",
|
20 |
-
alt: "",
|
21 |
-
error: "",
|
22 |
-
maskUrl: "",
|
23 |
-
segments: []
|
24 |
-
},
|
25 |
isLoading: false,
|
26 |
setLoading: (isLoading: boolean) => {
|
27 |
set({ isLoading })
|
28 |
},
|
29 |
-
|
30 |
set({
|
31 |
-
|
32 |
})
|
33 |
},
|
34 |
setPrompt: (prompt: string) => {
|
|
|
2 |
|
3 |
import { create } from "zustand"
|
4 |
|
|
|
|
|
5 |
export const useStore = create<{
|
6 |
prompt: string
|
7 |
+
assetUrl: string
|
8 |
isLoading: boolean
|
9 |
setLoading: (isLoading: boolean) => void
|
10 |
+
setAssetUrl: (assetUrl: string) => void
|
11 |
setPrompt: (prompt: string) => void
|
12 |
}>((set, get) => ({
|
13 |
prompt: "",
|
14 |
+
assetUrl: "",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
isLoading: false,
|
16 |
setLoading: (isLoading: boolean) => {
|
17 |
set({ isLoading })
|
18 |
},
|
19 |
+
setAssetUrl: (assetUrl: string) => {
|
20 |
set({
|
21 |
+
assetUrl
|
22 |
})
|
23 |
},
|
24 |
setPrompt: (prompt: string) => {
|
src/lib/getInitialRenderedScene.ts
DELETED
@@ -1,11 +0,0 @@
|
|
1 |
-
import { RenderedScene } from "@/types"
|
2 |
-
|
3 |
-
export const getInitialRenderedScene = (): RenderedScene => ({
|
4 |
-
renderId: "",
|
5 |
-
status: "pending",
|
6 |
-
assetUrl: "",
|
7 |
-
alt: "",
|
8 |
-
error: "",
|
9 |
-
maskUrl: "",
|
10 |
-
segments: []
|
11 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/types.ts
CHANGED
@@ -1,92 +1,4 @@
|
|
1 |
-
export type ProjectionMode = 'cartesian' | 'spherical'
|
2 |
|
3 |
-
export type MouseEventType = "hover" | "click"
|
4 |
-
|
5 |
-
export type MouseEventHandler = (type: MouseEventType, x: number, y: number) => Promise<void>
|
6 |
-
|
7 |
-
export type CacheMode = "use" | "renew" | "ignore"
|
8 |
-
|
9 |
-
export interface RenderRequest {
|
10 |
-
prompt: string
|
11 |
-
|
12 |
-
// whether to use video segmentation
|
13 |
-
// disabled (default)
|
14 |
-
// firstframe: we only analyze the first frame
|
15 |
-
// allframes: we analyze all the frames
|
16 |
-
segmentation: 'disabled' | 'firstframe' | 'allframes'
|
17 |
-
|
18 |
-
// segmentation will only be executed if we have a non-empty list of actionnables
|
19 |
-
// actionnables are names of things like "chest", "key", "tree", "chair" etc
|
20 |
-
actionnables: string[]
|
21 |
-
|
22 |
-
// note: this is the number of frames for Zeroscope,
|
23 |
-
// which is currently configured to only output 3 seconds, so:
|
24 |
-
// nbFrames=8 -> 1 sec
|
25 |
-
// nbFrames=16 -> 2 sec
|
26 |
-
// nbFrames=24 -> 3 sec
|
27 |
-
nbFrames: number // min: 1, max: 24
|
28 |
-
|
29 |
-
nbSteps: number // min: 1, max: 50
|
30 |
-
|
31 |
-
seed: number
|
32 |
-
|
33 |
-
width: number // fixed at 1024 for now
|
34 |
-
height: number // fixed at 512 for now
|
35 |
-
|
36 |
-
// upscaling factor
|
37 |
-
// 0: no upscaling
|
38 |
-
// 1: no upscaling
|
39 |
-
// 2: 2x larger
|
40 |
-
// 3: 3x larger
|
41 |
-
// 4x: 4x larger, up to 4096x4096 (warning: a PNG of this size can be 50 Mb!)
|
42 |
-
upscalingFactor: number
|
43 |
-
|
44 |
-
projection: ProjectionMode
|
45 |
-
|
46 |
-
cache: CacheMode
|
47 |
-
|
48 |
-
wait: boolean // wait until the job is completed
|
49 |
-
|
50 |
-
analyze: boolean // analyze the image to generate a caption (optional)
|
51 |
-
}
|
52 |
-
|
53 |
-
export interface ImageSegment {
|
54 |
-
id: number
|
55 |
-
box: number[]
|
56 |
-
color: number[]
|
57 |
-
label: string
|
58 |
-
score: number
|
59 |
-
}
|
60 |
-
|
61 |
-
export type RenderedSceneStatus =
|
62 |
-
| "pending"
|
63 |
-
| "completed"
|
64 |
-
| "error"
|
65 |
-
|
66 |
-
export interface RenderedScene {
|
67 |
-
renderId: string
|
68 |
-
status: RenderedSceneStatus
|
69 |
-
assetUrl: string
|
70 |
-
alt: string
|
71 |
-
error: string
|
72 |
-
maskUrl: string
|
73 |
-
segments: ImageSegment[]
|
74 |
-
}
|
75 |
-
|
76 |
-
export interface ImageAnalysisRequest {
|
77 |
-
image: string // in base64
|
78 |
-
prompt: string
|
79 |
-
}
|
80 |
-
|
81 |
-
export interface ImageAnalysisResponse {
|
82 |
-
result: string
|
83 |
-
error?: string
|
84 |
-
}
|
85 |
-
|
86 |
-
export type RenderingEngine =
|
87 |
-
| "VIDEOCHAIN"
|
88 |
-
| "OPENAI"
|
89 |
-
| "REPLICATE"
|
90 |
|
91 |
export type PostVisibility =
|
92 |
| "featured" // featured by admins
|
|
|
|
|
1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
export type PostVisibility =
|
4 |
| "featured" // featured by admins
|