Commit
β’
18e5e63
1
Parent(s):
f52fb0f
added community layer
Browse files- src/app/interface/about/index.tsx +1 -1
- src/app/interface/firehose/delete.tsx +60 -0
- src/app/interface/firehose/page.tsx +102 -0
- src/app/interface/generate/index.tsx +15 -2
- src/app/server/actions/censorship.ts +184 -0
- src/app/server/actions/community.ts +258 -0
- src/types.ts +2 -1
src/app/interface/about/index.tsx
CHANGED
@@ -22,7 +22,7 @@ export function About() {
|
|
22 |
</DialogHeader>
|
23 |
<div className="grid gap-4 py-4 text-stone-800">
|
24 |
<p className="">
|
25 |
-
The model used by the AI Clip Factory depends on what I
|
26 |
</p>
|
27 |
<p>
|
28 |
π Right now it uses an API that you can <a className="text-stone-600 underline" href="https://github.com/jbilcke-hf/Hotshot-XL-Gradio-API" target="_blank">fork from here</a>. This API is based on the amazing work made by <a className="text-stone-600 underline" href="https://huggingface.co/fffiloni" target="_blank">@fffiloni</a> for his super cool <a className="text-stone-600 underline" href="https://huggingface.co/spaces/fffiloni/text-to-gif" target="_blank">Hotshot-XL Space</a>.
|
|
|
22 |
</DialogHeader>
|
23 |
<div className="grid gap-4 py-4 text-stone-800">
|
24 |
<p className="">
|
25 |
+
The model used by the AI Clip Factory depends on what I'm experimenting at the current time.
|
26 |
</p>
|
27 |
<p>
|
28 |
π Right now it uses an API that you can <a className="text-stone-600 underline" href="https://github.com/jbilcke-hf/Hotshot-XL-Gradio-API" target="_blank">fork from here</a>. This API is based on the amazing work made by <a className="text-stone-600 underline" href="https://huggingface.co/fffiloni" target="_blank">@fffiloni</a> for his super cool <a className="text-stone-600 underline" href="https://huggingface.co/spaces/fffiloni/text-to-gif" target="_blank">Hotshot-XL Space</a>.
|
src/app/interface/firehose/delete.tsx
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { startTransition, useEffect, useState } from "react"
|
2 |
+
|
3 |
+
import { Button } from "@/components/ui/button"
|
4 |
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
import { Post } from "@/types"
|
7 |
+
import { deletePost } from "@/app/server/actions/community"
|
8 |
+
|
9 |
+
|
10 |
+
export function Delete({ post, moderationKey = "", onDelete = () => {} }: { post?: Post, moderationKey?: string; onDelete: (post: Post) => void }) {
|
11 |
+
const [isOpen, setOpen] = useState(false)
|
12 |
+
|
13 |
+
useEffect(() => {
|
14 |
+
if (post?.postId && !isOpen) {
|
15 |
+
setOpen(true)
|
16 |
+
}
|
17 |
+
}, [post?.postId])
|
18 |
+
|
19 |
+
const handleDelete = () => {
|
20 |
+
startTransition(() => {
|
21 |
+
const fn = async () => {
|
22 |
+
setOpen(false)
|
23 |
+
if (!post) { return }
|
24 |
+
const postId = post.postId
|
25 |
+
await deletePost({ postId, moderationKey })
|
26 |
+
onDelete(post)
|
27 |
+
}
|
28 |
+
fn()
|
29 |
+
})
|
30 |
+
}
|
31 |
+
|
32 |
+
return (
|
33 |
+
<Dialog open={isOpen} onOpenChange={setOpen}>
|
34 |
+
<DialogContent className="sm:max-w-[800px]">
|
35 |
+
<DialogHeader>
|
36 |
+
<DialogTitle>Delete</DialogTitle>
|
37 |
+
</DialogHeader>
|
38 |
+
{post ?<div className="flex flex-col py-4 text-stone-800">
|
39 |
+
|
40 |
+
<div className="w-full h-64">
|
41 |
+
<img
|
42 |
+
src={post.assetUrl}
|
43 |
+
className={cn(
|
44 |
+
`w-full h-64 rounded-xl overflow-hidden object-cover`,
|
45 |
+
`border border-zinc-900/70`
|
46 |
+
)}
|
47 |
+
/>
|
48 |
+
</div>
|
49 |
+
<div className="text-lg text-stone-800/80 word-break w-full py-6">{post.prompt}</div>
|
50 |
+
</div> : null}
|
51 |
+
<DialogFooter>
|
52 |
+
<div className="w-full flex flex-row space-x-6 items-center justify-center">
|
53 |
+
<Button type="submit" className="text-xl bg-green-800 text-green-100 hover:bg-green-700 hover:text-green-50" onClick={() => setOpen(false)}>Keep</Button>
|
54 |
+
<Button type="submit" className="text-xl bg-red-800 text-red-100 hover:bg-red-700 hover:text-red-50" onClick={handleDelete}>Delete</Button>
|
55 |
+
</div>
|
56 |
+
</DialogFooter>
|
57 |
+
</DialogContent>
|
58 |
+
</Dialog>
|
59 |
+
)
|
60 |
+
}
|
src/app/interface/firehose/page.tsx
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
import { useSearchParams } from "next/navigation"
|
10 |
+
import { Button } from "@/components/ui/button"
|
11 |
+
import { Delete } from "./delete"
|
12 |
+
import Link from "next/link"
|
13 |
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
14 |
+
import { getLatestPosts } from "@/app/server/actions/community"
|
15 |
+
|
16 |
+
export default function FirehosePage() {
|
17 |
+
const searchParams = useSearchParams()
|
18 |
+
const [_isPending, startTransition] = useTransition()
|
19 |
+
const [posts, setPosts] = useState<Post[]>([])
|
20 |
+
const moderationKey = (searchParams.get("moderationKey") as string) || ""
|
21 |
+
const [toDelete, setToDelete] = useState<Post>()
|
22 |
+
|
23 |
+
useEffect(() => {
|
24 |
+
startTransition(async () => {
|
25 |
+
const newPosts = await getLatestPosts({
|
26 |
+
maxNbPosts: 40
|
27 |
+
})
|
28 |
+
setPosts(newPosts)
|
29 |
+
})
|
30 |
+
}, [])
|
31 |
+
|
32 |
+
const handleOnDelete = ({ postId }: Post) => {
|
33 |
+
setPosts(posts.filter(post => post.postId !== postId))
|
34 |
+
setToDelete(undefined)
|
35 |
+
}
|
36 |
+
|
37 |
+
return (
|
38 |
+
<TooltipProvider delayDuration={100}>
|
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">π Panoremix</h1>
|
47 |
+
<h2 className="text-3xl mb-6">Generate cool panoramas using AI!</h2>
|
48 |
+
<h2 className="text-2xl">Latest locations synthesized:</h2>
|
49 |
+
</div>
|
50 |
+
|
51 |
+
<div className="w-full grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-4 gap-y-6 px-12">
|
52 |
+
{posts.map(post => (
|
53 |
+
<Link
|
54 |
+
key={post.postId}
|
55 |
+
href={`/?postId=${post.postId}`}
|
56 |
+
target="_blank">
|
57 |
+
<div
|
58 |
+
key={post.postId}
|
59 |
+
className="group flex flex-col cursor-pointer"
|
60 |
+
>
|
61 |
+
<div className="w-full h-32">
|
62 |
+
{moderationKey ? <div className="relative -mb-8 ml-2">
|
63 |
+
<Button
|
64 |
+
className="z-30 bg-red-200 text-red-700 hover:bg-red-300 hover:text-red-800 text-2xs px-2 h-7"
|
65 |
+
onClick={(e) => {
|
66 |
+
e.preventDefault()
|
67 |
+
setToDelete(post)
|
68 |
+
return false
|
69 |
+
}}>Delete</Button>
|
70 |
+
</div> : null}
|
71 |
+
<img
|
72 |
+
src={post.assetUrl}
|
73 |
+
className={cn(
|
74 |
+
`w-full h-32 rounded-xl overflow-hidden object-cover`,
|
75 |
+
`border border-zinc-900/70`,
|
76 |
+
// `group-hover:brightness-105`
|
77 |
+
)}
|
78 |
+
/>
|
79 |
+
</div>
|
80 |
+
<Tooltip>
|
81 |
+
<TooltipTrigger asChild>
|
82 |
+
<div
|
83 |
+
className="text-base text-stone-900/80 truncate w-full group-hover:underline underline-offset-2"
|
84 |
+
>{post.prompt}</div>
|
85 |
+
</TooltipTrigger>
|
86 |
+
<TooltipContent>
|
87 |
+
<p className="w-full max-w-xl">{post.prompt}</p>
|
88 |
+
</TooltipContent>
|
89 |
+
</Tooltip>
|
90 |
+
<div
|
91 |
+
className="text-sm text-stone-700/70 w-full group-hover:underline underline-offset-2"
|
92 |
+
>{new Date(Date.parse(post.createdAt)).toLocaleString()}</div>
|
93 |
+
</div>
|
94 |
+
</Link>
|
95 |
+
))}
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
<Delete post={toDelete} moderationKey={moderationKey} onDelete={handleOnDelete} />
|
99 |
+
</div>
|
100 |
+
</TooltipProvider>
|
101 |
+
)
|
102 |
+
}
|
src/app/interface/generate/index.tsx
CHANGED
@@ -7,6 +7,7 @@ import { cn } from "@/lib/utils"
|
|
7 |
import { headingFont } from "@/app/interface/fonts"
|
8 |
import { useCharacterLimit } from "@/lib/useCharacterLimit"
|
9 |
import { generateAnimation } from "@/app/server/actions/animation"
|
|
|
10 |
|
11 |
export function Generate() {
|
12 |
const [_isPending, startTransition] = useTransition()
|
@@ -42,13 +43,15 @@ export function Generate() {
|
|
42 |
if (!promptDraft) { return }
|
43 |
setLocked(true)
|
44 |
startTransition(async () => {
|
|
|
|
|
45 |
try {
|
46 |
console.log("starting transition, calling generateAnimation")
|
47 |
const newAssetUrl = await generateAnimation({
|
48 |
positivePrompt: promptDraft,
|
49 |
negativePrompt: "",
|
50 |
-
huggingFaceLora
|
51 |
-
triggerWord
|
52 |
// huggingFaceLora: "veryVANYA/ps1-graphics-sdxl-v2", //
|
53 |
// huggingFaceLora: "ostris/crayon_style_lora_sdxl", // "https://huggingface.co/ostris/crayon_style_lora_sdxl/resolve/main/crayons_v1_sdxl.safetensors",
|
54 |
// replicateLora: "https://replicate.com/jbilcke/sdxl-panorama",
|
@@ -77,6 +80,16 @@ export function Generate() {
|
|
77 |
steps: 25,
|
78 |
})
|
79 |
setAssetUrl(newAssetUrl)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
} catch (err) {
|
81 |
console.error(err)
|
82 |
} finally {
|
|
|
7 |
import { headingFont } from "@/app/interface/fonts"
|
8 |
import { useCharacterLimit } from "@/lib/useCharacterLimit"
|
9 |
import { generateAnimation } from "@/app/server/actions/animation"
|
10 |
+
import { postToCommunity } from "@/app/server/actions/community"
|
11 |
|
12 |
export function Generate() {
|
13 |
const [_isPending, startTransition] = useTransition()
|
|
|
43 |
if (!promptDraft) { return }
|
44 |
setLocked(true)
|
45 |
startTransition(async () => {
|
46 |
+
const huggingFaceLora = "KappaNeuro/studio-ghibli-style"
|
47 |
+
const triggerWord = "Studio Ghibli Style"
|
48 |
try {
|
49 |
console.log("starting transition, calling generateAnimation")
|
50 |
const newAssetUrl = await generateAnimation({
|
51 |
positivePrompt: promptDraft,
|
52 |
negativePrompt: "",
|
53 |
+
huggingFaceLora,
|
54 |
+
triggerWord,
|
55 |
// huggingFaceLora: "veryVANYA/ps1-graphics-sdxl-v2", //
|
56 |
// huggingFaceLora: "ostris/crayon_style_lora_sdxl", // "https://huggingface.co/ostris/crayon_style_lora_sdxl/resolve/main/crayons_v1_sdxl.safetensors",
|
57 |
// replicateLora: "https://replicate.com/jbilcke/sdxl-panorama",
|
|
|
80 |
steps: 25,
|
81 |
})
|
82 |
setAssetUrl(newAssetUrl)
|
83 |
+
|
84 |
+
try {
|
85 |
+
await postToCommunity({
|
86 |
+
prompt: promptDraft,
|
87 |
+
model: huggingFaceLora,
|
88 |
+
assetUrl: newAssetUrl,
|
89 |
+
})
|
90 |
+
} catch (err) {
|
91 |
+
console.error(`not a blocked, but we failed to post to the community (reason: ${err})`)
|
92 |
+
}
|
93 |
} catch (err) {
|
94 |
console.error(err)
|
95 |
} finally {
|
src/app/server/actions/censorship.ts
ADDED
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
// I don't want to be banned by Replicate because bad actors are asking
|
3 |
+
// for some naked anime stuff or whatever
|
4 |
+
// I also want to avoid a PR scandal due to some bad user generated content
|
5 |
+
|
6 |
+
import { computeSecretFingerprint } from "@/lib/computeSecretFingerprint"
|
7 |
+
|
8 |
+
// those keywords have been generated by looking at the logs of the panorama and the AI Comic Factory
|
9 |
+
// those are real requests some users tried to attempt.. :|
|
10 |
+
|
11 |
+
const chickens = [
|
12 |
+
"fcb4dacbd99b21368c50f29c1d47071c87cf2225ab9192282c785460391cd365",
|
13 |
+
"68840b60ac27eacaa7afe17e898d3c4a2dc71acff8c74d6782c1bcaafd14963d",
|
14 |
+
"67f745224fd6e1a7a3a244514d5807fcc994cbb62ca4ec8fa44cd14244a515ae",
|
15 |
+
"681fea565117808c6dbe002520d2cfeeb3e5c67e68630afb4a453449a9da587b",
|
16 |
+
"2f3d913b3db9e15a930aac43eb2d6fe8817db8e4bcf37794bf0227b06b718d1b",
|
17 |
+
"922a700b807e4994df82eba2b48a6ac131fe8d8d1035d06b3592d622fb232161",
|
18 |
+
"cb69ee6774eafcc720adb1f689d28acbb9f47998cbea0299ec66a58dedf91c37"
|
19 |
+
]
|
20 |
+
|
21 |
+
const ducks = [
|
22 |
+
"1c52cb20c0cbc76349fa63232b982bd394cf0850ebc17240dcf33c19fb15a26d",
|
23 |
+
"e1d4de9b8d464d7da07c276b63a42c1c9922224f0a6cab6b0826427ce4a7461a",
|
24 |
+
"0be3174bfb1a48a65875c2f035b1ae14fbc8f232f55785018de0cfe2132fa952",
|
25 |
+
"0f174769641b2e5d2c79b5a83e8ef91e004f6f3e62531cd70cfdff02159268cb",
|
26 |
+
"e9fb8ae8ff720acd91025229478a21e43e8e976e30119a76c293201adf572736",
|
27 |
+
"f65a0dc0e07b5d084ff24c69dcdb953f7b57101d2ebb716d4dfb5963076ef807",
|
28 |
+
"2bf38af1646489c2c086f811d082054cd29e23fa7bb5c525396bec01b3ab688e"
|
29 |
+
]
|
30 |
+
|
31 |
+
const cats = [
|
32 |
+
"fcffc3e997d952007d1b902a9cf40b750ba4a410ac65bfd95475996bf51359e4",
|
33 |
+
"3172a5fa159754d703489dfba5af520b8ace107cdf170f4c4cb38a6797aa163f",
|
34 |
+
"500012dbff4498a9c4513369d6b9b373fab9330ffd2cb1e622294043cc21b610",
|
35 |
+
"84e3a8d34ee7d0c8e7a2926dd1acad46a0b66b9d27725b3a7e5053550f490301"
|
36 |
+
]
|
37 |
+
|
38 |
+
const roasted = [
|
39 |
+
"a2bfbce0046c9a52a0eabf98f73e0f8e09959970431fc892ebdb4e1c97031b50",
|
40 |
+
"6eca1adf06851f99e9cdfbb496c27d46ff81106903d11f3346a146e96082b016",
|
41 |
+
"49a124c9ed6fbbad4105b3657dc25de369bcafb9d6787f610c08f584cd607d0f",
|
42 |
+
"c3afb59420c812cbc7c8f57ad3e8d79407f10106a99f829aa65316c99d0b29c4",
|
43 |
+
"2b808858836a5c205080f5b93201ef92e098cff931d8de6d9f20dc722997d077",
|
44 |
+
"07bef89d1a7d63c9c5ed64ba0f73d6cff689811847c2e20c8b3fbfb060e1d64e",
|
45 |
+
"baeb994922d5473f534aa54322d83effe74c6c4dac807e6b523a677d7acdc17b",
|
46 |
+
"ea4735a879edd5cc94ca7db26edd5a970df69a41f0009d3444486647e44175af",
|
47 |
+
"f2412249030454cd13ac6f7965871d924c16daacda0123de81892adb19ce49ac",
|
48 |
+
"9958c56e12bab8549cf752bcd8bec4ac36cf79c404b1faf5611f057bb71bc0e1",
|
49 |
+
"76cdade0b3d4caf0888f60318a5cbca00f830a3b0bf37735fc64fdaeb67c34d3",
|
50 |
+
"1bf53c97869e1ea89bda19da64a9173d48fe4ec823e949e2c898f8abb3fbf457",
|
51 |
+
"1bf53c97869e1ea89bda19da64a9173d48fe4ec823e949e2c898f8abb3fbf457",
|
52 |
+
"3d7f973fab8f4a19c0a3e59efe970ed7bd55a1cb795752d9cbe3c19e8a7d81ec"
|
53 |
+
]
|
54 |
+
|
55 |
+
const banned = [
|
56 |
+
"8a05d4869d9d6ce388c6cd2db13ca12b88097b90f9be027d5ffaaa467c7a6e5e",
|
57 |
+
"0c475212a608138244c5fc150b1563e5ef79c516234fd78dcd5993f726c359a0",
|
58 |
+
"df17388805f99f2ff3e5ae97a0f55e5c927eb47f17ca65822bf8c88f02bac3dd",
|
59 |
+
"86c3355d1bd581cdf7306729d8dd0ee9b7a317b9cfd6d7a6f5fad9c0dafe2167",
|
60 |
+
"23a2484cd420c9ffbfcc2c0075a9b330664450ced1fc64ab6a65e278086b8c6e",
|
61 |
+
"fb4cabe709b62eea1b4cc0030c76f5e4a43ee677ce19124e8e7bafa86c78ab66",
|
62 |
+
"d99c26daee85f7dc81c46c061a5874cff7179ed72d884d2316d664d36ffe7ab5",
|
63 |
+
"b93c38af5aa221d76c60ee3eb762efee0cdb0daf29ceb235b7dda6d46c06490d",
|
64 |
+
"8cf6c8765dc757319461dd9a785e77c201b8e5a604d36b817cd987c6a5e62500",
|
65 |
+
"f4a1cb290745717f86c3cee30fc324c0d80a9945fcbc7bbeb010579f58792f1e",
|
66 |
+
"7c87c47c42fc983119551342be9ddd5b32e530c0504ccdbbaa1e12b1d9f1bbcb",
|
67 |
+
"d04fad4f21d030da7a1301afbf480ef6246eb7bbf0f26e31865b2e015a25f747",
|
68 |
+
"d685ff22fb9da01ee949db212770729603989850864ef7a7085e1f086cfa7deb",
|
69 |
+
"533b90588d9ccf7967da54691f575e9fd4926c6e0b5fd94a47b932bcea270bee",
|
70 |
+
"9c2d61f28f5bb7f3f1dc9122be64cda8a428b46ce68b70120da4c41dba96ba4c",
|
71 |
+
"5d4b1a3eebe64dfa631d0e3b084bd96ee9364c3669269f838ca17a4900276264",
|
72 |
+
"d56f56413b9679fc0820a2c0237224ded8554c61fab8959c174123c8b68ba029",
|
73 |
+
"323a9ab60739726070d615ff3a05d7ff6bb6e3c4dd9ff16ce24f253ecd7b8851",
|
74 |
+
"975c6739de7d4999db15972f707f5f4e95649275f1c0c48e895b8c537e8638ec",
|
75 |
+
"67ee26eb9e1c1c7124797321b02bca90a19c18171782917cd4a487b722484dce",
|
76 |
+
"6df5aa7b72a4e6e3fb726489ff1437daa5752047507f4da912680b1d6647c7d6",
|
77 |
+
"b0864805364359e8c5810c233b1bf2c74dedce9055ae5f7680ba05b4e39db8e2",
|
78 |
+
"a8f841472ecffdd6266151148320c8e36847a24ead9d3338e0313b075c16649d",
|
79 |
+
"f9b127cd90e85b0ff68dd220361671663f0154b2b827f1f7ea797b020ca0018c",
|
80 |
+
"d5c20e9a1ecf01c82da24c514d867498b3e5f522adc1523ce29404a6563641d5",
|
81 |
+
"241022b49d7c0aba24a61eea1137a804f36e4bcb47af42950275baac9b4e7aac",
|
82 |
+
"fc99a70e17b6c86ef1b537654b0f50353567a7b59912c3ba955f3fca4d1ea696",
|
83 |
+
"255306e968009003d295cb2a7256f27bfcdb5d1743bf4d9f2aa4b8adf1a7734d",
|
84 |
+
"048c7b709763dd9c43794d241c369f0abcb079d546ddcbbba9968a1ed1da7ed7",
|
85 |
+
"520cbfeef3e4c405d79478eedccb97a4d476be585626dd2b1c53292797491bc7",
|
86 |
+
"f9f28a7ae7e8b1719b350a04dc087a4b8e33478d109ceeef6ba892b32d1105c9",
|
87 |
+
"d177f1bfe603647ef4c1c0e6f1a7172081fb9bbc2ea859705949f2c5aa5d4f22",
|
88 |
+
"302feef2c09247fbd23789581f7f5e2219f88ae0a937880954938573c2a52a84",
|
89 |
+
"99edd6f57b864873835f16f19c805dd94bed9da8967b84e3a62782f106d9ebcc",
|
90 |
+
"e75e5f01dcd8351c9553e89558085bd68e6feb295dee5d8da0c9b43ee303ce36",
|
91 |
+
"135e52a026aea9d2e12de358a85e05cf21121a18269269b7c62678c3bc846f5b",
|
92 |
+
"28e5b2d3eb5f1ef4cc7b570878b03acf303a6ca4ca95893591e0fb943b0beab0",
|
93 |
+
"a26b26340f8d0363633490556d20bcc250726d10e1431eb8c22d6b1ff3f2b14a",
|
94 |
+
"27e4ddde96ec6a1dbe1cf12d79448b3e72f144944c15b299629542d1b65fbabf",
|
95 |
+
"efd9c0a391ee93251046a58326d1b21b33fe21d71a3fb1855b9048ade53df77c",
|
96 |
+
"6d505fcce416c26a606878aab4d249a034ba2a9846cb1f883e0f9e3fb76ba6da",
|
97 |
+
"3a37b8a1b72f9bca51233536d50f9c8d33a787434684787871e0049c82347cda",
|
98 |
+
"16f9b451184a7c3148344c7d0315f5312ca20553d2271912ecaad91810d977e6",
|
99 |
+
"7406537eb74d1885bd05e191228de313b13702a64d90ae1736c6377b25ab579a",
|
100 |
+
"7e4d1395ae18980015cab16c85ffa20b4cb90a2db594126e893d0f7ac6eecaa8",
|
101 |
+
"ba813ee6c25698f0f68a07121d38bb47c9aa404c1ab0a6e767595cb75e1747b8",
|
102 |
+
"6586c93f3ece83e01ecc1eb84a7711e7975826a388d478a009468ea0ed9dc03e",
|
103 |
+
"8960174c74d86e03ae88fb6774580170e49952f2286d960be08c556bbd0dda95",
|
104 |
+
"4d611454369aa1a4e2b7eed1734fac5d480f08fb86b87a162967e416370f2a8e",
|
105 |
+
"59d48440f85eabf565fe8d3bc6b973ba64c70df3b36b0511e0e67ceca91762b3",
|
106 |
+
"cd926926e2af74e43d1a6a420a7e1933b78662320477a3c018b2711d8765e339",
|
107 |
+
"80e90057df6a59823f51aafac36ed5bc4e5ac26d675d9c1467501590c82f12d4",
|
108 |
+
"a9cf28b869b70e258adde5639a048f866ec86f8f3f3d53bfc960b86aa6da9239",
|
109 |
+
"cc2adbf8ac0cddeefa304d7b20f14a7e047a4b2299cc5e8f898f5c59660bd964",
|
110 |
+
"92a150a46146e9d3f84899cf15e12514af684e7ee18d7add782ddd4f4a15ef18",
|
111 |
+
"d9b2e84ef6dc0ce449357d52c9095f69b173a1b848ea2921199d33b0ec10024a",
|
112 |
+
"a9329a7e4d367a0135c1ca86c6ce5ecabcc26529235229d71b6bf991f7689e21",
|
113 |
+
"8f160c6fd8ccc3fb2a371a4b52748f0bd030766627c4322e2911fe82f6b10497",
|
114 |
+
"620e96eae4f3e88cbe0770292b33724c5df3866d83f39df6380441f7271c80e2",
|
115 |
+
"cafa3481fa3c45ed1e55cd0129c12b477eeab5aa3d6da20cae6d6292f19b0e6d",
|
116 |
+
"be07994e9a83aa3689e79b6e96123676ccc4fa29f523c28c750c6d60505531ee",
|
117 |
+
"f6498069768cd3aa79b2b0c91879694f05a259c8ee4a6bb343f0435f74eb1b53",
|
118 |
+
"c9b6b26cb3a694eb78fcac0a14ad18d46d50907186a9add41022d31d191b2b65"
|
119 |
+
]
|
120 |
+
|
121 |
+
const young = [
|
122 |
+
"ffdf66787b4a33b78b18c18822e334cfe2c8406caf442851deef451bd43140a1",
|
123 |
+
"858f22219afc4b32a7ba9a27a213d7f495e77c3cceed8147eae5282bf3e23d39",
|
124 |
+
"8c3c46df84ace3d58d4ce0fbc513017986b33c6002ae369d9f7dd1f892a898cb",
|
125 |
+
"66caa22b9483fdf026ce67de61067d81535a7c9b3169cbc5c2a455ac8dcc7bec",
|
126 |
+
"76893047b1eff9fadc7be07b13adb5aaed9c73bcdeea46ee07098605e2c7ff76",
|
127 |
+
"526cb848754e2baaa17376a5693d90ba3f69f71fd2a866f22876ac8a075849a7",
|
128 |
+
"f59c38e31d0f64dc1bfcdf34451723bc1a65570e209e5496c8d1d7f6d3d649db",
|
129 |
+
"e013a67e275c62c1402ccbbb11ad14afb8b8a82318a44c07d67599ed5ac874de",
|
130 |
+
"3bef34219fb07f867ecbff4d6748f598d6cc0761e17dd0d431ee1f4ec3281374",
|
131 |
+
"8211bf5f613fac06cd5d074d34c16dfacc9367c8afaa6ad3aff99d145e5221be"
|
132 |
+
]
|
133 |
+
|
134 |
+
const getFingerprint = (word: string) => {
|
135 |
+
return computeSecretFingerprint(
|
136 |
+
word.toLocaleLowerCase().replaceAll(/[^a-zA-Z0-9]/gi, "")
|
137 |
+
)
|
138 |
+
}
|
139 |
+
|
140 |
+
const encode = (list: string[]) => {
|
141 |
+
console.log(JSON.stringify(
|
142 |
+
list.sort((a, b) => (b.length - a.length))
|
143 |
+
.map(item => getFingerprint(item)), null, 2))
|
144 |
+
}
|
145 |
+
|
146 |
+
// encode([ "badword" ])
|
147 |
+
|
148 |
+
export const filterOutBadWords = (sentence: string) => {
|
149 |
+
if (process.env.ENABLE_CENSORSHIP !== "true") { return sentence }
|
150 |
+
|
151 |
+
let requireCensorship = false
|
152 |
+
|
153 |
+
const words = sentence.replaceAll(/[^a-zA-Z0-9]/gi, " ").replaceAll(/\s+/gi, " ").trim().split(" ")
|
154 |
+
|
155 |
+
const sanitized = words.map(word => {
|
156 |
+
const fingerprint = getFingerprint(word)
|
157 |
+
|
158 |
+
let result: string = word
|
159 |
+
// some users want to play it smart and bypass our system so let's play too
|
160 |
+
if (chickens.includes(fingerprint)) {
|
161 |
+
result = "large chicken"
|
162 |
+
} else if (ducks.includes(fingerprint)) {
|
163 |
+
result = "big duck"
|
164 |
+
} else if (cats.includes(fingerprint)) {
|
165 |
+
result = "cat"
|
166 |
+
} else if (roasted.includes(fingerprint)) {
|
167 |
+
result = "roasted chicken"
|
168 |
+
} else if (young.includes(fingerprint)) {
|
169 |
+
result = "adult"
|
170 |
+
} else if (banned.includes(fingerprint)) {
|
171 |
+
result = "_BANNED_"
|
172 |
+
}
|
173 |
+
|
174 |
+
if (result !== word) {
|
175 |
+
requireCensorship = true
|
176 |
+
}
|
177 |
+
return result
|
178 |
+
}).filter(item => item !== "_BANNED_").join(" ")
|
179 |
+
|
180 |
+
// if the user didn't try to use a bad word, we leave it untouched
|
181 |
+
// he words array has been degraded by the replace operation, but it removes commas etc which isn't great
|
182 |
+
// so if the request was genuine and SFW, it's best to return the original prompt
|
183 |
+
return requireCensorship ? sanitized : sentence
|
184 |
+
}
|
src/app/server/actions/community.ts
ADDED
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { v4 as uuidv4 } from "uuid"
|
4 |
+
|
5 |
+
import { CreatePostResponse, GetAppPostResponse, 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 |
+
const secretModerationKey = `${process.env.MODERATION_KEY || ""}`
|
12 |
+
|
13 |
+
export async function postToCommunity({
|
14 |
+
prompt = "",
|
15 |
+
model = "",
|
16 |
+
assetUrl = "",
|
17 |
+
}: {
|
18 |
+
prompt: string
|
19 |
+
model: string,
|
20 |
+
assetUrl: string
|
21 |
+
}): Promise<Post> {
|
22 |
+
|
23 |
+
const before = prompt
|
24 |
+
prompt = filterOutBadWords(prompt)
|
25 |
+
|
26 |
+
if (prompt !== before) {
|
27 |
+
console.log(`user attempted to use bad words! their original prompt is: ${before}`)
|
28 |
+
}
|
29 |
+
|
30 |
+
// if the community API is disabled,
|
31 |
+
// we don't fail, we just mock
|
32 |
+
if (!apiUrl) {
|
33 |
+
const mockPost: Post = {
|
34 |
+
postId: uuidv4(),
|
35 |
+
appId: "mock",
|
36 |
+
prompt,
|
37 |
+
model,
|
38 |
+
previewUrl: assetUrl,
|
39 |
+
assetUrl,
|
40 |
+
createdAt: new Date().toISOString(),
|
41 |
+
visibility: "normal",
|
42 |
+
upvotes: 0,
|
43 |
+
downvotes: 0
|
44 |
+
}
|
45 |
+
return mockPost
|
46 |
+
}
|
47 |
+
|
48 |
+
if (!prompt) {
|
49 |
+
console.error(`cannot call the community API without a prompt, aborting..`)
|
50 |
+
throw new Error(`cannot call the community API without a prompt, aborting..`)
|
51 |
+
}
|
52 |
+
if (!assetUrl) {
|
53 |
+
console.error(`cannot call the community API without an assetUrl, aborting..`)
|
54 |
+
throw new Error(`cannot call the community API without an assetUrl, aborting..`)
|
55 |
+
}
|
56 |
+
|
57 |
+
try {
|
58 |
+
console.log(`calling POST ${apiUrl}/posts/${appId} with prompt: ${prompt}`)
|
59 |
+
|
60 |
+
const postId = uuidv4()
|
61 |
+
|
62 |
+
const post: Partial<Post> = { postId, appId, prompt, assetUrl }
|
63 |
+
|
64 |
+
console.table(post)
|
65 |
+
|
66 |
+
const res = await fetch(`${apiUrl}/posts/${appId}`, {
|
67 |
+
method: "POST",
|
68 |
+
headers: {
|
69 |
+
Accept: "application/json",
|
70 |
+
"Content-Type": "application/json",
|
71 |
+
Authorization: `Bearer ${apiToken}`,
|
72 |
+
},
|
73 |
+
body: JSON.stringify(post),
|
74 |
+
cache: 'no-store',
|
75 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
76 |
+
// next: { revalidate: 1 }
|
77 |
+
})
|
78 |
+
|
79 |
+
// console.log("res:", res)
|
80 |
+
// The return value is *not* serialized
|
81 |
+
// You can return Date, Map, Set, etc.
|
82 |
+
|
83 |
+
// Recommendation: handle errors
|
84 |
+
if (res.status !== 200) {
|
85 |
+
// This will activate the closest `error.js` Error Boundary
|
86 |
+
throw new Error('Failed to fetch data')
|
87 |
+
}
|
88 |
+
|
89 |
+
const response = (await res.json()) as CreatePostResponse
|
90 |
+
// console.log("response:", response)
|
91 |
+
return response.post
|
92 |
+
} catch (err) {
|
93 |
+
const error = `failed to post to community: ${err}`
|
94 |
+
console.error(error)
|
95 |
+
throw new Error(error)
|
96 |
+
}
|
97 |
+
}
|
98 |
+
|
99 |
+
export async function getLatestPosts({
|
100 |
+
visibility,
|
101 |
+
maxNbPosts = 1000
|
102 |
+
}: {
|
103 |
+
visibility?: PostVisibility
|
104 |
+
maxNbPosts?: number
|
105 |
+
}): Promise<Post[]> {
|
106 |
+
|
107 |
+
let posts: Post[] = []
|
108 |
+
|
109 |
+
// if the community API is disabled we don't fail,
|
110 |
+
// we just mock
|
111 |
+
if (!apiUrl) {
|
112 |
+
return posts
|
113 |
+
}
|
114 |
+
|
115 |
+
try {
|
116 |
+
// console.log(`calling GET ${apiUrl}/posts with renderId: ${renderId}`)
|
117 |
+
// TODO: send the max number of posts
|
118 |
+
const res = await fetch(`${apiUrl}/posts/${appId}/${
|
119 |
+
visibility || "all"
|
120 |
+
}`, {
|
121 |
+
method: "GET",
|
122 |
+
headers: {
|
123 |
+
Accept: "application/json",
|
124 |
+
"Content-Type": "application/json",
|
125 |
+
Authorization: `Bearer ${apiToken}`,
|
126 |
+
},
|
127 |
+
cache: 'no-store',
|
128 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
129 |
+
// next: { revalidate: 1 }
|
130 |
+
})
|
131 |
+
|
132 |
+
// console.log("res:", res)
|
133 |
+
// The return value is *not* serialized
|
134 |
+
// You can return Date, Map, Set, etc.
|
135 |
+
|
136 |
+
// Recommendation: handle errors
|
137 |
+
if (res.status !== 200) {
|
138 |
+
// This will activate the closest `error.js` Error Boundary
|
139 |
+
throw new Error('Failed to fetch data')
|
140 |
+
}
|
141 |
+
|
142 |
+
const response = (await res.json()) as GetAppPostsResponse
|
143 |
+
// console.log("response:", response)
|
144 |
+
|
145 |
+
const posts: Post[] = Array.isArray(response?.posts) ? response?.posts : []
|
146 |
+
posts.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
|
147 |
+
return posts.slice(0, maxNbPosts)
|
148 |
+
} catch (err) {
|
149 |
+
// const error = `failed to get posts: ${err}`
|
150 |
+
// console.error(error)
|
151 |
+
// throw new Error(error)
|
152 |
+
return []
|
153 |
+
}
|
154 |
+
}
|
155 |
+
|
156 |
+
export async function getPost(postId: string): Promise<Post> {
|
157 |
+
|
158 |
+
// if the community API is disabled we don't fail,
|
159 |
+
// we just mock
|
160 |
+
if (!apiUrl) {
|
161 |
+
throw new Error("community API is not enabled")
|
162 |
+
}
|
163 |
+
|
164 |
+
try {
|
165 |
+
// console.log(`calling GET ${apiUrl}/posts with renderId: ${renderId}`)
|
166 |
+
const res = await fetch(`${apiUrl}/posts/${appId}/${postId}`, {
|
167 |
+
method: "GET",
|
168 |
+
headers: {
|
169 |
+
Accept: "application/json",
|
170 |
+
"Content-Type": "application/json",
|
171 |
+
Authorization: `Bearer ${apiToken}`,
|
172 |
+
},
|
173 |
+
cache: 'no-store',
|
174 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
175 |
+
// next: { revalidate: 1 }
|
176 |
+
})
|
177 |
+
|
178 |
+
// console.log("res:", res)
|
179 |
+
// The return value is *not* serialized
|
180 |
+
// You can return Date, Map, Set, etc.
|
181 |
+
|
182 |
+
// Recommendation: handle errors
|
183 |
+
if (res.status !== 200) {
|
184 |
+
// This will activate the closest `error.js` Error Boundary
|
185 |
+
throw new Error('Failed to fetch data')
|
186 |
+
}
|
187 |
+
|
188 |
+
const response = (await res.json()) as GetAppPostResponse
|
189 |
+
// console.log("response:", response)
|
190 |
+
return response.post
|
191 |
+
} catch (err) {
|
192 |
+
const error = `failed to get post: ${err}`
|
193 |
+
console.error(error)
|
194 |
+
throw new Error(error)
|
195 |
+
}
|
196 |
+
}
|
197 |
+
|
198 |
+
export async function deletePost({
|
199 |
+
postId,
|
200 |
+
moderationKey,
|
201 |
+
}: {
|
202 |
+
postId: string
|
203 |
+
moderationKey: string
|
204 |
+
}): Promise<boolean> {
|
205 |
+
|
206 |
+
// if the community API is disabled,
|
207 |
+
// we don't fail, we just mock
|
208 |
+
if (!apiUrl) {
|
209 |
+
return false
|
210 |
+
}
|
211 |
+
|
212 |
+
if (!postId) {
|
213 |
+
console.error(`cannot delete a post without a postId, aborting..`)
|
214 |
+
throw new Error(`cannot delete a post without a postId, aborting..`)
|
215 |
+
}
|
216 |
+
if (!moderationKey) {
|
217 |
+
console.error(`cannot delete a post without a moderationKey, aborting..`)
|
218 |
+
throw new Error(`cannot delete a post without a moderationKey, aborting..`)
|
219 |
+
}
|
220 |
+
|
221 |
+
if (moderationKey !== secretModerationKey) {
|
222 |
+
console.error(`invalid moderation key, operation denied! please ask a Panoremix admin for the mdoeration key`)
|
223 |
+
throw new Error(`invalid moderation key, operation denied! please ask a Panoremix admin for the mdoeration key`)
|
224 |
+
}
|
225 |
+
|
226 |
+
try {
|
227 |
+
console.log(`calling DELETE ${apiUrl}/posts/${appId}/${postId}`)
|
228 |
+
|
229 |
+
const res = await fetch(`${apiUrl}/posts/${appId}/${postId}`, {
|
230 |
+
method: "DELETE",
|
231 |
+
headers: {
|
232 |
+
Accept: "application/json",
|
233 |
+
"Content-Type": "application/json",
|
234 |
+
Authorization: `Bearer ${apiToken}`,
|
235 |
+
},
|
236 |
+
cache: 'no-store',
|
237 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
238 |
+
// next: { revalidate: 1 }
|
239 |
+
})
|
240 |
+
|
241 |
+
// console.log("res:", res)
|
242 |
+
// The return value is *not* serialized
|
243 |
+
// You can return Date, Map, Set, etc.
|
244 |
+
|
245 |
+
// Recommendation: handle errors
|
246 |
+
if (res.status !== 200) {
|
247 |
+
// This will activate the closest `error.js` Error Boundary
|
248 |
+
throw new Error('Failed to fetch data')
|
249 |
+
}
|
250 |
+
|
251 |
+
const response = (await res.json()) as CreatePostResponse
|
252 |
+
return true
|
253 |
+
} catch (err) {
|
254 |
+
const error = `failed to delete the post: ${err}`
|
255 |
+
console.error(error)
|
256 |
+
throw new Error(error)
|
257 |
+
}
|
258 |
+
}
|
src/types.ts
CHANGED
@@ -98,6 +98,7 @@ export type Post = {
|
|
98 |
postId: string
|
99 |
appId: string
|
100 |
prompt: string
|
|
|
101 |
previewUrl: string
|
102 |
assetUrl: string
|
103 |
createdAt: string
|
@@ -301,4 +302,4 @@ export type VideoOptions = {
|
|
301 |
duration?: number // in milliseconds
|
302 |
|
303 |
steps?: number
|
304 |
-
}
|
|
|
98 |
postId: string
|
99 |
appId: string
|
100 |
prompt: string
|
101 |
+
model: string
|
102 |
previewUrl: string
|
103 |
assetUrl: string
|
104 |
createdAt: string
|
|
|
302 |
duration?: number // in milliseconds
|
303 |
|
304 |
steps?: number
|
305 |
+
}
|