Commit
•
ee5bd94
1
Parent(s):
cfbfdc3
adding rate limiter
Browse files- .env +5 -0
- package-lock.json +43 -0
- package.json +2 -0
- src/app/firehose/page.tsx +2 -2
- src/app/interface/generate/index.tsx +58 -7
- src/app/main.tsx +2 -0
- src/app/server/actions/animation.ts +38 -0
- src/app/server/actions/generateWithGradioApi.ts +4 -2
- src/app/server/utils/isRateLimitError.ts +4 -0
- src/pages/api/get-key.ts +21 -0
- src/types.ts +2 -0
.env
CHANGED
@@ -28,6 +28,11 @@ VIDEO_HOTSHOT_XL_API_REPLICATE_MODEL_VERSION="75e26ffd033a59a78954a3d675632f47f7
|
|
28 |
INTERPOLATION_API_REPLICATE_MODEL="zsxkib/st-mfnet"
|
29 |
INTERPOLATION_API_REPLICATE_MODEL_VERSION="faa7693430b0a4ac95d1b8e25165673c1d7a7263537a7c4bb9be82a3e2d130fb"
|
30 |
|
|
|
|
|
|
|
|
|
|
|
31 |
# ----------- CENSORSHIP -------
|
32 |
ENABLE_CENSORSHIP=""
|
33 |
FINGERPRINT_KEY=""
|
|
|
28 |
INTERPOLATION_API_REPLICATE_MODEL="zsxkib/st-mfnet"
|
29 |
INTERPOLATION_API_REPLICATE_MODEL_VERSION="faa7693430b0a4ac95d1b8e25165673c1d7a7263537a7c4bb9be82a3e2d130fb"
|
30 |
|
31 |
+
# ----------- RATE LIMIT -------
|
32 |
+
ENABLE_RATE_LIMIT=""
|
33 |
+
UPSTASH_REDIS_REST_URL="<USE YOUR OWN>"
|
34 |
+
UPSTASH_REDIS_REST_TOKEN="<USE YOUR OWN>"
|
35 |
+
|
36 |
# ----------- CENSORSHIP -------
|
37 |
ENABLE_CENSORSHIP=""
|
38 |
FINGERPRINT_KEY=""
|
package-lock.json
CHANGED
@@ -36,6 +36,8 @@
|
|
36 |
"@types/react": "18.2.15",
|
37 |
"@types/react-dom": "18.2.7",
|
38 |
"@types/uuid": "^9.0.2",
|
|
|
|
|
39 |
"autoprefixer": "10.4.14",
|
40 |
"class-variance-authority": "^0.6.1",
|
41 |
"clsx": "^2.0.0",
|
@@ -4103,6 +4105,33 @@
|
|
4103 |
"url": "https://opencollective.com/typescript-eslint"
|
4104 |
}
|
4105 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4106 |
"node_modules/abs-svg-path": {
|
4107 |
"version": "0.1.1",
|
4108 |
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
|
@@ -6948,6 +6977,15 @@
|
|
6948 |
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
6949 |
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
6950 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6951 |
"node_modules/iterator.prototype": {
|
6952 |
"version": "1.1.2",
|
6953 |
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
|
@@ -9580,6 +9618,11 @@
|
|
9580 |
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
9581 |
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
9582 |
},
|
|
|
|
|
|
|
|
|
|
|
9583 |
"node_modules/whatwg-url": {
|
9584 |
"version": "5.0.0",
|
9585 |
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
|
|
36 |
"@types/react": "18.2.15",
|
37 |
"@types/react-dom": "18.2.7",
|
38 |
"@types/uuid": "^9.0.2",
|
39 |
+
"@upstash/ratelimit": "^0.4.4",
|
40 |
+
"@upstash/redis": "^1.23.4",
|
41 |
"autoprefixer": "10.4.14",
|
42 |
"class-variance-authority": "^0.6.1",
|
43 |
"clsx": "^2.0.0",
|
|
|
4105 |
"url": "https://opencollective.com/typescript-eslint"
|
4106 |
}
|
4107 |
},
|
4108 |
+
"node_modules/@upstash/core-analytics": {
|
4109 |
+
"version": "0.0.6",
|
4110 |
+
"resolved": "https://registry.npmjs.org/@upstash/core-analytics/-/core-analytics-0.0.6.tgz",
|
4111 |
+
"integrity": "sha512-cpPSR0XJAJs4Ddz9nq3tINlPS5aLfWVCqhhtHnXt4p7qr5+/Znlt1Es736poB/9rnl1hAHrOsOvVj46NEXcVqA==",
|
4112 |
+
"dependencies": {
|
4113 |
+
"@upstash/redis": "^1.19.3"
|
4114 |
+
},
|
4115 |
+
"engines": {
|
4116 |
+
"node": ">=16.0.0"
|
4117 |
+
}
|
4118 |
+
},
|
4119 |
+
"node_modules/@upstash/ratelimit": {
|
4120 |
+
"version": "0.4.4",
|
4121 |
+
"resolved": "https://registry.npmjs.org/@upstash/ratelimit/-/ratelimit-0.4.4.tgz",
|
4122 |
+
"integrity": "sha512-y3q6cNDdcRQ2MRPRf5UNWBN36IwnZ4kAEkGoH3i6OqdWwz4qlBxNsw4/Rpqn9h93+Nx1cqg5IOq7O2e2zMJY1w==",
|
4123 |
+
"dependencies": {
|
4124 |
+
"@upstash/core-analytics": "^0.0.6"
|
4125 |
+
}
|
4126 |
+
},
|
4127 |
+
"node_modules/@upstash/redis": {
|
4128 |
+
"version": "1.23.4",
|
4129 |
+
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.23.4.tgz",
|
4130 |
+
"integrity": "sha512-7KtG6RE5W7QbByDjQq7cEpwG2ir46VrEXZ8NFRn17FYSJUHKeHl6qnAqQJIR5rAItQWtyrKNYBij5IGEjUevhA==",
|
4131 |
+
"dependencies": {
|
4132 |
+
"isomorphic-fetch": "^3.0.0"
|
4133 |
+
}
|
4134 |
+
},
|
4135 |
"node_modules/abs-svg-path": {
|
4136 |
"version": "0.1.1",
|
4137 |
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
|
|
|
6977 |
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
6978 |
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
6979 |
},
|
6980 |
+
"node_modules/isomorphic-fetch": {
|
6981 |
+
"version": "3.0.0",
|
6982 |
+
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
|
6983 |
+
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
|
6984 |
+
"dependencies": {
|
6985 |
+
"node-fetch": "^2.6.1",
|
6986 |
+
"whatwg-fetch": "^3.4.1"
|
6987 |
+
}
|
6988 |
+
},
|
6989 |
"node_modules/iterator.prototype": {
|
6990 |
"version": "1.1.2",
|
6991 |
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
|
|
|
9618 |
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
9619 |
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
9620 |
},
|
9621 |
+
"node_modules/whatwg-fetch": {
|
9622 |
+
"version": "3.6.19",
|
9623 |
+
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz",
|
9624 |
+
"integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw=="
|
9625 |
+
},
|
9626 |
"node_modules/whatwg-url": {
|
9627 |
"version": "5.0.0",
|
9628 |
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
package.json
CHANGED
@@ -37,6 +37,8 @@
|
|
37 |
"@types/react": "18.2.15",
|
38 |
"@types/react-dom": "18.2.7",
|
39 |
"@types/uuid": "^9.0.2",
|
|
|
|
|
40 |
"autoprefixer": "10.4.14",
|
41 |
"class-variance-authority": "^0.6.1",
|
42 |
"clsx": "^2.0.0",
|
|
|
37 |
"@types/react": "18.2.15",
|
38 |
"@types/react-dom": "18.2.7",
|
39 |
"@types/uuid": "^9.0.2",
|
40 |
+
"@upstash/ratelimit": "^0.4.4",
|
41 |
+
"@upstash/redis": "^1.23.4",
|
42 |
"autoprefixer": "10.4.14",
|
43 |
"class-variance-authority": "^0.6.1",
|
44 |
"clsx": "^2.0.0",
|
src/app/firehose/page.tsx
CHANGED
@@ -19,8 +19,8 @@ export default function FirehosePage() {
|
|
19 |
const searchParams = useSearchParams()
|
20 |
const [_isPending, startTransition] = useTransition()
|
21 |
const [posts, setPosts] = useState<Post[]>([])
|
22 |
-
const moderationKey = (searchParams.get("moderationKey") as string) || ""
|
23 |
-
const limit = Number((searchParams.get("limit") as string) || defaultLimit)
|
24 |
const [toDelete, setToDelete] = useState<Post>()
|
25 |
|
26 |
useEffect(() => {
|
|
|
19 |
const searchParams = useSearchParams()
|
20 |
const [_isPending, startTransition] = useTransition()
|
21 |
const [posts, setPosts] = useState<Post[]>([])
|
22 |
+
const moderationKey = searchParams ? ((searchParams.get("moderationKey") as string) || "") : ""
|
23 |
+
const limit = searchParams ? (Number((searchParams.get("limit") as string) || defaultLimit)) : defaultLimit
|
24 |
const [toDelete, setToDelete] = useState<Post>()
|
25 |
|
26 |
useEffect(() => {
|
src/app/interface/generate/index.tsx
CHANGED
@@ -4,18 +4,21 @@ import { useEffect, useRef, useState, useTransition } from "react"
|
|
4 |
import { useSpring, animated } from "@react-spring/web"
|
5 |
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
6 |
|
|
|
7 |
import { cn } from "@/lib/utils"
|
8 |
import { headingFont } from "@/app/interface/fonts"
|
9 |
import { useCharacterLimit } from "@/lib/useCharacterLimit"
|
10 |
import { generateAnimation } from "@/app/server/actions/animation"
|
11 |
import { getLatestPosts, getPost, postToCommunity } from "@/app/server/actions/community"
|
12 |
-
import { useCountdown } from "@/lib/useCountdown"
|
13 |
-
import { Countdown } from "../countdown"
|
14 |
import { getSDXLModels } from "@/app/server/actions/models"
|
15 |
import { HotshotImageInferenceSize, Post, QualityLevel, QualityOption, SDXLModel } from "@/types"
|
16 |
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
17 |
import { TooltipProvider } from "@radix-ui/react-tooltip"
|
18 |
import { interpolate } from "@/app/server/actions/interpolate"
|
|
|
|
|
|
|
|
|
19 |
|
20 |
const qualityOptions = [
|
21 |
{
|
@@ -34,6 +37,7 @@ export function Generate() {
|
|
34 |
const router = useRouter()
|
35 |
const pathname = usePathname()
|
36 |
const searchParams = useSearchParams()
|
|
|
37 |
const [_isPending, startTransition] = useTransition()
|
38 |
|
39 |
const scrollRef = useRef<HTMLDivElement>(null)
|
@@ -56,6 +60,8 @@ export function Generate() {
|
|
56 |
const [stage, setStage] = useState<Stage>("generate")
|
57 |
|
58 |
const [qualityLevel, setQualityLevel] = useState<QualityLevel>("low")
|
|
|
|
|
59 |
|
60 |
const { progressPercent, remainingTimeInSec } = useCountdown({
|
61 |
isActive: isLocked,
|
@@ -103,7 +109,7 @@ export function Generate() {
|
|
103 |
const triggerWord = selectedModel ? selectedModel.trigger_word : "Studio Ghibli Style"
|
104 |
|
105 |
// now you got a read/write object
|
106 |
-
const current = new URLSearchParams(
|
107 |
current.set("prompt", promptDraft)
|
108 |
current.set("model", huggingFaceLora)
|
109 |
const search = current.toString()
|
@@ -114,6 +120,21 @@ export function Generate() {
|
|
114 |
// 608x416 @ 25 steps -> 32 seconds
|
115 |
const steps = qualityLevel === "low" ? 25 : 35
|
116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
const params = {
|
118 |
positivePrompt: promptDraft,
|
119 |
negativePrompt: "",
|
@@ -122,7 +143,8 @@ export function Generate() {
|
|
122 |
nbFrames: 10, // if duration is 1000ms then it means 8 FPS
|
123 |
duration: 1000, // in ms
|
124 |
steps,
|
125 |
-
size
|
|
|
126 |
}
|
127 |
|
128 |
let rawAssetUrl = ""
|
@@ -137,11 +159,40 @@ export function Generate() {
|
|
137 |
setAssetUrl(rawAssetUrl)
|
138 |
|
139 |
} catch (err) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
console.log("generation failed! probably just a Gradio failure, so let's just run the round robin again!")
|
141 |
|
142 |
try {
|
143 |
rawAssetUrl = await generateAnimation(params)
|
144 |
} catch (err) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
console.error(`generation failed again! ${err}`)
|
146 |
}
|
147 |
}
|
@@ -188,7 +239,7 @@ export function Generate() {
|
|
188 |
console.log("successfully submitted to the community!", post)
|
189 |
|
190 |
// now you got a read/write object
|
191 |
-
const current = new URLSearchParams(
|
192 |
current.set("postId", post.postId.trim())
|
193 |
current.set("prompt", post.prompt.trim())
|
194 |
current.set("model", post.model.trim())
|
@@ -212,7 +263,7 @@ export function Generate() {
|
|
212 |
}
|
213 |
|
214 |
// now we load URL params
|
215 |
-
const current = new URLSearchParams(
|
216 |
|
217 |
// URL query params
|
218 |
const existingPostId = current.get("postId") || ""
|
@@ -293,7 +344,7 @@ export function Generate() {
|
|
293 |
})
|
294 |
|
295 |
// now you got a read/write object
|
296 |
-
const current = new URLSearchParams(
|
297 |
current.set("postId", post.postId.trim())
|
298 |
current.set("prompt", post.prompt.trim())
|
299 |
current.set("model", post.model.trim())
|
|
|
4 |
import { useSpring, animated } from "@react-spring/web"
|
5 |
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
6 |
|
7 |
+
import { useToast } from "@/components/ui/use-toast"
|
8 |
import { cn } from "@/lib/utils"
|
9 |
import { headingFont } from "@/app/interface/fonts"
|
10 |
import { useCharacterLimit } from "@/lib/useCharacterLimit"
|
11 |
import { generateAnimation } from "@/app/server/actions/animation"
|
12 |
import { getLatestPosts, getPost, postToCommunity } from "@/app/server/actions/community"
|
|
|
|
|
13 |
import { getSDXLModels } from "@/app/server/actions/models"
|
14 |
import { HotshotImageInferenceSize, Post, QualityLevel, QualityOption, SDXLModel } from "@/types"
|
15 |
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
16 |
import { TooltipProvider } from "@radix-ui/react-tooltip"
|
17 |
import { interpolate } from "@/app/server/actions/interpolate"
|
18 |
+
import { isRateLimitError } from "@/app/server/utils/isRateLimitError"
|
19 |
+
import { useCountdown } from "@/lib/useCountdown"
|
20 |
+
|
21 |
+
import { Countdown } from "../countdown"
|
22 |
|
23 |
const qualityOptions = [
|
24 |
{
|
|
|
37 |
const router = useRouter()
|
38 |
const pathname = usePathname()
|
39 |
const searchParams = useSearchParams()
|
40 |
+
const searchParamsEntries = searchParams ? Array.from(searchParams.entries()) : []
|
41 |
const [_isPending, startTransition] = useTransition()
|
42 |
|
43 |
const scrollRef = useRef<HTMLDivElement>(null)
|
|
|
60 |
const [stage, setStage] = useState<Stage>("generate")
|
61 |
|
62 |
const [qualityLevel, setQualityLevel] = useState<QualityLevel>("low")
|
63 |
+
|
64 |
+
const { toast } = useToast()
|
65 |
|
66 |
const { progressPercent, remainingTimeInSec } = useCountdown({
|
67 |
isActive: isLocked,
|
|
|
109 |
const triggerWord = selectedModel ? selectedModel.trigger_word : "Studio Ghibli Style"
|
110 |
|
111 |
// now you got a read/write object
|
112 |
+
const current = new URLSearchParams(searchParamsEntries)
|
113 |
current.set("prompt", promptDraft)
|
114 |
current.set("model", huggingFaceLora)
|
115 |
const search = current.toString()
|
|
|
120 |
// 608x416 @ 25 steps -> 32 seconds
|
121 |
const steps = qualityLevel === "low" ? 25 : 35
|
122 |
|
123 |
+
let key = ""
|
124 |
+
try {
|
125 |
+
const res = await fetch("/api/get-key", {
|
126 |
+
method: "GET",
|
127 |
+
headers: {
|
128 |
+
Accept: "application/json",
|
129 |
+
"Content-Type": "application/json",
|
130 |
+
},
|
131 |
+
cache: 'no-store',
|
132 |
+
})
|
133 |
+
key = await res.text()
|
134 |
+
} catch (err) {
|
135 |
+
console.error("failed to get key, but this is not a blocker")
|
136 |
+
}
|
137 |
+
|
138 |
const params = {
|
139 |
positivePrompt: promptDraft,
|
140 |
negativePrompt: "",
|
|
|
143 |
nbFrames: 10, // if duration is 1000ms then it means 8 FPS
|
144 |
duration: 1000, // in ms
|
145 |
steps,
|
146 |
+
size,
|
147 |
+
key
|
148 |
}
|
149 |
|
150 |
let rawAssetUrl = ""
|
|
|
159 |
setAssetUrl(rawAssetUrl)
|
160 |
|
161 |
} catch (err) {
|
162 |
+
|
163 |
+
// check the rate limit
|
164 |
+
if (isRateLimitError(err)) {
|
165 |
+
console.error("error, too many requests")
|
166 |
+
toast({
|
167 |
+
title: "You can generate only one video per minute 👀",
|
168 |
+
description: "Please wait a bit before trying again 🤗",
|
169 |
+
})
|
170 |
+
setLocked(false)
|
171 |
+
return
|
172 |
+
} else {
|
173 |
+
toast({
|
174 |
+
title: "We couldn't generate your video 👀",
|
175 |
+
description: "We aere probably over capacity, but you can try again 🤗",
|
176 |
+
})
|
177 |
+
}
|
178 |
+
|
179 |
console.log("generation failed! probably just a Gradio failure, so let's just run the round robin again!")
|
180 |
|
181 |
try {
|
182 |
rawAssetUrl = await generateAnimation(params)
|
183 |
} catch (err) {
|
184 |
+
|
185 |
+
// check the rate limit
|
186 |
+
if (isRateLimitError(err)) {
|
187 |
+
console.error("error, too many requests")
|
188 |
+
toast({
|
189 |
+
title: "Error: the free server is over capacity 👀",
|
190 |
+
description: "You can generate one video per minute 🤗 Please try again in a moment!",
|
191 |
+
})
|
192 |
+
setLocked(false)
|
193 |
+
return
|
194 |
+
}
|
195 |
+
|
196 |
console.error(`generation failed again! ${err}`)
|
197 |
}
|
198 |
}
|
|
|
239 |
console.log("successfully submitted to the community!", post)
|
240 |
|
241 |
// now you got a read/write object
|
242 |
+
const current = new URLSearchParams(searchParamsEntries)
|
243 |
current.set("postId", post.postId.trim())
|
244 |
current.set("prompt", post.prompt.trim())
|
245 |
current.set("model", post.model.trim())
|
|
|
263 |
}
|
264 |
|
265 |
// now we load URL params
|
266 |
+
const current = new URLSearchParams(searchParamsEntries)
|
267 |
|
268 |
// URL query params
|
269 |
const existingPostId = current.get("postId") || ""
|
|
|
344 |
})
|
345 |
|
346 |
// now you got a read/write object
|
347 |
+
const current = new URLSearchParams(searchParamsEntries)
|
348 |
current.set("postId", post.postId.trim())
|
349 |
current.set("prompt", post.prompt.trim())
|
350 |
current.set("model", post.model.trim())
|
src/app/main.tsx
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
"use client"
|
2 |
|
3 |
import { cn } from "@/lib/utils"
|
|
|
4 |
import { paragraphFont } from "@/app/interface/fonts"
|
5 |
import { Background } from "./interface/background"
|
6 |
import { Generate } from "./interface/generate"
|
@@ -17,6 +18,7 @@ export function Main() {
|
|
17 |
<Background />
|
18 |
<Generate />
|
19 |
<BottomBar />
|
|
|
20 |
</div>
|
21 |
)
|
22 |
}
|
|
|
1 |
"use client"
|
2 |
|
3 |
import { cn } from "@/lib/utils"
|
4 |
+
import { Toaster } from "@/components/ui/toaster"
|
5 |
import { paragraphFont } from "@/app/interface/fonts"
|
6 |
import { Background } from "./interface/background"
|
7 |
import { Generate } from "./interface/generate"
|
|
|
18 |
<Background />
|
19 |
<Generate />
|
20 |
<BottomBar />
|
21 |
+
<Toaster />
|
22 |
</div>
|
23 |
)
|
24 |
}
|
src/app/server/actions/animation.ts
CHANGED
@@ -1,5 +1,8 @@
|
|
1 |
"use server"
|
2 |
|
|
|
|
|
|
|
3 |
import { VideoOptions } from "@/types"
|
4 |
|
5 |
import { generateVideoWithGradioAPI } from "./generateWithGradioApi"
|
@@ -11,6 +14,20 @@ const videoEngine = `${process.env.VIDEO_ENGINE || ""}`
|
|
11 |
// const officialApi = `${process.env.VIDEO_HOTSHOT_XL_API_OFFICIAL || ""}`
|
12 |
const nodeApi = `${process.env.VIDEO_HOTSHOT_XL_API_NODE || ""}`
|
13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
export async function generateAnimation({
|
15 |
positivePrompt = "",
|
16 |
negativePrompt = "",
|
@@ -21,11 +38,32 @@ export async function generateAnimation({
|
|
21 |
nbFrames = 8,
|
22 |
duration = 1000,
|
23 |
steps = 30,
|
|
|
24 |
}: VideoOptions): Promise<string> {
|
25 |
if (!positivePrompt?.length) {
|
26 |
throw new Error(`prompt is too short!`)
|
27 |
}
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
positivePrompt = filterOutBadWords(positivePrompt)
|
30 |
|
31 |
// pimp the prompt
|
|
|
1 |
"use server"
|
2 |
|
3 |
+
import {Ratelimit} from "@upstash/ratelimit"
|
4 |
+
import {Redis} from "@upstash/redis"
|
5 |
+
|
6 |
import { VideoOptions } from "@/types"
|
7 |
|
8 |
import { generateVideoWithGradioAPI } from "./generateWithGradioApi"
|
|
|
14 |
// const officialApi = `${process.env.VIDEO_HOTSHOT_XL_API_OFFICIAL || ""}`
|
15 |
const nodeApi = `${process.env.VIDEO_HOTSHOT_XL_API_NODE || ""}`
|
16 |
|
17 |
+
const redis = new Redis({
|
18 |
+
url: `${process.env.UPSTASH_REDIS_REST_URL || ""}`,
|
19 |
+
token: `${process.env.UPSTASH_REDIS_REST_TOKEN || ""}`,
|
20 |
+
})
|
21 |
+
|
22 |
+
// Create a new ratelimiter for anonymous users, that allows 1 requests per 60 seconds
|
23 |
+
const rateLimitAnons = new Ratelimit({
|
24 |
+
redis,
|
25 |
+
limiter: Ratelimit.slidingWindow(1, "60 s"),
|
26 |
+
analytics: true,
|
27 |
+
timeout: 1000,
|
28 |
+
prefix: "production:anon"
|
29 |
+
})
|
30 |
+
|
31 |
export async function generateAnimation({
|
32 |
positivePrompt = "",
|
33 |
negativePrompt = "",
|
|
|
38 |
nbFrames = 8,
|
39 |
duration = 1000,
|
40 |
steps = 30,
|
41 |
+
key = "",
|
42 |
}: VideoOptions): Promise<string> {
|
43 |
if (!positivePrompt?.length) {
|
44 |
throw new Error(`prompt is too short!`)
|
45 |
}
|
46 |
|
47 |
+
const cropped = positivePrompt.slice(0, 30)
|
48 |
+
|
49 |
+
console.log(`user ${key.slice(0, 10)} requested "${cropped}${cropped !== positivePrompt ? "..." : ""}"`)
|
50 |
+
|
51 |
+
// this waits for 3 seconds before failing the request
|
52 |
+
// we don't wait more because it is frustrating for someone to wait a failure
|
53 |
+
const rateLimitResult = await rateLimitAnons.limit(key || "anon")
|
54 |
+
// const rateLimitResult = await rateLimitAnons.blockUntilReady(key, 3_000)
|
55 |
+
|
56 |
+
// admin / developers will have this key:
|
57 |
+
// eff8e7ca506627fe15dda5e0e512fcaad70b6d520f37cc76597fdb4f2d83a1a3
|
58 |
+
|
59 |
+
// result.limit
|
60 |
+
if (!rateLimitResult.success) {
|
61 |
+
console.log(`blocking user ${key.slice(0, 10)} who requested "${cropped}${cropped !== positivePrompt ? "..." : ""}"`)
|
62 |
+
throw new Error(`Rate Limit Reached`)
|
63 |
+
} else {
|
64 |
+
console.log(`allowing user ${key.slice(0, 10)}: who requested "${cropped}${cropped !== positivePrompt ? "..." : ""}"`)
|
65 |
+
}
|
66 |
+
|
67 |
positivePrompt = filterOutBadWords(positivePrompt)
|
68 |
|
69 |
// pimp the prompt
|
src/app/server/actions/generateWithGradioApi.ts
CHANGED
@@ -14,6 +14,7 @@ export async function generateVideoWithGradioAPI({
|
|
14 |
duration = 1000,
|
15 |
steps = 30,
|
16 |
}: VideoOptions): Promise<string> {
|
|
|
17 |
console.log(`SEND TO ${gradioApi + (gradioApi.endsWith("/") ? "" : "/") + "api/predict"}:`, [
|
18 |
// accessToken,
|
19 |
positivePrompt,
|
@@ -25,6 +26,7 @@ export async function generateVideoWithGradioAPI({
|
|
25 |
nbFrames,
|
26 |
duration,
|
27 |
])
|
|
|
28 |
const res = await fetch(gradioApi + (gradioApi.endsWith("/") ? "" : "/") + "api/predict", {
|
29 |
method: "POST",
|
30 |
headers: {
|
@@ -54,9 +56,9 @@ export async function generateVideoWithGradioAPI({
|
|
54 |
|
55 |
// console.log("data:", data)
|
56 |
// Recommendation: handle errors
|
57 |
-
if (res.status !== 200) {
|
58 |
// This will activate the closest `error.js` Error Boundary
|
59 |
-
throw new Error(
|
60 |
}
|
61 |
// console.log("data:", data.slice(0, 50))
|
62 |
|
|
|
14 |
duration = 1000,
|
15 |
steps = 30,
|
16 |
}: VideoOptions): Promise<string> {
|
17 |
+
/*
|
18 |
console.log(`SEND TO ${gradioApi + (gradioApi.endsWith("/") ? "" : "/") + "api/predict"}:`, [
|
19 |
// accessToken,
|
20 |
positivePrompt,
|
|
|
26 |
nbFrames,
|
27 |
duration,
|
28 |
])
|
29 |
+
*/
|
30 |
const res = await fetch(gradioApi + (gradioApi.endsWith("/") ? "" : "/") + "api/predict", {
|
31 |
method: "POST",
|
32 |
headers: {
|
|
|
56 |
|
57 |
// console.log("data:", data)
|
58 |
// Recommendation: handle errors
|
59 |
+
if (res.status !== 200 || !Array.isArray(data)) {
|
60 |
// This will activate the closest `error.js` Error Boundary
|
61 |
+
throw new Error(`Failed to fetch data (status: ${res.status})`)
|
62 |
}
|
63 |
// console.log("data:", data.slice(0, 50))
|
64 |
|
src/app/server/utils/isRateLimitError.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function isRateLimitError(something: unknown) {
|
2 |
+
// yeah this is a very crude implementation
|
3 |
+
return `${something || ""}`.includes("Rate Limit Reached")
|
4 |
+
}
|
src/pages/api/get-key.ts
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import crypto from "node:crypto"
|
2 |
+
|
3 |
+
import { NextApiRequest, NextApiResponse } from "next"
|
4 |
+
|
5 |
+
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
6 |
+
let ipAddress = req.headers["x-real-ip"] as string
|
7 |
+
|
8 |
+
const forwardedFor = req.headers["x-forwarded-for"] as string
|
9 |
+
|
10 |
+
if (!ipAddress && forwardedFor) {
|
11 |
+
ipAddress = forwardedFor?.split(",").at(0) ?? "Unknown"
|
12 |
+
}
|
13 |
+
|
14 |
+
console.log("ipAddress:", ipAddress)
|
15 |
+
const hash = crypto.createHash('sha256')
|
16 |
+
hash.update(ipAddress)
|
17 |
+
const digest = hash.digest('hex')
|
18 |
+
res.status(200).json(digest)
|
19 |
+
}
|
20 |
+
|
21 |
+
export default handler
|
src/types.ts
CHANGED
@@ -302,6 +302,8 @@ export type VideoOptions = {
|
|
302 |
duration?: number // in milliseconds
|
303 |
|
304 |
steps?: number
|
|
|
|
|
305 |
}
|
306 |
|
307 |
export type SDXLModel = {
|
|
|
302 |
duration?: number // in milliseconds
|
303 |
|
304 |
steps?: number
|
305 |
+
|
306 |
+
key?: string // a semi-unique key to prevent abuse from some users
|
307 |
}
|
308 |
|
309 |
export type SDXLModel = {
|