jbilcke-hf HF staff commited on
Commit
ee5bd94
1 Parent(s): cfbfdc3

adding rate limiter

Browse files
.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(Array.from(searchParams.entries()))
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(Array.from(searchParams.entries()))
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(Array.from(searchParams.entries()))
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(Array.from(searchParams.entries()))
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('Failed to fetch data')
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 = {