jbilcke-hf HF staff commited on
Commit
58379d0
1 Parent(s): 1805613
.env CHANGED
@@ -7,4 +7,17 @@ MICROSERVICE_API_SECRET_TOKEN="<USE YOUR OWN>"
7
 
8
  AI_TUBE_API_SECRET_JWT_KEY=""
9
  AI_TUBE_API_SECRET_JWT_ISSUER=""
10
- AI_TUBE_API_SECRET_JWT_AUDIENCE=""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  AI_TUBE_API_SECRET_JWT_KEY=""
9
  AI_TUBE_API_SECRET_JWT_ISSUER=""
10
+ AI_TUBE_API_SECRET_JWT_AUDIENCE=""
11
+
12
+ # ------------- HUGGING FACE OAUTH -------------
13
+ NEXT_PUBLIC_ENABLE_HUGGING_FACE_OAUTH=""
14
+ NEXT_PUBLIC_ENABLE_HUGGING_FACE_OAUTH_WALL=""
15
+ NEXT_PUBLIC_HUGGING_FACE_OAUTH_CLIENT_ID=""
16
+
17
+ # this one must be kept secret (and is unused for now)
18
+ HUGGING_FACE_OAUTH_SECRET=""
19
+
20
+ # ----------- RATE LIMIT -------
21
+ ENABLE_RATE_LIMIT=""
22
+ UPSTASH_REDIS_REST_URL=""
23
+ UPSTASH_REDIS_REST_TOKEN=""
package-lock.json CHANGED
@@ -10,6 +10,7 @@
10
  "dependencies": {
11
  "@aitube/clap": "0.0.17",
12
  "@aitube/client": "0.0.25",
 
13
  "@radix-ui/react-accordion": "^1.1.2",
14
  "@radix-ui/react-avatar": "^1.0.4",
15
  "@radix-ui/react-checkbox": "^1.0.4",
@@ -32,6 +33,8 @@
32
  "@types/react": "18.3.0",
33
  "@types/react-dom": "18.3.0",
34
  "@types/uuid": "^9.0.8",
 
 
35
  "autoprefixer": "10.4.17",
36
  "class-variance-authority": "^0.7.0",
37
  "clsx": "^2.1.0",
@@ -226,6 +229,17 @@
226
  "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz",
227
  "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw=="
228
  },
 
 
 
 
 
 
 
 
 
 
 
229
  "node_modules/@humanwhocodes/config-array": {
230
  "version": "0.11.14",
231
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -2618,6 +2632,33 @@
2618
  "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
2619
  "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
2620
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2621
  "node_modules/acorn": {
2622
  "version": "8.11.3",
2623
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@@ -3547,6 +3588,11 @@
3547
  "node": ">= 8"
3548
  }
3549
  },
 
 
 
 
 
3550
  "node_modules/cssesc": {
3551
  "version": "3.0.0",
3552
  "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4822,6 +4868,11 @@
4822
  "url": "https://github.com/sponsors/ljharb"
4823
  }
4824
  },
 
 
 
 
 
4825
  "node_modules/hasown": {
4826
  "version": "2.0.2",
4827
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
 
10
  "dependencies": {
11
  "@aitube/clap": "0.0.17",
12
  "@aitube/client": "0.0.25",
13
+ "@huggingface/hub": "^0.15.0",
14
  "@radix-ui/react-accordion": "^1.1.2",
15
  "@radix-ui/react-avatar": "^1.0.4",
16
  "@radix-ui/react-checkbox": "^1.0.4",
 
33
  "@types/react": "18.3.0",
34
  "@types/react-dom": "18.3.0",
35
  "@types/uuid": "^9.0.8",
36
+ "@upstash/ratelimit": "^1.1.3",
37
+ "@upstash/redis": "^1.31.1",
38
  "autoprefixer": "10.4.17",
39
  "class-variance-authority": "^0.7.0",
40
  "clsx": "^2.1.0",
 
229
  "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz",
230
  "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw=="
231
  },
232
+ "node_modules/@huggingface/hub": {
233
+ "version": "0.15.0",
234
+ "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-0.15.0.tgz",
235
+ "integrity": "sha512-8jV+DjC68FXTNFCJeaKIa2e13rvfE4MBcJSlVtNOoA1cflLNmVBbta7iwKnMbUgdjW6DObztBLFneUcvZ3SHkQ==",
236
+ "dependencies": {
237
+ "hash-wasm": "^4.9.0"
238
+ },
239
+ "engines": {
240
+ "node": ">=18"
241
+ }
242
+ },
243
  "node_modules/@humanwhocodes/config-array": {
244
  "version": "0.11.14",
245
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
 
2632
  "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
2633
  "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
2634
  },
2635
+ "node_modules/@upstash/core-analytics": {
2636
+ "version": "0.0.8",
2637
+ "resolved": "https://registry.npmjs.org/@upstash/core-analytics/-/core-analytics-0.0.8.tgz",
2638
+ "integrity": "sha512-MCJoF+Y8fkzq4NRLG7kEHjtGyMsZ2DICBdmEdwoK9umoSrfkzgBlYdZiHTIaewyt9PGaMZCHOasz0LAuMpxwxQ==",
2639
+ "dependencies": {
2640
+ "@upstash/redis": "^1.28.3"
2641
+ },
2642
+ "engines": {
2643
+ "node": ">=16.0.0"
2644
+ }
2645
+ },
2646
+ "node_modules/@upstash/ratelimit": {
2647
+ "version": "1.1.3",
2648
+ "resolved": "https://registry.npmjs.org/@upstash/ratelimit/-/ratelimit-1.1.3.tgz",
2649
+ "integrity": "sha512-rl+GMvPdZJ9xPDIvIrqRl/g0nzAEaH75hwR5lXAKW8zPPplD/AeliDCHwuwcFCPIjg49FKyA1oc5H473WkVFrQ==",
2650
+ "dependencies": {
2651
+ "@upstash/core-analytics": "^0.0.8"
2652
+ }
2653
+ },
2654
+ "node_modules/@upstash/redis": {
2655
+ "version": "1.31.1",
2656
+ "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.31.1.tgz",
2657
+ "integrity": "sha512-lAsOo+kYjD5lpP+lH/nxHfzFYeCkWBwwKsyZZmh0AoOumBA9ZpS52Gorm7c2bmNu3UFijpPiLSFdW/nRdjbRpQ==",
2658
+ "dependencies": {
2659
+ "crypto-js": "^4.2.0"
2660
+ }
2661
+ },
2662
  "node_modules/acorn": {
2663
  "version": "8.11.3",
2664
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
 
3588
  "node": ">= 8"
3589
  }
3590
  },
