File size: 4,560 Bytes
9a42933
 
ee5bd94
 
 
7448744
6c5a5b1
1cf03f7
 
827f345
9a42933
8f35fb6
 
7448744
a9e45ba
6c5a5b1
ee5bd94
 
 
 
 
6eef442
 
 
 
 
 
 
 
 
 
 
 
 
 
ee5bd94
 
6eef442
ee5bd94
 
 
 
 
9a42933
7448744
 
 
 
 
8bc9511
7448744
 
 
ee5bd94
7448744
 
9a42933
 
 
ee5bd94
 
 
 
6eef442
 
ee5bd94
 
6eef442
ee5bd94
 
 
 
 
 
6eef442
ee5bd94
 
 
 
 
 
827f345
 
7448744
 
8bc9511
7448744
6c5a5b1
 
 
 
7448744
 
6c5a5b1
 
 
 
 
 
b56210f
 
9a42933
 
6c5a5b1
1cf03f7
7448744
 
 
 
 
 
 
 
6c5a5b1
 
 
5c5e659
 
a9e45ba
5c5e659
 
 
 
 
 
 
7448744
 
5c5e659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a9e45ba
1cf03f7
7448744
 
 
 
 
 
 
 
5c5e659
 
 
 
9a42933
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
"use server"

import {Ratelimit} from "@upstash/ratelimit"
import {Redis} from "@upstash/redis"

import { VideoOptions } from "@/types"

import { generateGradio } from "./generateGradio"
import { generateReplicate } from "./generateReplicate"
import { filterOutBadWords } from "./censorship"

const videoEngine = `${process.env.VIDEO_ENGINE || ""}`

// const officialApi = `${process.env.VIDEO_HOTSHOT_XL_API_OFFICIAL || ""}`
const nodeApi = `${process.env.VIDEO_HOTSHOT_XL_API_NODE || ""}`

const redis = new Redis({
  url: `${process.env.UPSTASH_REDIS_REST_URL || ""}`,
  token: `${process.env.UPSTASH_REDIS_REST_TOKEN || ""}`,
})

// Create a global ratelimiter for all users, that allows 14 requests per 60 seconds
// 14 is roughly the number of requests that can be handled by the server
/*
const rateLimitGlobal = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(14, "60 s"),
  analytics: true,
  timeout: 1000,
  prefix: "production"
})
*/


// Create a new ratelimiter for anonymous users, that allows 2 requests per minute
const rateLimitAnons = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(2, "60 s"),
  analytics: true,
  timeout: 1000,
  prefix: "production:anon"
})

export async function generateAnimation({
  positivePrompt = "",
  negativePrompt = "",
  size = "512x512",
  huggingFaceLora,
  replicateLora,
  triggerWord,
  nbFrames = 8,
  duration = 1000,
  steps = 30,
  key = "",
}: VideoOptions): Promise<string> {
  if (!positivePrompt?.length) {
    throw new Error(`prompt is too short!`)
  }

  const cropped = positivePrompt.slice(0, 30)

  console.log(`user ${key.slice(0, 10)} requested "${cropped}${cropped !== positivePrompt ? "..." : ""}"`)

  // const globalRateLimitResult = rateLimitGlobal.limit("global")

  // this waits for 3 seconds before failing the request
  // we don't wait more because it is frustrating for someone to wait a failure
  const userRateLimitResult = await rateLimitAnons.limit(key || "anon")
  // const rateLimitResult = await rateLimitAnons.blockUntilReady(key, 3_000)

  // admin / developers will have this key:
  // eff8e7ca506627fe15dda5e0e512fcaad70b6d520f37cc76597fdb4f2d83a1a3

  // result.limit
  if (!userRateLimitResult.success) {
    console.log(`blocking user ${key.slice(0, 10)} who requested "${cropped}${cropped !== positivePrompt ? "..." : ""}"`)
    throw new Error(`Rate Limit Reached`)
  } else {
    console.log(`allowing user ${key.slice(0, 10)}: who requested "${cropped}${cropped !== positivePrompt ? "..." : ""}"`)
  }

  positivePrompt = filterOutBadWords(positivePrompt)

  // pimp the prompt
  positivePrompt = [
    triggerWord,
    positivePrompt,
    "beautiful",
    "hd"
  ].join(", ")

  negativePrompt = [
    negativePrompt,
    "cropped",
    "dark",
    "underexposed",
    "overexposed",
    "watermark",
    "watermarked",
  ].join(", ")

  try {

    if (videoEngine === "VIDEO_HOTSHOT_XL_API_REPLICATE") {
      return generateReplicate({
        positivePrompt,
        negativePrompt,
        size,
        huggingFaceLora,
        replicateLora,
        nbFrames,
        duration,
        steps,
      })
      
    } else if (videoEngine === "VIDEO_HOTSHOT_XL_API_NODE") {
      // TODO: support other API to avoid duplicate work?
      // (are the other API supporting custom LoRAs?)
      const res = await fetch(nodeApi, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          // access isn't secured for now, the free lunch is open
          // Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({
          prompt: positivePrompt,
          lora: huggingFaceLora,
          size,
        }),
        cache: "no-store",
        // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
        // next: { revalidate: 1 }
      })

      const content = await res.text()

      // Recommendation: handle errors
      if (res.status !== 200) {
        console.error(content)
        // This will activate the closest `error.js` Error Boundary
        throw new Error('Failed to fetch data')
      }

      return content
    } else if (videoEngine === "VIDEO_HOTSHOT_XL_API_GRADIO") {
      return generateGradio({
        positivePrompt,
        negativePrompt,
        size,
        huggingFaceLora,
        replicateLora,
        nbFrames,
        duration,
        steps,
      })
    } else {
      throw new Error(`not implemented yet!`)
    }

  } catch (err) {
    throw new Error(`failed to generate the image ${err}`)
  }
}