Spaces:
Running
Running
Commit
·
026baa0
1
Parent(s):
f39b1f2
working on community sharing features
Browse files- .env +1 -1
- src/app/engine/censorship.ts +32 -0
- src/app/engine/community.ts +17 -9
- src/app/engine/render.ts +3 -1
- src/app/{main.tsx → generate/page.tsx} +18 -9
- src/app/interface/about/index.tsx +1 -1
- src/app/interface/panel/index.txt +0 -303
- src/app/interface/top-menu/index.tsx +1 -1
- src/app/landing.tsx +51 -0
- src/app/page.tsx +5 -3
- src/app/todo.tsx +0 -21
- src/types.ts +28 -23
.env
CHANGED
@@ -7,12 +7,12 @@ RENDERING_ENGINE="REPLICATE"
|
|
7 |
VIDEOCHAIN_API_URL="http://localhost:7860"
|
8 |
VIDEOCHAIN_API_TOKEN=
|
9 |
|
10 |
-
# Not supported yet
|
11 |
REPLICATE_API_TOKEN=
|
12 |
REPLICATE_API_MODEL="lucataco/sdxl-panoramic"
|
13 |
REPLICATE_API_MODEL_VERSION="76acc4075d0633dcb3823c1fed0419de21d42001b65c816c7b5b9beff30ec8cd"
|
14 |
|
15 |
# ----------- COMMUNITY SHARING (OPTIONAL, YOU DON'T NEED THIS IN LOCAL) -----------
|
|
|
16 |
# You don't need those community sharing options to run Panoremix
|
17 |
# locally or on your own server (they are meant to be used by the Hugging Face team)
|
18 |
COMMUNITY_API_URL=
|
|
|
7 |
VIDEOCHAIN_API_URL="http://localhost:7860"
|
8 |
VIDEOCHAIN_API_TOKEN=
|
9 |
|
|
|
10 |
REPLICATE_API_TOKEN=
|
11 |
REPLICATE_API_MODEL="lucataco/sdxl-panoramic"
|
12 |
REPLICATE_API_MODEL_VERSION="76acc4075d0633dcb3823c1fed0419de21d42001b65c816c7b5b9beff30ec8cd"
|
13 |
|
14 |
# ----------- COMMUNITY SHARING (OPTIONAL, YOU DON'T NEED THIS IN LOCAL) -----------
|
15 |
+
NEXT_PUBLIC_ENABLE_COMMUNITY_SHARING="false"
|
16 |
# You don't need those community sharing options to run Panoremix
|
17 |
# locally or on your own server (they are meant to be used by the Hugging Face team)
|
18 |
COMMUNITY_API_URL=
|
src/app/engine/censorship.ts
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// I don't want to be banned by Replicate because bad actors are asking
|
2 |
+
// for some naked anime stuff or whatever
|
3 |
+
// I also want to avoid a PR scandal due to some bad user generated content
|
4 |
+
|
5 |
+
const forbiddenWords = [
|
6 |
+
// those keywords have been generated by looking at the logs of the AI Comic Factory
|
7 |
+
// those are real requests some users tried to attempt.. :|
|
8 |
+
"nazi",
|
9 |
+
"hitler",
|
10 |
+
"boob",
|
11 |
+
"boobs",
|
12 |
+
"boobies",
|
13 |
+
"nipple",
|
14 |
+
"nipples",
|
15 |
+
"nude",
|
16 |
+
"nudes",
|
17 |
+
"naked",
|
18 |
+
"pee",
|
19 |
+
"peeing",
|
20 |
+
"erotic",
|
21 |
+
"sexy"
|
22 |
+
]
|
23 |
+
|
24 |
+
// temporary utility to make sure Replicate doesn't ban my account
|
25 |
+
// because of what users do in their prompt
|
26 |
+
export const filterOutBadWords = (sentence: string) => {
|
27 |
+
const words = sentence.split(" ")
|
28 |
+
return words.filter(word => {
|
29 |
+
const lowerCase = word.toLocaleLowerCase()
|
30 |
+
return !forbiddenWords.includes(lowerCase)
|
31 |
+
}).join(" ")
|
32 |
+
}
|
src/app/engine/community.ts
CHANGED
@@ -2,11 +2,12 @@
|
|
2 |
|
3 |
import { v4 as uuidv4 } from "uuid"
|
4 |
|
5 |
-
import { CreatePostResponse, GetAppPostsResponse, Post } from "@/types"
|
|
|
6 |
|
7 |
const apiUrl = `${process.env.COMMUNITY_API_URL || ""}`
|
8 |
const apiToken = `${process.env.COMMUNITY_API_TOKEN || ""}`
|
9 |
-
const appId = `${process.env.
|
10 |
|
11 |
export async function postToCommunity({
|
12 |
prompt,
|
@@ -15,6 +16,9 @@ export async function postToCommunity({
|
|
15 |
prompt: string
|
16 |
assetUrl: string
|
17 |
}): Promise<Post> {
|
|
|
|
|
|
|
18 |
// if the community API is disabled,
|
19 |
// we don't fail, we just mock
|
20 |
if (!apiUrl) {
|
@@ -25,6 +29,7 @@ export async function postToCommunity({
|
|
25 |
previewUrl: assetUrl,
|
26 |
assetUrl,
|
27 |
createdAt: new Date().toISOString(),
|
|
|
28 |
upvotes: 0,
|
29 |
downvotes: 0
|
30 |
}
|
@@ -41,7 +46,7 @@ export async function postToCommunity({
|
|
41 |
}
|
42 |
|
43 |
try {
|
44 |
-
console.log(`calling POST ${apiUrl}/
|
45 |
|
46 |
const postId = uuidv4()
|
47 |
|
@@ -49,7 +54,7 @@ export async function postToCommunity({
|
|
49 |
|
50 |
console.table(post)
|
51 |
|
52 |
-
const res = await fetch(`${apiUrl}/
|
53 |
method: "POST",
|
54 |
headers: {
|
55 |
Accept: "application/json",
|
@@ -82,7 +87,7 @@ export async function postToCommunity({
|
|
82 |
}
|
83 |
}
|
84 |
|
85 |
-
export async function getLatestPosts(): Promise<Post[]> {
|
86 |
|
87 |
let posts: Post[] = []
|
88 |
|
@@ -94,7 +99,9 @@ export async function getLatestPosts(): Promise<Post[]> {
|
|
94 |
|
95 |
try {
|
96 |
// console.log(`calling GET ${apiUrl}/posts with renderId: ${renderId}`)
|
97 |
-
const res = await fetch(`${apiUrl}/posts/${appId}
|
|
|
|
|
98 |
method: "GET",
|
99 |
headers: {
|
100 |
Accept: "application/json",
|
@@ -120,8 +127,9 @@ export async function getLatestPosts(): Promise<Post[]> {
|
|
120 |
// console.log("response:", response)
|
121 |
return Array.isArray(response?.posts) ? response?.posts : []
|
122 |
} catch (err) {
|
123 |
-
const error = `failed to get posts: ${err}`
|
124 |
-
console.error(error)
|
125 |
-
throw new Error(error)
|
|
|
126 |
}
|
127 |
}
|
|
|
2 |
|
3 |
import { v4 as uuidv4 } from "uuid"
|
4 |
|
5 |
+
import { CreatePostResponse, GetAppPostsResponse, Post, PostVisibility } from "@/types"
|
6 |
+
import { filterOutBadWords } from "./censorship"
|
7 |
|
8 |
const apiUrl = `${process.env.COMMUNITY_API_URL || ""}`
|
9 |
const apiToken = `${process.env.COMMUNITY_API_TOKEN || ""}`
|
10 |
+
const appId = `${process.env.COMMUNITY_API_ID || ""}`
|
11 |
|
12 |
export async function postToCommunity({
|
13 |
prompt,
|
|
|
16 |
prompt: string
|
17 |
assetUrl: string
|
18 |
}): Promise<Post> {
|
19 |
+
|
20 |
+
prompt = filterOutBadWords(prompt)
|
21 |
+
|
22 |
// if the community API is disabled,
|
23 |
// we don't fail, we just mock
|
24 |
if (!apiUrl) {
|
|
|
29 |
previewUrl: assetUrl,
|
30 |
assetUrl,
|
31 |
createdAt: new Date().toISOString(),
|
32 |
+
visibility: "normal",
|
33 |
upvotes: 0,
|
34 |
downvotes: 0
|
35 |
}
|
|
|
46 |
}
|
47 |
|
48 |
try {
|
49 |
+
console.log(`calling POST ${apiUrl}/posts/${appId} with prompt: ${prompt}`)
|
50 |
|
51 |
const postId = uuidv4()
|
52 |
|
|
|
54 |
|
55 |
console.table(post)
|
56 |
|
57 |
+
const res = await fetch(`${apiUrl}/posts/${appId}`, {
|
58 |
method: "POST",
|
59 |
headers: {
|
60 |
Accept: "application/json",
|
|
|
87 |
}
|
88 |
}
|
89 |
|
90 |
+
export async function getLatestPosts(visibility?: PostVisibility): Promise<Post[]> {
|
91 |
|
92 |
let posts: Post[] = []
|
93 |
|
|
|
99 |
|
100 |
try {
|
101 |
// console.log(`calling GET ${apiUrl}/posts with renderId: ${renderId}`)
|
102 |
+
const res = await fetch(`${apiUrl}/posts/${appId}/${
|
103 |
+
visibility || "all"
|
104 |
+
}`, {
|
105 |
method: "GET",
|
106 |
headers: {
|
107 |
Accept: "application/json",
|
|
|
127 |
// console.log("response:", response)
|
128 |
return Array.isArray(response?.posts) ? response?.posts : []
|
129 |
} catch (err) {
|
130 |
+
// const error = `failed to get posts: ${err}`
|
131 |
+
// console.error(error)
|
132 |
+
// throw new Error(error)
|
133 |
+
return []
|
134 |
}
|
135 |
}
|
src/app/engine/render.ts
CHANGED
@@ -5,6 +5,7 @@ import Replicate, { Prediction } from "replicate"
|
|
5 |
import { RenderRequest, RenderedScene, RenderingEngine } from "@/types"
|
6 |
import { generateSeed } from "@/lib/generateSeed"
|
7 |
import { sleep } from "@/lib/sleep"
|
|
|
8 |
|
9 |
const renderingEngine = `${process.env.RENDERING_ENGINE || ""}` as RenderingEngine
|
10 |
|
@@ -32,7 +33,7 @@ export async function newRender({
|
|
32 |
`hdri view`,
|
33 |
`highly detailed`,
|
34 |
`intricate details`,
|
35 |
-
prompt
|
36 |
].join(', ')
|
37 |
|
38 |
// return await Gorgon.get(cacheKey, async () => {
|
@@ -240,6 +241,7 @@ export async function getRender(renderId: string) {
|
|
240 |
|
241 |
const response = (await res.json()) as RenderedScene
|
242 |
// console.log("response:", response)
|
|
|
243 |
return response
|
244 |
}
|
245 |
} catch (err) {
|
|
|
5 |
import { RenderRequest, RenderedScene, RenderingEngine } from "@/types"
|
6 |
import { generateSeed } from "@/lib/generateSeed"
|
7 |
import { sleep } from "@/lib/sleep"
|
8 |
+
import { filterOutBadWords } from "./censorship"
|
9 |
|
10 |
const renderingEngine = `${process.env.RENDERING_ENGINE || ""}` as RenderingEngine
|
11 |
|
|
|
33 |
`hdri view`,
|
34 |
`highly detailed`,
|
35 |
`intricate details`,
|
36 |
+
filterOutBadWords(prompt)
|
37 |
].join(', ')
|
38 |
|
39 |
// return await Gorgon.get(cacheKey, async () => {
|
|
|
241 |
|
242 |
const response = (await res.json()) as RenderedScene
|
243 |
// console.log("response:", response)
|
244 |
+
|
245 |
return response
|
246 |
}
|
247 |
} catch (err) {
|
src/app/{main.tsx → generate/page.tsx}
RENAMED
@@ -1,18 +1,19 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { useEffect, useRef,
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
-
import { TopMenu } from "
|
7 |
import { fonts } from "@/lib/fonts"
|
8 |
|
9 |
-
import { useStore } from "
|
10 |
-
import { BottomBar } from "
|
11 |
-
import { SphericalImage } from "
|
12 |
-
import { getRender, newRender } from "
|
13 |
import { RenderedScene } from "@/types"
|
|
|
14 |
|
15 |
-
export default function
|
16 |
const [_isPending, startTransition] = useTransition()
|
17 |
|
18 |
const prompt = useStore(state => state.prompt)
|
@@ -78,6 +79,14 @@ export default function Generator() {
|
|
78 |
setLoading(false)
|
79 |
} else {
|
80 |
console.log("panorama finished:", newRendered)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
setRendered(newRendered)
|
82 |
setLoading(false)
|
83 |
}
|
@@ -101,7 +110,7 @@ export default function Generator() {
|
|
101 |
}, [prompt])
|
102 |
|
103 |
return (
|
104 |
-
<div>
|
105 |
<TopMenu />
|
106 |
<div className={cn(
|
107 |
`fixed inset-0 w-screen h-screen overflow-y-scroll`,
|
@@ -129,7 +138,7 @@ export default function Generator() {
|
|
129 |
isLoading ? ``: `scale-0 opacity-0`,
|
130 |
`transition-all duration-300 ease-in-out`,
|
131 |
)}>
|
132 |
-
{isLoading ? 'Generating
|
133 |
</div>
|
134 |
</div>
|
135 |
</div>
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { useEffect, useRef, useTransition } from "react"
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
+
import { TopMenu } from "../interface/top-menu"
|
7 |
import { fonts } from "@/lib/fonts"
|
8 |
|
9 |
+
import { useStore } from "../store"
|
10 |
+
import { BottomBar } from "../interface/bottom-bar"
|
11 |
+
import { SphericalImage } from "../interface/spherical-image"
|
12 |
+
import { getRender, newRender } from "../engine/render"
|
13 |
import { RenderedScene } from "@/types"
|
14 |
+
import { postToCommunity } from "../engine/community"
|
15 |
|
16 |
+
export default function GeneratePage() {
|
17 |
const [_isPending, startTransition] = useTransition()
|
18 |
|
19 |
const prompt = useStore(state => state.prompt)
|
|
|
79 |
setLoading(false)
|
80 |
} else {
|
81 |
console.log("panorama finished:", newRendered)
|
82 |
+
try {
|
83 |
+
await postToCommunity({
|
84 |
+
prompt,
|
85 |
+
assetUrl: newRendered.assetUrl,
|
86 |
+
})
|
87 |
+
} catch (err) {
|
88 |
+
console.log("failed to post to community, but it's no big deal")
|
89 |
+
}
|
90 |
setRendered(newRendered)
|
91 |
setLoading(false)
|
92 |
}
|
|
|
110 |
}, [prompt])
|
111 |
|
112 |
return (
|
113 |
+
<div className="">
|
114 |
<TopMenu />
|
115 |
<div className={cn(
|
116 |
`fixed inset-0 w-screen h-screen overflow-y-scroll`,
|
|
|
138 |
isLoading ? ``: `scale-0 opacity-0`,
|
139 |
`transition-all duration-300 ease-in-out`,
|
140 |
)}>
|
141 |
+
{isLoading ? 'Generating metaverse location in the latent space..' : ''}
|
142 |
</div>
|
143 |
</div>
|
144 |
</div>
|
src/app/interface/about/index.tsx
CHANGED
@@ -8,7 +8,7 @@ export function About() {
|
|
8 |
return (
|
9 |
<Dialog open={isOpen} onOpenChange={setOpen}>
|
10 |
<DialogTrigger asChild>
|
11 |
-
<Button variant="outline">
|
12 |
<span className="hidden md:inline">About this project</span>
|
13 |
<span className="inline md:hidden">About</span>
|
14 |
</Button>
|
|
|
8 |
return (
|
9 |
<Dialog open={isOpen} onOpenChange={setOpen}>
|
10 |
<DialogTrigger asChild>
|
11 |
+
<Button variant="outline" className="text-stone-800 dark:text-stone-200">
|
12 |
<span className="hidden md:inline">About this project</span>
|
13 |
<span className="inline md:hidden">About</span>
|
14 |
</Button>
|
src/app/interface/panel/index.txt
DELETED
@@ -1,303 +0,0 @@
|
|
1 |
-
"use client"
|
2 |
-
|
3 |
-
import { useEffect, useRef, useState, useTransition } from "react"
|
4 |
-
// import AutoSizer from "react-virtualized-auto-sizer"
|
5 |
-
|
6 |
-
import { RenderedScene } from "@/types"
|
7 |
-
|
8 |
-
import { getRender, newRender } from "@/app/engine/render"
|
9 |
-
import { useStore } from "@/app/store"
|
10 |
-
|
11 |
-
import { cn } from "@/lib/utils"
|
12 |
-
import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
|
13 |
-
import { Progress } from "@/app/interface/progress"
|
14 |
-
|
15 |
-
// import { see } from "@/app/engine/caption"
|
16 |
-
// import { replaceTextInSpeechBubbles } from "@/lib/replaceTextInSpeechBubbles"
|
17 |
-
|
18 |
-
export function Panel({
|
19 |
-
panel,
|
20 |
-
className = "",
|
21 |
-
width = 1,
|
22 |
-
height = 1,
|
23 |
-
}: {
|
24 |
-
panel: number
|
25 |
-
className?: string
|
26 |
-
width?: number
|
27 |
-
height?: number
|
28 |
-
}) {
|
29 |
-
const panelId = `${panel}`
|
30 |
-
|
31 |
-
const ref = useRef<HTMLImageElement>(null)
|
32 |
-
const font = useStore(state => state.font)
|
33 |
-
const preset = useStore(state => state.preset)
|
34 |
-
|
35 |
-
const setGeneratingImages = useStore(state => state.setGeneratingImages)
|
36 |
-
|
37 |
-
const [imageWithText, setImageWithText] = useState("")
|
38 |
-
const panels = useStore(state => state.panels)
|
39 |
-
const prompt = panels[panel] || ""
|
40 |
-
|
41 |
-
const captions = useStore(state => state.captions)
|
42 |
-
const caption = captions[panel] || ""
|
43 |
-
|
44 |
-
const zoomLevel = useStore(state => state.zoomLevel)
|
45 |
-
const showCaptions = useStore(state => state.showCaptions)
|
46 |
-
|
47 |
-
const addToUpscaleQueue = useStore(state => state.addToUpscaleQueue)
|
48 |
-
|
49 |
-
const [_isPending, startTransition] = useTransition()
|
50 |
-
const renderedScenes = useStore(state => state.renderedScenes)
|
51 |
-
const setRendered = useStore(state => state.setRendered)
|
52 |
-
|
53 |
-
const rendered = renderedScenes[panel] || getInitialRenderedScene()
|
54 |
-
|
55 |
-
// keep a ref in sync
|
56 |
-
const renderedRef = useRef<RenderedScene>()
|
57 |
-
const renderedKey = JSON.stringify(rendered)
|
58 |
-
useEffect(() => { renderedRef.current = rendered }, [renderedKey])
|
59 |
-
|
60 |
-
const timeoutRef = useRef<any>(null)
|
61 |
-
|
62 |
-
const delay = 3000 + (1000 * panel)
|
63 |
-
|
64 |
-
// since this run in its own loop, we need to use references everywhere
|
65 |
-
// but perhaps this could be refactored
|
66 |
-
useEffect(() => {
|
67 |
-
// console.log("Panel prompt: "+ prompt)
|
68 |
-
if (!prompt?.length) { return }
|
69 |
-
|
70 |
-
// important: update the status, and clear the scene
|
71 |
-
setGeneratingImages(panelId, true)
|
72 |
-
|
73 |
-
// just to empty it
|
74 |
-
setRendered(panelId, getInitialRenderedScene())
|
75 |
-
|
76 |
-
setTimeout(() => {
|
77 |
-
startTransition(async () => {
|
78 |
-
|
79 |
-
// console.log(`Loading panel ${panel}..`)
|
80 |
-
|
81 |
-
let newRendered: RenderedScene
|
82 |
-
try {
|
83 |
-
newRendered = await newRender({ prompt, width, height })
|
84 |
-
} catch (err) {
|
85 |
-
// "Failed to load the panel! Don't worry, we are retrying..")
|
86 |
-
newRendered = await newRender({ prompt, width, height })
|
87 |
-
}
|
88 |
-
|
89 |
-
if (newRendered) {
|
90 |
-
// console.log("newRendered:", newRendered)
|
91 |
-
setRendered(panelId, newRendered)
|
92 |
-
|
93 |
-
// but we are still loading!
|
94 |
-
} else {
|
95 |
-
setRendered(panelId, {
|
96 |
-
renderId: "",
|
97 |
-
status: "pending",
|
98 |
-
assetUrl: "",
|
99 |
-
alt: "",
|
100 |
-
maskUrl: "",
|
101 |
-
error: "",
|
102 |
-
segments: []
|
103 |
-
})
|
104 |
-
setGeneratingImages(panelId, false)
|
105 |
-
return
|
106 |
-
}
|
107 |
-
})
|
108 |
-
}, 2000 * panel)
|
109 |
-
}, [prompt, width, height])
|
110 |
-
|
111 |
-
|
112 |
-
const checkStatus = () => {
|
113 |
-
startTransition(async () => {
|
114 |
-
clearTimeout(timeoutRef.current)
|
115 |
-
|
116 |
-
if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
|
117 |
-
timeoutRef.current = setTimeout(checkStatus, delay)
|
118 |
-
return
|
119 |
-
}
|
120 |
-
try {
|
121 |
-
setGeneratingImages(panelId, true)
|
122 |
-
// console.log(`Checking job status API for job ${renderedRef.current?.renderId}`)
|
123 |
-
const newRendered = await getRender(renderedRef.current.renderId)
|
124 |
-
// console.log("got a response!", newRendered)
|
125 |
-
|
126 |
-
if (JSON.stringify(renderedRef.current) !== JSON.stringify(newRendered)) {
|
127 |
-
// console.log("updated panel:", newRendered)
|
128 |
-
setRendered(panelId, renderedRef.current = newRendered)
|
129 |
-
setGeneratingImages(panelId, true)
|
130 |
-
}
|
131 |
-
// console.log("status:", newRendered.status)
|
132 |
-
|
133 |
-
if (newRendered.status === "pending") {
|
134 |
-
// console.log("job not finished")
|
135 |
-
timeoutRef.current = setTimeout(checkStatus, delay)
|
136 |
-
} else if (newRendered.status === "error" ||
|
137 |
-
(newRendered.status === "completed" && !newRendered.assetUrl?.length)) {
|
138 |
-
// console.log(`panel got an error and/or an empty asset url :/ "${newRendered.error}", but let's try to recover..`)
|
139 |
-
try {
|
140 |
-
const newAttempt = await newRender({ prompt, width, height })
|
141 |
-
setRendered(panelId, newAttempt)
|
142 |
-
} catch (err) {
|
143 |
-
console.error("yeah sorry, something is wrong.. aborting", err)
|
144 |
-
setGeneratingImages(panelId, false)
|
145 |
-
}
|
146 |
-
} else {
|
147 |
-
console.log("panel finished!")
|
148 |
-
setGeneratingImages(panelId, false)
|
149 |
-
addToUpscaleQueue(panelId, newRendered)
|
150 |
-
}
|
151 |
-
} catch (err) {
|
152 |
-
console.error(err)
|
153 |
-
timeoutRef.current = setTimeout(checkStatus, delay)
|
154 |
-
}
|
155 |
-
})
|
156 |
-
}
|
157 |
-
|
158 |
-
useEffect(() => {
|
159 |
-
// console.log("starting timeout")
|
160 |
-
clearTimeout(timeoutRef.current)
|
161 |
-
|
162 |
-
// normally it should reply in < 1sec, but we could also use an interval
|
163 |
-
timeoutRef.current = setTimeout(checkStatus, delay)
|
164 |
-
|
165 |
-
return () => {
|
166 |
-
clearTimeout(timeoutRef.current)
|
167 |
-
}
|
168 |
-
}, [prompt, width, height])
|
169 |
-
|
170 |
-
/*
|
171 |
-
doing the captionning from the browser is expensive
|
172 |
-
a simpler solution is to caption directly during SDXL generation
|
173 |
-
|
174 |
-
useEffect(() => {
|
175 |
-
if (!rendered.assetUrl) { return }
|
176 |
-
// the asset url can evolve with time (link to a better resolution image)
|
177 |
-
// however it would be costly to ask for the caption, the low resolution is enough for the semantic resolution
|
178 |
-
// so we just do nothing if we already have the caption
|
179 |
-
if (caption) { return }
|
180 |
-
startTransition(async () => {
|
181 |
-
try {
|
182 |
-
const newCaption = await see({
|
183 |
-
prompt: "please caption the following image",
|
184 |
-
imageBase64: rendered.assetUrl
|
185 |
-
})
|
186 |
-
if (newCaption) {
|
187 |
-
setCaption(newCaption)
|
188 |
-
}
|
189 |
-
} catch (err) {
|
190 |
-
console.error(`failed to generate the caption:`, err)
|
191 |
-
}
|
192 |
-
})
|
193 |
-
}, [rendered.assetUrl, caption])
|
194 |
-
*/
|
195 |
-
|
196 |
-
const frameClassName = cn(
|
197 |
-
//`flex`,
|
198 |
-
`w-full h-full`,
|
199 |
-
`border-stone-800`,
|
200 |
-
`transition-all duration-200 ease-in-out`,
|
201 |
-
zoomLevel > 140 ? `border-[2px] md:border-[4px] rounded-sm md:rounded-md` :
|
202 |
-
zoomLevel > 120 ? `border-[1.5px] md:border-[3px] rounded-xs md:rounded-sm` :
|
203 |
-
zoomLevel > 90 ? `border-[1px] md:border-[2px] rounded-xs md:rounded-sm` :
|
204 |
-
zoomLevel > 40 ? `border-[0.5px] md:border-[1px] rounded-none md:rounded-xs` :
|
205 |
-
`border-transparent md:border-[0.5px] rounded-none md:rounded-none`,
|
206 |
-
`shadow-sm`,
|
207 |
-
`overflow-hidden`,
|
208 |
-
`print:border-[1.5px] print:shadow-none`,
|
209 |
-
)
|
210 |
-
|
211 |
-
|
212 |
-
/*
|
213 |
-
text detection (doesn't work)
|
214 |
-
useEffect(() => {
|
215 |
-
const fn = async () => {
|
216 |
-
if (!rendered.assetUrl || !ref.current) {
|
217 |
-
return
|
218 |
-
}
|
219 |
-
|
220 |
-
const result = await replaceTextInSpeechBubbles(
|
221 |
-
rendered.assetUrl,
|
222 |
-
"Lorem ipsum dolor sit amet, dolor ipsum. Sit amet? Ipsum! Dolor!!!"
|
223 |
-
)
|
224 |
-
if (result) {
|
225 |
-
setImageWithText(result)
|
226 |
-
}
|
227 |
-
}
|
228 |
-
fn()
|
229 |
-
|
230 |
-
}, [rendered.assetUrl, ref.current])
|
231 |
-
*/
|
232 |
-
|
233 |
-
if (prompt && !rendered.assetUrl) {
|
234 |
-
return (
|
235 |
-
<div className={cn(
|
236 |
-
frameClassName,
|
237 |
-
`flex flex-col items-center justify-center`,
|
238 |
-
className,
|
239 |
-
)}>
|
240 |
-
<Progress isLoading />
|
241 |
-
</div>
|
242 |
-
)
|
243 |
-
}
|
244 |
-
|
245 |
-
return (
|
246 |
-
<div className={cn(
|
247 |
-
frameClassName,
|
248 |
-
{ "grayscale": preset.color === "grayscale" },
|
249 |
-
className
|
250 |
-
)}>
|
251 |
-
<div className={cn(
|
252 |
-
``,
|
253 |
-
`bg-stone-50`,
|
254 |
-
`border-stone-800`,
|
255 |
-
`transition-all duration-200 ease-in-out`,
|
256 |
-
zoomLevel > 140 ? `border-b-[2px] md:border-b-[4px]` :
|
257 |
-
zoomLevel > 120 ? `border-b-[1.5px] md:border-b-[3px]` :
|
258 |
-
zoomLevel > 90 ? `border-b-[1px] md:border-b-[2px]` :
|
259 |
-
zoomLevel > 40 ? `border-b-[0.5px] md:border-b-[1px]` :
|
260 |
-
`border-transparent md:border-b-[0.5px]`,
|
261 |
-
`print:border-b-[1.5px]`,
|
262 |
-
`truncate`,
|
263 |
-
|
264 |
-
zoomLevel > 200 ? `p-4 md:p-8` :
|
265 |
-
zoomLevel > 180 ? `p-[14px] md:p-8` :
|
266 |
-
zoomLevel > 160 ? `p-[12px] md:p-[28px]` :
|
267 |
-
zoomLevel > 140 ? `p-[10px] md:p-[26px]` :
|
268 |
-
zoomLevel > 120 ? `p-2 md:p-6` :
|
269 |
-
zoomLevel > 100 ? `p-1.5 md:p-[20px]` :
|
270 |
-
zoomLevel > 90 ? `p-1.5 md:p-4` :
|
271 |
-
zoomLevel > 40 ? `p-1 md:p-2` :
|
272 |
-
`p-0.5 md:p-2`,
|
273 |
-
|
274 |
-
zoomLevel > 220 ? `text-xl md:text-4xl` :
|
275 |
-
zoomLevel > 200 ? `text-lg md:text-3xl` :
|
276 |
-
zoomLevel > 180 ? `text-md md:text-2xl` :
|
277 |
-
zoomLevel > 140 ? `text-2xs md:text-2xl` :
|
278 |
-
zoomLevel > 120 ? `text-3xs md:text-xl` :
|
279 |
-
zoomLevel > 100 ? `text-4xs md:text-lg` :
|
280 |
-
zoomLevel > 90 ? `text-5xs md:text-sm` :
|
281 |
-
zoomLevel > 40 ? `md:text-xs` : `md:text-2xs`,
|
282 |
-
|
283 |
-
showCaptions ? (
|
284 |
-
zoomLevel > 90 ? `block` : `hidden md:block`
|
285 |
-
) : `hidden`,
|
286 |
-
)}
|
287 |
-
>{caption || ""}
|
288 |
-
</div>
|
289 |
-
{rendered.assetUrl &&
|
290 |
-
<img
|
291 |
-
ref={ref}
|
292 |
-
src={imageWithText || rendered.assetUrl}
|
293 |
-
width={width}
|
294 |
-
height={height}
|
295 |
-
alt={rendered.alt}
|
296 |
-
className={cn(
|
297 |
-
`comic-panel w-full h-full object-cover max-w-max`,
|
298 |
-
// showCaptions ? `-mt-11` : ''
|
299 |
-
)}
|
300 |
-
/>}
|
301 |
-
</div>
|
302 |
-
)
|
303 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/interface/top-menu/index.tsx
CHANGED
@@ -45,7 +45,7 @@ export function TopMenu() {
|
|
45 |
)}>
|
46 |
<div className="flex flex-row flex-grow w-full">
|
47 |
<Input
|
48 |
-
placeholder={`
|
49 |
className="w-full bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800 rounded-r-none"
|
50 |
// disabled={atLeastOnePanelIsBusy}
|
51 |
onChange={(e) => {
|
|
|
45 |
)}>
|
46 |
<div className="flex flex-row flex-grow w-full">
|
47 |
<Input
|
48 |
+
placeholder={`Invent a location e.g. Jurassic Park entrance, Spaceport in Mos Eisley..`}
|
49 |
className="w-full bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800 rounded-r-none"
|
50 |
// disabled={atLeastOnePanelIsBusy}
|
51 |
onChange={(e) => {
|
src/app/landing.tsx
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useEffect, useState, useTransition } from "react"
|
4 |
+
|
5 |
+
import { Post } from "@/types"
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
import { actionman } from "@/lib/fonts"
|
8 |
+
|
9 |
+
|
10 |
+
import { getLatestPosts } from "./engine/community"
|
11 |
+
|
12 |
+
export default function Landing() {
|
13 |
+
const [_isPending, startTransition] = useTransition()
|
14 |
+
const [posts, setPosts] = useState<Post[]>([])
|
15 |
+
|
16 |
+
useEffect(() => {
|
17 |
+
startTransition(async () => {
|
18 |
+
const newPosts = await getLatestPosts()
|
19 |
+
setPosts(newPosts)
|
20 |
+
})
|
21 |
+
}, [])
|
22 |
+
|
23 |
+
return (
|
24 |
+
<div className={cn(
|
25 |
+
`light fixed w-full h-full flex flex-col items-center bg-slate-300 text-slate-800`,
|
26 |
+
`pt-24`,
|
27 |
+
actionman.className
|
28 |
+
)}>
|
29 |
+
<div className="w-full flex flex-col items-center">
|
30 |
+
<h1 className="text-[100px] text-cyan-700">🌐 Panoremix</h1>
|
31 |
+
<h2 className="text-3xl mb-12">Generate cool panoramas using AI!</h2>
|
32 |
+
|
33 |
+
<h2 className="text-2xl">Latest locations synthesized:</h2>
|
34 |
+
|
35 |
+
<div className="grid grid-col-2 sm:grid-col-3 md:grid-col-4 lg:grid-cols-5 gap-4">
|
36 |
+
{posts.map(post => (
|
37 |
+
<div key={post.postId} className="flex flex-col space-y-3">
|
38 |
+
<div className="w-full h-24">
|
39 |
+
<img
|
40 |
+
src={post.assetUrl}
|
41 |
+
className="w-full h-full rounded-xl overflow-hidden"
|
42 |
+
/>
|
43 |
+
</div>
|
44 |
+
<div className="text-base truncate w-full">{post.prompt}</div>
|
45 |
+
</div>
|
46 |
+
))}
|
47 |
+
</div>
|
48 |
+
</div>
|
49 |
+
</div>
|
50 |
+
)
|
51 |
+
}
|
src/app/page.tsx
CHANGED
@@ -2,12 +2,14 @@
|
|
2 |
|
3 |
import Head from "next/head"
|
4 |
|
5 |
-
import
|
|
|
|
|
6 |
import { TooltipProvider } from "@/components/ui/tooltip"
|
7 |
|
8 |
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
9 |
|
10 |
-
export default async function
|
11 |
return (
|
12 |
<>
|
13 |
<Head>
|
@@ -19,7 +21,7 @@ export default async function IndexPage({ params: { ownerId } }: { params: { own
|
|
19 |
`light bg-zinc-50 text-stone-900
|
20 |
`}>
|
21 |
<TooltipProvider delayDuration={100}>
|
22 |
-
<
|
23 |
</TooltipProvider>
|
24 |
</main>
|
25 |
</>
|
|
|
2 |
|
3 |
import Head from "next/head"
|
4 |
|
5 |
+
// import Landing from "./landing"
|
6 |
+
import Generate from "./generate/page"
|
7 |
+
|
8 |
import { TooltipProvider } from "@/components/ui/tooltip"
|
9 |
|
10 |
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
11 |
|
12 |
+
export default async function Page() {
|
13 |
return (
|
14 |
<>
|
15 |
<Head>
|
|
|
21 |
`light bg-zinc-50 text-stone-900
|
22 |
`}>
|
23 |
<TooltipProvider delayDuration={100}>
|
24 |
+
<Generate />
|
25 |
</TooltipProvider>
|
26 |
</main>
|
27 |
</>
|
src/app/todo.tsx
DELETED
@@ -1,21 +0,0 @@
|
|
1 |
-
"use client"
|
2 |
-
|
3 |
-
import { useState, useTransition } from "react"
|
4 |
-
|
5 |
-
import { Post } from "@/types"
|
6 |
-
|
7 |
-
export default function Main() {
|
8 |
-
const [_isPending, startTransition] = useTransition()
|
9 |
-
const posts = useState<Post[]>([])
|
10 |
-
|
11 |
-
return (
|
12 |
-
<div>
|
13 |
-
<h1>Panoremix</h1>
|
14 |
-
<h2>Generate 360° panoramas from text!</h2>
|
15 |
-
|
16 |
-
<h2>Explore latent locations discovered by the community</h2>
|
17 |
-
|
18 |
-
|
19 |
-
</div>
|
20 |
-
)
|
21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/types.ts
CHANGED
@@ -88,26 +88,31 @@ export type RenderingEngine =
|
|
88 |
| "OPENAI"
|
89 |
| "REPLICATE"
|
90 |
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
88 |
| "OPENAI"
|
89 |
| "REPLICATE"
|
90 |
|
91 |
+
export type PostVisibility =
|
92 |
+
| "featured" // featured by admins
|
93 |
+
| "trending" // top trending / received more than 10 upvotes
|
94 |
+
| "normal" // default visibility
|
95 |
+
|
96 |
+
export type Post = {
|
97 |
+
postId: string
|
98 |
+
appId: string
|
99 |
+
prompt: string
|
100 |
+
previewUrl: string
|
101 |
+
assetUrl: string
|
102 |
+
createdAt: string
|
103 |
+
visibility: PostVisibility
|
104 |
+
upvotes: number
|
105 |
+
downvotes: number
|
106 |
+
}
|
107 |
+
|
108 |
+
export type CreatePostResponse = {
|
109 |
+
success?: boolean
|
110 |
+
error?: string
|
111 |
+
post: Post
|
112 |
+
}
|
113 |
+
|
114 |
+
export type GetAppPostsResponse = {
|
115 |
+
success?: boolean
|
116 |
+
error?: string
|
117 |
+
posts: Post[]
|
118 |
+
}
|