Commit
•
58379d0
1
Parent(s):
1805613
add login
Browse files- .env +14 -1
- package-lock.json +51 -0
- package.json +3 -0
- src/app/config.ts +8 -1
- src/app/main.tsx +9 -1
- src/app/server/aitube/createClap.ts +25 -1
- src/app/server/{aitube/config.ts → config.ts} +1 -1
- src/app/server/redis/getRateLimit.ts +28 -0
- src/app/server/redis/redis.ts +6 -0
- src/app/store.ts +5 -1
- src/components/interface/auth-wall.tsx +24 -0
- src/components/interface/login/index.tsx +8 -0
- src/components/interface/login/login.tsx +20 -0
- src/components/interface/video-preview.tsx +33 -7
- src/lib/hooks/useProcessors.ts +42 -11
- src/lib/oauth/getOAuthRedirectUrl.ts +11 -0
- src/lib/oauth/getValidOAuth.ts +30 -0
- src/lib/oauth/useOAuth.ts +146 -0
- src/lib/oauth/usePersistedOAuth.ts +23 -0
- src/lib/oauth/useShouldDisplayLoginWall.ts +16 -0
- src/lib/utils/isRateLimitError.ts +4 -0
.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 "
|
|
|
|
|
|
|
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/
|
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 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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">{
|
|
|
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 |
-
:
|
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 |
-
/> :
|
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(`
|
73 |
|
74 |
-
console.log(`
|
75 |
|
76 |
-
console.log(`
|
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(`
|
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(`
|
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(`
|
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(`
|
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 |
-
|
429 |
-
|
430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|