jbilcke-hf HF staff commited on
Commit
18e5e63
β€’
1 Parent(s): f52fb0f

added community layer

Browse files
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'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>.
 
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&apos;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: "KappaNeuro/studio-ghibli-style",
51
- triggerWord: "Studio Ghibli Style",
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
+ }