3591
+ "node_modules/crypto-js": {
3592
+ "version": "4.2.0",
3593
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
3594
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
3595
+ },
3596
  "node_modules/cssesc": {
3597
  "version": "3.0.0",
3598
  "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
 
4868
  "url": "https://github.com/sponsors/ljharb"
4869
  }
4870
  },
4871
+ "node_modules/hash-wasm": {
4872
+ "version": "4.11.0",
4873
+ "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.11.0.tgz",
4874
+ "integrity": "sha512-HVusNXlVqHe0fzIzdQOGolnFN6mX/fqcrSAOcTBXdvzrXVHwTz11vXeKRmkR5gTuwVpvHZEIyKoePDvuAR+XwQ=="
4875
+ },
4876
  "node_modules/hasown": {
4877
  "version": "2.0.2",
4878
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
package.json CHANGED
@@ -11,6 +11,7 @@
11
  "dependencies": {
12
  "@aitube/clap": "0.0.17",
13
  "@aitube/client": "0.0.25",
 
14
  "@radix-ui/react-accordion": "^1.1.2",
15
  "@radix-ui/react-avatar": "^1.0.4",
16
  "@radix-ui/react-checkbox": "^1.0.4",
@@ -33,6 +34,8 @@
33
  "@types/react": "18.3.0",
34
  "@types/react-dom": "18.3.0",
35
  "@types/uuid": "^9.0.8",
 
 
36
  "autoprefixer": "10.4.17",
37
  "class-variance-authority": "^0.7.0",
38
  "clsx": "^2.1.0",
 
11
  "dependencies": {
12
  "@aitube/clap": "0.0.17",
13
  "@aitube/client": "0.0.25",
14
+ "@huggingface/hub": "^0.15.0",
15
  "@radix-ui/react-accordion": "^1.1.2",
16
  "@radix-ui/react-avatar": "^1.0.4",
17
  "@radix-ui/react-checkbox": "^1.0.4",
 
34
  "@types/react": "18.3.0",
35
  "@types/react-dom": "18.3.0",
36
  "@types/uuid": "^9.0.8",
37
+ "@upstash/ratelimit": "^1.1.3",
38
+ "@upstash/redis": "^1.31.1",
39
  "autoprefixer": "10.4.17",
40
  "class-variance-authority": "^0.7.0",
41
  "clsx": "^2.1.0",
src/app/config.ts CHANGED
@@ -5,4 +5,11 @@ export const defaultPrompt =
5
  // "beautiful footage of a Caribbean fishing village and bay, sail ships, during golden hour, no captions"
6
  "videogame gameplay footage, first person, exploring some mysterious ruins, no commentary"
7
 
8
- export const localStorageStoryDraftKey = "AI_STORIES_FACTORY_STORY_PROMPT_DRAFT"
 
 
 
 
 
 
 
 
5
  // "beautiful footage of a Caribbean fishing village and bay, sail ships, during golden hour, no captions"
6
  "videogame gameplay footage, first person, exploring some mysterious ruins, no commentary"
7
 
8
+ export const localStorageStoryDraftKey = "AI_STORIES_FACTORY_STORY_PROMPT_DRAFT"
9
+
10
+ export const localStorageHuggingFaceOAuthKey = "AI_STORIES_FACTORY_HUGGING_FACE_OAUTH"
11
+
12
+ export const oauthClientId = `${process.env.NEXT_PUBLIC_HUGGING_FACE_OAUTH_CLIENT_ID || ""}`
13
+ export const oauthScopes = "openid profile inference-api"
14
+ export const enableHuggingFaceOAuth = `${process.env.NEXT_PUBLIC_ENABLE_HUGGING_FACE_OAUTH || ""}` === "true"
15
+ export const enableHuggingFaceOAuthWall = `${process.env.NEXT_PUBLIC_ENABLE_HUGGING_FACE_OAUTH_WALL || ""}` === "true"
src/app/main.tsx CHANGED
@@ -20,6 +20,9 @@ import { LoadClapButton } from "@/components/interface/load-clap-button"
20
  import { SaveClapButton } from "@/components/interface/save-clap-button"
21
  import { useProcessors } from "@/lib/hooks/useProcessors"
22
  import { Characters } from "@/components/interface/characters"
 
 
 
23
 
24
  export function Main() {
25
  const { storyPromptDraft, setStoryPromptDraft, promptDraftRef } = useStoryPromptDraft()
@@ -27,6 +30,10 @@ export function Main() {
27
  const { orientation, toggleOrientation } = useOrientation()
28
  const { handleSubmit } = useProcessors()
29
  useQueryStringParams()
 
 
 
 
30
  return (
31
  <TooltipProvider>
32
  <div className={cn(
@@ -275,7 +282,7 @@ export function Main() {
275
  {/* END OF ORIENTATION SWITCH */}
276
  <Button
277
  onClick={handleSubmit}
278
- disabled={!storyPromptDraft || isBusy}
279
  // variant="ghost"
280
  className={cn(
281
  `text-base md:text-lg lg:text-xl xl:text-2xl`,
@@ -308,6 +315,7 @@ export function Main() {
308
  <BottomBar />
309
  </div>
310
  <Toaster />
 
311
  </div>
312
  </TooltipProvider>
313
  );
 
20
  import { SaveClapButton } from "@/components/interface/save-clap-button"
21
  import { useProcessors } from "@/lib/hooks/useProcessors"
22
  import { Characters } from "@/components/interface/characters"
23
+ import { useOAuth } from "@/lib/oauth/useOAuth"
24
+ import { useStore } from "./store"
25
+ import { AuthWall } from "@/components/interface/auth-wall"
26
 
27
  export function Main() {
28
  const { storyPromptDraft, setStoryPromptDraft, promptDraftRef } = useStoryPromptDraft()
 
30
  const { orientation, toggleOrientation } = useOrientation()
31
  const { handleSubmit } = useProcessors()
32
  useQueryStringParams()
33
+
34
+ const showAuthWall = useStore(s => s.showAuthWall)
35
+ const { isLoggedIn, enableOAuthWall } = useOAuth({ debug: true })
36
+
37
  return (
38
  <TooltipProvider>
39
  <div className={cn(
 
282
  {/* END OF ORIENTATION SWITCH */}
283
  <Button
284
  onClick={handleSubmit}
285
+ disabled={!storyPromptDraft || isBusy || !isLoggedIn}
286
  // variant="ghost"
287
  className={cn(
288
  `text-base md:text-lg lg:text-xl xl:text-2xl`,
 
315
  <BottomBar />
316
  </div>
317
  <Toaster />
318
+ <AuthWall show={showAuthWall} />
319
  </div>
320
  </TooltipProvider>
321
  );
src/app/server/aitube/createClap.ts CHANGED
@@ -1,10 +1,16 @@
1
  "use server"
2
 
 
 
 
3
  import { ClapProject, ClapMediaOrientation } from "@aitube/clap"
4
  import { createClap as apiCreateClap } from "@aitube/client"
5
 
6
  import { getToken } from "./getToken"
7
- import { RESOLUTION_LONG, RESOLUTION_SHORT } from "./config"
 
 
 
8
 
9
  export async function createClap({
10
  prompt = "",
@@ -15,6 +21,24 @@ export async function createClap({
15
  orientation?: ClapMediaOrientation
16
  turbo?: boolean
17
  }): Promise<ClapProject> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const clap: ClapProject = await apiCreateClap({
19
  prompt: prompt.slice(0, 512),
20
 
 
1
  "use server"
2
 
3
+
4
+ import { Ratelimit } from "@upstash/ratelimit"
5
+ import { Redis } from "@upstash/redis"
6
  import { ClapProject, ClapMediaOrientation } from "@aitube/clap"
7
  import { createClap as apiCreateClap } from "@aitube/client"
8
 
9
  import { getToken } from "./getToken"
10
+ import { RESOLUTION_LONG, RESOLUTION_SHORT } from "../config"
11
+ import { getRateLimit } from "../redis/getRateLimit"
12
+
13
+ const rateLimit = getRateLimit()
14
 
15
  export async function createClap({
16
  prompt = "",
 
21
  orientation?: ClapMediaOrientation
22
  turbo?: boolean
23
  }): Promise<ClapProject> {
24
+
25
+ // TODO: use
26
+
27
+ /*
28
+ if (process.env.ENABLE_RATE_LIMIT) {
29
+ const user = "anon"
30
+ const userRateLimitResult = await rateLimit.limit(user)
31
+
32
+ // result.limit
33
+ if (!userRateLimitResult.success) {
34
+ console.log(`blocking user ${user} who requested "${prompt}${prompt.length > 60 ? "..." : ""}"`)
35
+ throw new Error(`Rate Limit Reached`)
36
+ } else {
37
+ console.log(`allowing user ${user}: who requested "${prompt}${prompt.length >630 ? "..." : ""}"`)
38
+ }
39
+ }
40
+ */
41
+
42
  const clap: ClapProject = await apiCreateClap({
43
  prompt: prompt.slice(0, 512),
44
 
src/app/server/{aitube/config.ts → config.ts} RENAMED
@@ -8,4 +8,4 @@ export const serverHuggingfaceApiKey = `${process.env.HF_API_TOKEN || ""}`
8
  export const RESOLUTION_LONG = 896 // 832 // 768
9
  export const RESOLUTION_SHORT = 512 // 448 // 384
10
 
11
- // ValueError: `height` and `width` have to be divisible by 8 but are 512 and 1.
 
8
  export const RESOLUTION_LONG = 896 // 832 // 768
9
  export const RESOLUTION_SHORT = 512 // 448 // 384
10
 
11
+ // ValueError: `height` and `width` have to be divisible by 8 but are 512 and 1.
src/app/server/redis/getRateLimit.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Ratelimit } from "@upstash/ratelimit"
2
+
3
+ import { redis } from "./redis"
4
+
5
+ // Create a global ratelimiter for all users, that allows 14 requests per 60 seconds
6
+ // 14 is roughly the number of requests that can be handled by the server
7
+ /*
8
+ const rateLimitGlobal = new Ratelimit({
9
+ redis,
10
+ limiter: Ratelimit.slidingWindow(14, "60 s"),
11
+ analytics: true,
12
+ timeout: 1000,
13
+ prefix: "production"
14
+ })
15
+ */
16
+
17
+ // Create a new ratelimiter for anonymous users
18
+ export function getRateLimit() {
19
+ const rateLimit = new Ratelimit({
20
+ redis,
21
+ limiter: Ratelimit.slidingWindow(1, "1 m"), // 1 request every minute
22
+ analytics: true,
23
+ // timeout: 120000,
24
+ prefix: "production:anon"
25
+ })
26
+
27
+ return rateLimit
28
+ }
src/app/server/redis/redis.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { Redis } from "@upstash/redis"
2
+
3
+ export const redis = new Redis({
4
+ url: `${process.env.UPSTASH_REDIS_REST_URL || ""}`,
5
+ token: `${process.env.UPSTASH_REDIS_REST_TOKEN || ""}`,
6
+ })
src/app/store.ts CHANGED
@@ -6,7 +6,7 @@ import { create } from "zustand"
6
  import { GenerationStage, GlobalStatus, TaskStatus } from "@/types"
7
  import { getVideoOrientation } from "@/lib/utils/getVideoOrientation"
8
 
9
- import { RESOLUTION_LONG, RESOLUTION_SHORT } from "./server/aitube/config"
10
  import { putTextInTextAreaElement } from "@/lib/utils/putTextInTextAreaElement"
11
  import { defaultPrompt } from "./config"
12
 
@@ -41,6 +41,8 @@ export const useStore = create<{
41
  currentVideoOrientation: ClapMediaOrientation
42
  progress: number
43
  error: string
 
 
44
  toggleOrientation: () => void
45
  setOrientation: (orientation: ClapMediaOrientation) => void
46
  setCurrentVideoOrientation: (currentVideoOrientation: ClapMediaOrientation) => void
@@ -94,6 +96,8 @@ export const useStore = create<{
94
  currentVideoOrientation: ClapMediaOrientation.PORTRAIT,
95
  progress: 0,
96
  error: "",
 
 
97
  toggleOrientation: () => {
98
  const { orientation: previousOrientation, currentVideoOrientation, currentVideo } = get()
99
  const orientation =
 
6
  import { GenerationStage, GlobalStatus, TaskStatus } from "@/types"
7
  import { getVideoOrientation } from "@/lib/utils/getVideoOrientation"
8
 
9
+ import { RESOLUTION_LONG, RESOLUTION_SHORT } from "./server/config"
10
  import { putTextInTextAreaElement } from "@/lib/utils/putTextInTextAreaElement"
11
  import { defaultPrompt } from "./config"
12
 
 
41
  currentVideoOrientation: ClapMediaOrientation
42
  progress: number
43
  error: string
44
+ showAuthWall: boolean
45
+ setShowAuthWall: (showAuthWall: boolean) => void
46
  toggleOrientation: () => void
47
  setOrientation: (orientation: ClapMediaOrientation) => void
48
  setCurrentVideoOrientation: (currentVideoOrientation: ClapMediaOrientation) => void
 
96
  currentVideoOrientation: ClapMediaOrientation.PORTRAIT,
97
  progress: 0,
98
  error: "",
99
+ showAuthWall: false,
100
+ setShowAuthWall: (showAuthWall: boolean) => { set({ showAuthWall }) },
101
  toggleOrientation: () => {
102
  const { orientation: previousOrientation, currentVideoOrientation, currentVideo } = get()
103
  const orientation =
src/components/interface/auth-wall.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { Dialog, DialogContent } from "@/components/ui/dialog"
3
+ import { Login } from "./login"
4
+
5
+ export function AuthWall({ show }: { show: boolean }) {
6
+ return (
7
+ <Dialog open={show}>
8
+ <DialogContent className="sm:max-w-[800px]">
9
+ <div className="grid gap-4 py-4 text-stone-800 text-center text-xl">
10
+ <p className="">
11
+ The AI Stories Factory is an app to generate consistent video stories.
12
+ </p>
13
+ <p>
14
+ By default it uses Hugging Face for story and image generation,<br/>
15
+ our service is free of charge but we would like you to sign-in 👇
16
+ </p>
17
+ <p>
18
+ <Login />
19
+ </p>
20
+ </div>
21
+ </DialogContent>
22
+ </Dialog>
23
+ )
24
+ }
src/components/interface/login/index.tsx ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import dynamic from "next/dynamic";
4
+
5
+ export const Login = dynamic(() => import("./login"), {
6
+ // Make sure we turn SSR off
7
+ ssr: false,
8
+ });
src/components/interface/login/login.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { useOAuth } from "@/lib/oauth/useOAuth"
5
+
6
+ function Login() {
7
+ const { login } = useOAuth()
8
+ return <Button
9
+ variant="ghost"
10
+ onClick={login}
11
+ className="
12
+ text-xs
13
+ bg-lime-500 dark:bg-lime-500
14
+ text-stone-950/80 dark:text-stone-950/80
15
+ hover:bg-lime-400 dark:hover:bg-lime-400
16
+ hover:text-stone-950/100 dark:hover:text-stone-950/100
17
+ ">Login with Hugging Face</Button>
18
+ }
19
+
20
+ export default Login
src/components/interface/video-preview.tsx CHANGED
@@ -12,8 +12,13 @@ import { cn } from "@/lib/utils/cn"
12
 
13
  import { useStore } from "../../app/store"
14
  import HFLogo from "../../app/hf-logo.svg"
 
 
15
 
16
  export function VideoPreview() {
 
 
 
17
  const status = useStore(s => s.status)
18
  const parseGenerationStatus = useStore(s => s.parseGenerationStatus)
19
  const storyGenerationStatus = useStore(s => s.storyGenerationStatus)
@@ -38,6 +43,17 @@ export function VideoPreview() {
38
  isPortrait,
39
  } = useOrientation()
40
 
 
 
 
 
 
 
 
 
 
 
 
41
  return (
42
  <div className={cn(`
43
  -mt-8 md:mt-0
@@ -67,12 +83,25 @@ export function VideoPreview() {
67
  w-full h-full
68
  bg-black text-white
69
  ">
70
- {isBusy ? <div className="
 
 
 
 
 
 
 
 
 
 
 
 
71
  flex flex-col
72
  items-center justify-center
73
  text-center space-y-1.5">
74
  <p className="text-2xl font-bold">{progress}%</p>
75
- <p className="text-base text-white/70">{isBusy
 
76
  ? (
77
  // note: some of those tasks are running in parallel,
78
  // and some are super-slow (like music or video)
@@ -91,7 +120,7 @@ export function VideoPreview() {
91
  )
92
  : status === "error"
93
  ? <span>{error || ""}</span>
94
- : <span>{error ? error : <span>&nbsp;</span>}</span> // to prevent layout changes
95
  }</p>
96
  </div>
97
  : (currentVideo && currentVideo?.length > 128) ? <video
@@ -106,10 +135,7 @@ export function VideoPreview() {
106
  className="object-cover"
107
  style={{
108
  }}
109
- /> : <div className="
110
- flex flex-col
111
- items-center justify-center
112
- text-lg text-center"></div>}
113
  </div>
114
 
115
  <div className={cn(`
 
12
 
13
  import { useStore } from "../../app/store"
14
  import HFLogo from "../../app/hf-logo.svg"
15
+ import { Login } from "./login"
16
+ import { useOAuth } from "@/lib/oauth/useOAuth"
17
 
18
  export function VideoPreview() {
19
+
20
+ const { isLoggedIn, enableOAuthWall } = useOAuth()
21
+
22
  const status = useStore(s => s.status)
23
  const parseGenerationStatus = useStore(s => s.parseGenerationStatus)
24
  const storyGenerationStatus = useStore(s => s.storyGenerationStatus)
 
43
  isPortrait,
44
  } = useOrientation()
45
 
46
+ const placeholder = <div
47
+ className="
48
+ text-base
49
+ text-center
50
+ text-stone-50/90 dark:text-stone-50/90
51
+ "
52
+ >{
53
+ error ? <span>{error}</span> :
54
+ <span>No video yet</span>
55
+ }</div>
56
+
57
  return (
58
  <div className={cn(`
59
  -mt-8 md:mt-0
 
83
  w-full h-full
84
  bg-black text-white
85
  ">
86
+ {
87
+ !isLoggedIn ? <div className="
88
+ flex flex-col items-center justify-center
89
+ space-y-2
90
+ ">
91
+ <div className="
92
+ text-base
93
+ text-center
94
+ text-stone-50/90 dark:text-stone-50/90
95
+ ">Please login to generate videos:</div>
96
+ <Login />
97
+ </div>
98
+ : isBusy ? <div className="
99
  flex flex-col
100
  items-center justify-center
101
  text-center space-y-1.5">
102
  <p className="text-2xl font-bold">{progress}%</p>
103
+ <p className="text-base text-white/70">{
104
+ isBusy
105
  ? (
106
  // note: some of those tasks are running in parallel,
107
  // and some are super-slow (like music or video)
 
120
  )
121
  : status === "error"
122
  ? <span>{error || ""}</span>
123
+ : placeholder // to prevent layout changes
124
  }</p>
125
  </div>
126
  : (currentVideo && currentVideo?.length > 128) ? <video
 
135
  className="object-cover"
136
  style={{
137
  }}
138
+ /> : placeholder}
 
 
 
139
  </div>
140
 
141
  <div className={cn(`
src/lib/hooks/useProcessors.ts CHANGED
@@ -1,10 +1,12 @@
1
  "use client"
2
 
3
- import React, { useTransition } from "react"
4
  import { ClapProject, ClapSegmentCategory, getClapAssetSourceType, newEntity, updateClap } from "@aitube/clap"
5
 
6
  import { logImage } from "@/lib/utils"
7
  import { useIsBusy, useStoryPromptDraft } from "@/lib/hooks"
 
 
8
 
9
  import { createClap } from "@/app/server/aitube/createClap"
10
  import { editClapEntities } from "@/app/server/aitube/editClapEntities"
@@ -16,8 +18,11 @@ import { editClapVideos } from "@/app/server/aitube/editClapVideos"
16
  import { exportClapToVideo } from "@/app/server/aitube/exportClapToVideo"
17
 
18
  import { useStore } from "../../app/store"
 
19
 
20
  export function useProcessors() {
 
 
21
  const { storyPromptDraft, setStoryPromptDraft, promptDraftRef } = useStoryPromptDraft()
22
 
23
  const [_isPending, startTransition] = useTransition()
@@ -30,6 +35,7 @@ export function useProcessors() {
30
  const setMainCharacterImage = useStore(s => s.setMainCharacterImage)
31
  const setMainCharacterVoice = useStore(s => s.setMainCharacterVoice)
32
  const setStatus = useStore(s => s.setStatus)
 
33
 
34
  const error = useStore(s => s.error)
35
  const setError = useStore(s => s.setError)
@@ -46,8 +52,12 @@ export function useProcessors() {
46
  const setCurrentVideo = useStore(s => s.setCurrentVideo)
47
  const setProgress = useStore(s => s.setProgress)
48
 
 
 
49
  const { isBusy, busyRef } = useIsBusy()
50
 
 
 
51
  const generateStory = async (): Promise<ClapProject> => {
52
 
53
  let clap: ClapProject | undefined = undefined
@@ -69,11 +79,11 @@ export function useProcessors() {
69
 
70
  if (clap.segments.length <= 1) { throw new Error(`failed to generate more than one segments`) }
71
 
72
- console.log(`handleSubmit(): received a clap = `, clap)
73
 
74
- console.log(`handleSubmit(): copying over entities from the previous clap`)
75
 
76
- console.log(`handleSubmit(): later we can add button(s) to clear the project and/or the character(s)`)
77
  const { currentClap } = useStore.getState()
78
 
79
  clap.entities = Array.isArray(currentClap?.entities) ? currentClap.entities : []
@@ -153,7 +163,7 @@ export function useProcessors() {
153
 
154
  if (!clap) { throw new Error(`failed to edit the sound`) }
155
 
156
- console.log(`handleSubmit(): received a clap with sound = `, clap)
157
  setSoundGenerationStatus("finished")
158
  console.log("---------------- GENERATED SOUND ----------------")
159
  console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.SOUND), [
@@ -180,7 +190,7 @@ export function useProcessors() {
180
 
181
  if (!clap) { throw new Error(`failed to edit the music`) }
182
 
183
- console.log(`handleSubmit(): received a clap with music = `, clap)
184
  setMusicGenerationStatus("finished")
185
  console.log("---------------- GENERATED MUSIC ----------------")
186
  console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.MUSIC), [
@@ -210,7 +220,7 @@ export function useProcessors() {
210
  if (!clap) { throw new Error(`failed to edit the storyboards`) }
211
 
212
  // const fusion =
213
- console.log(`handleSubmit(): received storyboards = `, clap)
214
 
215
  setImageGenerationStatus("finished")
216
  console.log("---------------- GENERATED STORYBOARDS ----------------")
@@ -282,7 +292,7 @@ export function useProcessors() {
282
 
283
  if (!clap) { throw new Error(`failed to edit the dialogues`) }
284
 
285
- console.log(`handleSubmit(): received dialogues = `, clap)
286
  setVoiceGenerationStatus("finished")
287
  console.log("---------------- GENERATED DIALOGUES ----------------")
288
  console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.DIALOGUE), [
@@ -322,6 +332,14 @@ export function useProcessors() {
322
  }
323
 
324
  const handleSubmit = async () => {
 
 
 
 
 
 
 
 
325
  setStatus("generating")
326
  busyRef.current = true
327
 
@@ -425,9 +443,22 @@ export function useProcessors() {
425
  setStatus("finished")
426
  setError("")
427
  } catch (err) {
428
- console.error(`failed to generate: `, err)
429
- setStatus("error")
430
- setError(`Error, please contact an admin on Discord (${err})`)
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  }
432
  })
433
  }
 
1
  "use client"
2
 
3
+ import React, { useState, useTransition } from "react"
4
  import { ClapProject, ClapSegmentCategory, getClapAssetSourceType, newEntity, updateClap } from "@aitube/clap"
5
 
6
  import { logImage } from "@/lib/utils"
7
  import { useIsBusy, useStoryPromptDraft } from "@/lib/hooks"
8
+ import { isRateLimitError } from "@/lib/utils/isRateLimitError"
9
+ import { useToast } from "@/components/ui/use-toast"
10
 
11
  import { createClap } from "@/app/server/aitube/createClap"
12
  import { editClapEntities } from "@/app/server/aitube/editClapEntities"
 
18
  import { exportClapToVideo } from "@/app/server/aitube/exportClapToVideo"
19
 
20
  import { useStore } from "../../app/store"
21
+ import { useOAuth } from "../oauth/useOAuth"
22
 
23
  export function useProcessors() {
24
+ const [isLocked, setLocked] = useState(false)
25
+
26
  const { storyPromptDraft, setStoryPromptDraft, promptDraftRef } = useStoryPromptDraft()
27
 
28
  const [_isPending, startTransition] = useTransition()
 
35
  const setMainCharacterImage = useStore(s => s.setMainCharacterImage)
36
  const setMainCharacterVoice = useStore(s => s.setMainCharacterVoice)
37
  const setStatus = useStore(s => s.setStatus)
38
+ const setShowAuthWall = useStore(s => s.setShowAuthWall)
39
 
40
  const error = useStore(s => s.error)
41
  const setError = useStore(s => s.setError)
 
52
  const setCurrentVideo = useStore(s => s.setCurrentVideo)
53
  const setProgress = useStore(s => s.setProgress)
54
 
55
+ const { isLoggedIn, enableOAuthWall } = useOAuth()
56
+
57
  const { isBusy, busyRef } = useIsBusy()
58
 
59
+ const { toast } = useToast()
60
+
61
  const generateStory = async (): Promise<ClapProject> => {
62
 
63
  let clap: ClapProject | undefined = undefined
 
79
 
80
  if (clap.segments.length <= 1) { throw new Error(`failed to generate more than one segments`) }
81
 
82
+ console.log(`generateStory(): received a clap = `, clap)
83
 
84
+ console.log(`generateStory(): copying over entities from the previous clap`)
85
 
86
+ console.log(`generateStory(): later we can add button(s) to clear the project and/or the character(s)`)
87
  const { currentClap } = useStore.getState()
88
 
89
  clap.entities = Array.isArray(currentClap?.entities) ? currentClap.entities : []
 
163
 
164
  if (!clap) { throw new Error(`failed to edit the sound`) }
165
 
166
+ console.log(`generateSounds(): received a clap with sound = `, clap)
167
  setSoundGenerationStatus("finished")
168
  console.log("---------------- GENERATED SOUND ----------------")
169
  console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.SOUND), [
 
190
 
191
  if (!clap) { throw new Error(`failed to edit the music`) }
192
 
193
+ console.log(`generateMusic(): received a clap with music = `, clap)
194
  setMusicGenerationStatus("finished")
195
  console.log("---------------- GENERATED MUSIC ----------------")
196
  console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.MUSIC), [
 
220
  if (!clap) { throw new Error(`failed to edit the storyboards`) }
221
 
222
  // const fusion =
223
+ console.log(`generateStoryboards(): received storyboards = `, clap)
224
 
225
  setImageGenerationStatus("finished")
226
  console.log("---------------- GENERATED STORYBOARDS ----------------")
 
292
 
293
  if (!clap) { throw new Error(`failed to edit the dialogues`) }
294
 
295
+ console.log(`generateDialogues(): received dialogues = `, clap)
296
  setVoiceGenerationStatus("finished")
297
  console.log("---------------- GENERATED DIALOGUES ----------------")
298
  console.table(clap.segments.filter(s => s.category === ClapSegmentCategory.DIALOGUE), [
 
332
  }
333
 
334
  const handleSubmit = async () => {
335
+
336
+ if (busyRef.current) { return }
337
+
338
+ if (enableOAuthWall && !isLoggedIn) {
339
+ setShowAuthWall(true)
340
+ return
341
+ }
342
+
343
  setStatus("generating")
344
  busyRef.current = true
345
 
 
443
  setStatus("finished")
444
  setError("")
445
  } catch (err) {
446
+
447
+ if (isRateLimitError(err)) {
448
+ console.error("Critical error: you are doing too many requests!")
449
+ toast({
450
+ title: "You can generate only one video per minute 👀",
451
+ description: "Don't send too many requests at once 🤗",
452
+ })
453
+ return
454
+ } else {
455
+ console.error(err)
456
+ toast({
457
+ title: "We couldn't generate this video 👀",
458
+ description: "We are currently experiencing a surge in traffic, please try later in the day 🤗",
459
+ })
460
+ }
461
+
462
  }
463
  })
464
  }
src/lib/oauth/getOAuthRedirectUrl.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function getOAuthRedirectUrl(): string {
2
+ if (typeof window === "undefined") {
3
+ return "http://localhost:3000"
4
+ }
5
+
6
+ return (
7
+ window.location.hostname === "aistoriesfactory.app" ? "https://aistoriesfactory.app"
8
+ : window.location.hostname === "jbilcke-hf-ai-stories-factory.hf.space" ? "https://jbilcke-hf-ai-stories-factory.hf.space"
9
+ : "http://localhost:3000"
10
+ )
11
+ }
src/lib/oauth/getValidOAuth.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { OAuthResult } from "@huggingface/hub"
2
+
3
+ // return a valid OAuthResult, or else undefined
4
+ export function getValidOAuth(rawInput?: any): OAuthResult | undefined {
5
+ try {
6
+ let untypedOAuthResult: any
7
+ try {
8
+ untypedOAuthResult = JSON.parse(rawInput)
9
+ if (!untypedOAuthResult) { throw new Error("no valid serialized oauth result") }
10
+ } catch (err) {
11
+ untypedOAuthResult = rawInput
12
+ }
13
+
14
+ const maybeValidOAuth = untypedOAuthResult as OAuthResult
15
+
16
+ const accessTokenExpiresAt = new Date(maybeValidOAuth.accessTokenExpiresAt)
17
+
18
+ // Get the current date
19
+ const currentDate = new Date()
20
+
21
+ if (accessTokenExpiresAt.getTime() < currentDate.getTime()) {
22
+ throw new Error("the serialized oauth result has expired")
23
+ }
24
+
25
+ return maybeValidOAuth
26
+ } catch (err) {
27
+ // console.error(err)
28
+ return undefined
29
+ }
30
+ }
src/lib/oauth/useOAuth.ts ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect } from "react"
4
+ import { useSearchParams } from "next/navigation"
5
+ import { OAuthResult, oauthHandleRedirectIfPresent, oauthLoginUrl } from "@huggingface/hub"
6
+
7
+ import { enableHuggingFaceOAuth, oauthClientId, oauthScopes } from "@/app/config"
8
+
9
+ import { usePersistedOAuth } from "./usePersistedOAuth"
10
+ import { getValidOAuth } from "./getValidOAuth"
11
+ import { useShouldDisplayLoginWall } from "./useShouldDisplayLoginWall"
12
+ import { getOAuthRedirectUrl } from "./getOAuthRedirectUrl"
13
+
14
+ export function useOAuth({
15
+ debug = false
16
+ }: {
17
+ debug?: boolean
18
+ } = {
19
+ debug: false
20
+ }): {
21
+ clientId: string
22
+ redirectUrl: string
23
+ scopes: string
24
+ canLogin: boolean
25
+ login: () => Promise<void>
26
+ isLoggedIn: boolean
27
+ enableOAuth: boolean
28
+ enableOAuthWall: boolean
29
+ oauthResult?: OAuthResult
30
+ } {
31
+ const [oauthResult, setOAuthResult] = usePersistedOAuth()
32
+
33
+ const clientId = oauthClientId
34
+
35
+ // const redirectUrl = config.oauthRedirectUrl
36
+ const redirectUrl = getOAuthRedirectUrl()
37
+
38
+ const scopes = oauthScopes
39
+ const enableOAuth = enableHuggingFaceOAuth
40
+
41
+ const searchParams = useSearchParams()
42
+ const code = searchParams?.get("code") || ""
43
+ const state = searchParams?.get("state") || ""
44
+
45
+ const hasReceivedFreshOAuth = Boolean(code && state)
46
+
47
+ // note: being able to log into hugging face using the popup
48
+ // is different from seeing the "login wall"
49
+ const canLogin: boolean = Boolean(oauthClientId && enableOAuth)
50
+ const isLoggedIn = Boolean(oauthResult)
51
+
52
+ const enableOAuthWall = useShouldDisplayLoginWall()
53
+
54
+ if (debug) {
55
+ console.log("useOAuth debug:", {
56
+ oauthResult,
57
+ clientId,
58
+ redirectUrl,
59
+ scopes,
60
+ enableOAuth,
61
+ enableOAuthWall,
62
+ code,
63
+ state,
64
+ hasReceivedFreshOAuth,
65
+ canLogin,
66
+ isLoggedIn,
67
+ })
68
+
69
+ /*
70
+ useOAuth debug: {
71
+ oauthResult: '',
72
+ clientId: '........',
73
+ redirectUrl: 'http://localhost:3000',
74
+ scopes: 'openid profile inference-api',
75
+ isOAuthEnabled: true,
76
+ code: '...........',
77
+ state: '{"nonce":".........","redirectUri":"http://localhost:3000"}',
78
+ hasReceivedFreshOAuth: true,
79
+ canLogin: false,
80
+ isLoggedIn: false
81
+ }
82
+ */
83
+ }
84
+
85
+ useEffect(() => {
86
+ // no need to perfor the rest if the operation is there is nothing in the url
87
+ if (hasReceivedFreshOAuth) {
88
+
89
+ (async () => {
90
+ const maybeValidOAuth = await oauthHandleRedirectIfPresent()
91
+
92
+ const newOAuth = getValidOAuth(maybeValidOAuth)
93
+
94
+ if (!newOAuth) {
95
+ if (debug) {
96
+ console.log("useOAuth::useEffect 1: got something in the url but no valid oauth data to show.. something went terribly wrong")
97
+ }
98
+ } else {
99
+ if (debug) {
100
+ console.log("useOAuth::useEffect 1: correctly received the new oauth result, saving it to local storage:", newOAuth)
101
+ }
102
+ setOAuthResult(newOAuth)
103
+
104
+ // once set we can (brutally) reload the page
105
+ window.location.href = `//${window.location.host}${window.location.pathname}`
106
+ }
107
+ })()
108
+ }
109
+ }, [debug, hasReceivedFreshOAuth])
110
+
111
+ // for debugging purpose
112
+ useEffect(() => {
113
+ if (!debug) {
114
+ return
115
+ }
116
+ // console.log(`useOAuth::useEffect 2: canLogin? ${canLogin}`)
117
+ if (!canLogin) {
118
+ return
119
+ }
120
+ // console.log(`useOAuth::useEffect2: isLoggedIn? ${isLoggedIn}`)
121
+ if (!isLoggedIn) {
122
+ return
123
+ }
124
+ // console.log(`useOAuth::useEffect 2: oauthResult:`, oauthResult)
125
+ }, [debug, canLogin, isLoggedIn, oauthResult])
126
+
127
+ const login = async () => {
128
+ window.location.href = await oauthLoginUrl({
129
+ clientId,
130
+ redirectUrl,
131
+ scopes,
132
+ })
133
+ }
134
+
135
+ return {
136
+ clientId,
137
+ redirectUrl,
138
+ scopes,
139
+ canLogin,
140
+ login,
141
+ isLoggedIn,
142
+ enableOAuth,
143
+ enableOAuthWall,
144
+ oauthResult
145
+ }
146
+ }
src/lib/oauth/usePersistedOAuth.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useLocalStorage } from "usehooks-ts"
4
+ import { OAuthResult } from "@huggingface/hub"
5
+
6
+ import { localStorageHuggingFaceOAuthKey } from "@/app/config"
7
+
8
+ import { getValidOAuth } from "./getValidOAuth"
9
+
10
+ export function usePersistedOAuth(): [OAuthResult | undefined, (oauthResult: OAuthResult) => void] {
11
+ const [serializedHuggingFaceOAuth, setSerializedHuggingFaceOAuth] = useLocalStorage<string>(
12
+ localStorageHuggingFaceOAuthKey,
13
+ ""
14
+ )
15
+
16
+ const oauthResult = getValidOAuth(serializedHuggingFaceOAuth)
17
+
18
+ const setOAuthResult = (oauthResult: OAuthResult) => {
19
+ setSerializedHuggingFaceOAuth(JSON.stringify(oauthResult))
20
+ }
21
+
22
+ return [oauthResult, setOAuthResult]
23
+ }
src/lib/oauth/useShouldDisplayLoginWall.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ // we don't want to display the login wall to people forking the project,
3
+
4
+ import { enableHuggingFaceOAuth, enableHuggingFaceOAuthWall, oauthClientId } from "@/app/config"
5
+
6
+ // or to people who selected no hugging face server at all
7
+ export function useShouldDisplayLoginWall() {
8
+
9
+ const shouldDisplayLoginWall = Boolean(
10
+ oauthClientId &&
11
+ enableHuggingFaceOAuth &&
12
+ enableHuggingFaceOAuthWall
13
+ )
14
+
15
+ return shouldDisplayLoginWall
16
+ }
src/lib/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
+ }