enzostvs HF staff commited on
Commit
eb29a95
β€’
1 Parent(s): 3067dbb

refacto models + community

Browse files
Files changed (36) hide show
  1. package-lock.json +17 -0
  2. package.json +1 -0
  3. prisma/dev.db +0 -0
  4. prisma/migrations/20240104191224_init/migration.sql +0 -14
  5. prisma/migrations/20240111171434_/migration.sql +31 -0
  6. prisma/schema.prisma +24 -6
  7. src/lib/assets/banner.webp +0 -0
  8. src/lib/assets/sparkles.svg +4 -0
  9. src/lib/components/Button.svelte +14 -7
  10. src/lib/components/Loading.svelte +2 -2
  11. src/lib/components/community/Card.svelte +8 -8
  12. src/lib/components/community/reactions/Reactions.svelte +22 -0
  13. src/lib/components/fields/Textarea.svelte +18 -0
  14. src/lib/components/generate/Banner.svelte +31 -0
  15. src/lib/components/generate/Response.svelte +93 -0
  16. src/lib/components/models/Card.svelte +3 -3
  17. src/lib/components/models/Submit.svelte +7 -7
  18. src/lib/components/models/autocomplete/Autocomplete.svelte +14 -4
  19. src/lib/components/models/autocomplete/Item.svelte +1 -1
  20. src/lib/type.ts +5 -3
  21. src/lib/utils/index.ts +6 -6
  22. src/lib/utils/uploader.ts +32 -0
  23. src/routes/+layout.server.ts +1 -1
  24. src/routes/+layout.svelte +1 -1
  25. src/routes/+page.svelte +74 -36
  26. src/routes/+page.ts +1 -1
  27. src/routes/api/bulk-create-models/+server.ts +7 -10
  28. src/routes/api/community/+server.ts +36 -11
  29. src/routes/api/generate/+server.ts +55 -0
  30. src/routes/api/generate/share/+server.ts +68 -0
  31. src/routes/api/models/+server.ts +3 -12
  32. src/routes/api/models/{[repo] β†’ [id]}/+server.ts +2 -2
  33. src/routes/api/models/submit/+server.ts +3 -3
  34. src/routes/{models β†’ gallery}/+page.svelte +37 -54
  35. src/routes/{models β†’ gallery}/+page.ts +1 -1
  36. src/routes/generate/+page.svelte +74 -10
package-lock.json CHANGED
@@ -8,6 +8,7 @@
8
  "name": "loras-explorer",
9
  "version": "0.0.1",
10
  "dependencies": {
 
11
  "@iconify/svelte": "^3.1.4",
12
  "@prisma/client": "^5.7.1",
13
  "@svelte-put/clickoutside": "^3.0.1",
@@ -486,6 +487,17 @@
486
  "integrity": "sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==",
487
  "dev": true
488
  },
 
 
 
 
 
 
 
 
 
 
 
489
  "node_modules/@humanwhocodes/config-array": {
490
  "version": "0.11.13",
491
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
@@ -2719,6 +2731,11 @@
2719
  "node": ">=8"
2720
  }
2721
  },
 
 
 
 
 
2722
  "node_modules/hasown": {
2723
  "version": "2.0.0",
2724
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
 
8
  "name": "loras-explorer",
9
  "version": "0.0.1",
10
  "dependencies": {
11
+ "@huggingface/hub": "^0.12.3-oauth",
12
  "@iconify/svelte": "^3.1.4",
13
  "@prisma/client": "^5.7.1",
14
  "@svelte-put/clickoutside": "^3.0.1",
 
487
  "integrity": "sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==",
488
  "dev": true
489
  },
490
+ "node_modules/@huggingface/hub": {
491
+ "version": "0.12.3-oauth",
492
+ "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-0.12.3-oauth.tgz",
493
+ "integrity": "sha512-9LE4Ded2VsjQTK/rxsIF/nvhkxs1UE+lcpNpuBxnTGNpaVzbO6HIlAtbThgNenKPplRt1iDkn82AQYBco1QHug==",
494
+ "dependencies": {
495
+ "hash-wasm": "^4.9.0"
496
+ },
497
+ "engines": {
498
+ "node": ">=18"
499
+ }
500
+ },
501
  "node_modules/@humanwhocodes/config-array": {
502
  "version": "0.11.13",
503
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
 
2731
  "node": ">=8"
2732
  }
2733
  },
2734
+ "node_modules/hash-wasm": {
2735
+ "version": "4.11.0",
2736
+ "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.11.0.tgz",
2737
+ "integrity": "sha512-HVusNXlVqHe0fzIzdQOGolnFN6mX/fqcrSAOcTBXdvzrXVHwTz11vXeKRmkR5gTuwVpvHZEIyKoePDvuAR+XwQ=="
2738
+ },
2739
  "node_modules/hasown": {
2740
  "version": "2.0.0",
2741
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
package.json CHANGED
@@ -38,6 +38,7 @@
38
  },
39
  "type": "module",
40
  "dependencies": {
 
41
  "@iconify/svelte": "^3.1.4",
42
  "@prisma/client": "^5.7.1",
43
  "@svelte-put/clickoutside": "^3.0.1",
 
38
  },
39
  "type": "module",
40
  "dependencies": {
41
+ "@huggingface/hub": "^0.12.3-oauth",
42
  "@iconify/svelte": "^3.1.4",
43
  "@prisma/client": "^5.7.1",
44
  "@svelte-put/clickoutside": "^3.0.1",
prisma/dev.db CHANGED
Binary files a/prisma/dev.db and b/prisma/dev.db differ
 
prisma/migrations/20240104191224_init/migration.sql DELETED
@@ -1,14 +0,0 @@
1
- -- CreateTable
2
- CREATE TABLE "Model" (
3
- "id" TEXT NOT NULL PRIMARY KEY,
4
- "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
5
- "repo" TEXT NOT NULL,
6
- "title" TEXT NOT NULL,
7
- "image" TEXT,
8
- "likes" INTEGER,
9
- "downloads" INTEGER,
10
- "isPublic" BOOLEAN NOT NULL DEFAULT false
11
- );
12
-
13
- -- CreateIndex
14
- CREATE UNIQUE INDEX "Model_repo_key" ON "Model"("repo");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prisma/migrations/20240111171434_/migration.sql ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- CreateTable
2
+ CREATE TABLE "Model" (
3
+ "id" TEXT NOT NULL PRIMARY KEY,
4
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
5
+ "title" TEXT NOT NULL,
6
+ "image" TEXT NOT NULL,
7
+ "likes" INTEGER,
8
+ "downloads" INTEGER,
9
+ "isPublic" BOOLEAN NOT NULL DEFAULT false,
10
+ "hf_user_id" TEXT
11
+ );
12
+
13
+ -- CreateTable
14
+ CREATE TABLE "Gallery" (
15
+ "id" TEXT NOT NULL PRIMARY KEY,
16
+ "hf_user_id" TEXT,
17
+ "prompt" TEXT NOT NULL,
18
+ "image" TEXT NOT NULL,
19
+ "modelId" TEXT NOT NULL,
20
+ CONSTRAINT "Gallery_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "Model" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
21
+ );
22
+
23
+ -- CreateTable
24
+ CREATE TABLE "Reaction" (
25
+ "id" TEXT NOT NULL PRIMARY KEY,
26
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
27
+ "emoji" TEXT NOT NULL,
28
+ "hf_user_id" TEXT,
29
+ "galleryId" TEXT,
30
+ CONSTRAINT "Reaction_galleryId_fkey" FOREIGN KEY ("galleryId") REFERENCES "Gallery" ("id") ON DELETE SET NULL ON UPDATE CASCADE
31
+ );
prisma/schema.prisma CHANGED
@@ -11,15 +11,33 @@ datasource db {
11
  }
12
 
13
  model Model {
14
- id String @id @default(uuid())
15
- createdAt DateTime @default(now())
16
- repo String @unique
17
  title String
18
- // trigger_word String?
19
  image String
20
- // weights String?
21
  likes Int?
22
  downloads Int?
23
- isPublic Boolean @default(false)
 
 
 
 
 
 
24
  hf_user_id String?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
 
11
  }
12
 
13
  model Model {
14
+ id String @id
15
+ createdAt DateTime @default(now())
 
16
  title String
 
17
  image String
 
18
  likes Int?
19
  downloads Int?
20
+ isPublic Boolean @default(false)
21
+ hf_user_id String?
22
+ Gallery Gallery[]
23
+ }
24
+
25
+ model Gallery {
26
+ id String @id @default(uuid())
27
  hf_user_id String?
28
+ createdAt DateTime @default(now())
29
+ prompt String
30
+ image String
31
+ reactions Reaction[]
32
+ model Model @relation(fields: [modelId], references: [id])
33
+ modelId String
34
+ }
35
+
36
+ model Reaction {
37
+ id String @id @default(uuid())
38
+ createdAt DateTime @default(now())
39
+ emoji String
40
+ hf_user_id String
41
+ Gallery Gallery? @relation(fields: [galleryId], references: [id])
42
+ galleryId String?
43
  }
src/lib/assets/banner.webp ADDED
src/lib/assets/sparkles.svg ADDED
src/lib/components/Button.svelte CHANGED
@@ -1,6 +1,7 @@
1
  <script lang="ts">
2
  import { goto } from '$app/navigation';
3
  import Icon from "@iconify/svelte";
 
4
 
5
  export let theme: "light" | "dark" | "blue" | "pink" = "light";
6
  export let size: "md" | "lg" = "md";
@@ -24,16 +25,22 @@ import { goto } from '$app/navigation';
24
 
25
  </script>
26
 
27
- <button on:click={handleClick} class="button {theme} {size}">
28
  {#if icon && iconPosition === "left"}
29
- <Icon icon={icon} class="w-[20px] h-[20px]" />
 
 
30
  {/if}
31
- <!-- {#if loading}
32
- <Icon icon="akar-icons:circle" class="w-5 h-5 animate-spin" />
33
- {/if} -->
34
- <slot />
 
 
35
  {#if icon && iconPosition === "right"}
36
- <Icon icon={icon} class="w-[20px] h-[20px]" />
 
 
37
  {/if}
38
  </button>
39
 
 
1
  <script lang="ts">
2
  import { goto } from '$app/navigation';
3
  import Icon from "@iconify/svelte";
4
+ import Loading from './Loading.svelte';
5
 
6
  export let theme: "light" | "dark" | "blue" | "pink" = "light";
7
  export let size: "md" | "lg" = "md";
 
25
 
26
  </script>
27
 
28
+ <button on:click={handleClick} class="button {theme} {size} relative whitespace-nowrap" class:!bg-neutral-400={loading} class:!border-neutral-400={loading}>
29
  {#if icon && iconPosition === "left"}
30
+ <p class:opacity-0={loading}>
31
+ <Icon icon={icon} class="w-[20px] h-[20px]" />
32
+ </p>
33
  {/if}
34
+ {#if loading}
35
+ <Loading />
36
+ {/if}
37
+ <p class:opacity-0={loading}>
38
+ <slot />
39
+ </p>
40
  {#if icon && iconPosition === "right"}
41
+ <p class:opacity-0={loading}>
42
+ <Icon icon={icon} class="w-[20px] h-[20px]" />
43
+ </p>
44
  {/if}
45
  </button>
46
 
src/lib/components/Loading.svelte CHANGED
@@ -1,6 +1,6 @@
1
- <div class="absolute left-0 top-0 w-full h-full flex items-center justify-center flex-col">
2
  <svg
3
- class="animate-spin -ml-1 mr-3 h-8 w-8 text-white"
4
  xmlns="http://www.w3.org/2000/svg"
5
  fill="none"
6
  viewBox="0 0 24 24"
 
1
+ <div class="absolute left-0 top-0 w-full h-full flex items-center justify-center flex-col gap-3">
2
  <svg
3
+ class="animate-spin h-8 w-8 text-white"
4
  xmlns="http://www.w3.org/2000/svg"
5
  fill="none"
6
  viewBox="0 0 24 24"
src/lib/components/community/Card.svelte CHANGED
@@ -1,25 +1,25 @@
1
  <script lang="ts">
2
- import Reaction from "$lib/components/community/reactions/Reaction.svelte";
3
  import Add from "$lib/components/community/reactions/Add.svelte";
4
  import type { CommunityCard } from "$lib/type";
 
5
 
6
  export let card: CommunityCard;
7
- </script>
8
 
9
  <div
10
  class="cursor-pointer group bg-neutral-700 rounded-xl h-[310px] relative flex items-start justify-between flex-col p-5 transition-all duration-200 brightness-75 hover:brightness-100 z-[1]"
11
  >
12
  <div class="w-full h-full absolute top-0 left-0 -z-[1] rounded-xl overflow-hidden">
13
- <div class="w-full h-full bg-center bg-cover transition-all duration-200 group-hover:scale-110 " style="background-image: url('{card.image}');"></div>
14
  </div>
15
- <div class="bg-black/40 backdrop-blur-sm border border-white/30 rounded-lg px-6 py-3 text-white transition-all duration-200 opacity-0 group-hover:opacity-100">
16
  <p class="text-white font-semibold text-base">{card.prompt}</p>
17
- <p class="text-white/75 font-regular text-sm">{card.model_name}</p>
18
  </div>
19
  <div class="flex items-center justify-start gap-2">
20
- {#each card.reactions as reaction}
21
- <Reaction emoji={reaction.emoji} count={reaction?.users?.length} />
22
- {/each}
23
  <Add count={card?.reactions?.length} />
24
  </div>
25
  </div>
 
1
  <script lang="ts">
 
2
  import Add from "$lib/components/community/reactions/Add.svelte";
3
  import type { CommunityCard } from "$lib/type";
4
+ import Reactions from "./reactions/Reactions.svelte";
5
 
6
  export let card: CommunityCard;
7
+ </script>
8
 
9
  <div
10
  class="cursor-pointer group bg-neutral-700 rounded-xl h-[310px] relative flex items-start justify-between flex-col p-5 transition-all duration-200 brightness-75 hover:brightness-100 z-[1]"
11
  >
12
  <div class="w-full h-full absolute top-0 left-0 -z-[1] rounded-xl overflow-hidden">
13
+ <div class="w-full h-full bg-center bg-cover transition-all duration-200 group-hover:scale-110 " style="background-image: url('https://huggingface.co/datasets/enzostvs/loras-studio/resolve/main/{card.image}?expose=true');"></div>
14
  </div>
15
+ <div class="bg-black/40 backdrop-blur-sm border border-white/30 rounded-lg px-6 py-3 text-white transition-all duration-200 opacity-0 group-hover:opacity-100 w-full">
16
  <p class="text-white font-semibold text-base">{card.prompt}</p>
17
+ <p class="text-white/75 font-regular text-sm">{card.model.id}</p>
18
  </div>
19
  <div class="flex items-center justify-start gap-2">
20
+ {#if card.reactions.length > 0}
21
+ <Reactions reactions={card.reactions} />
22
+ {/if}
23
  <Add count={card?.reactions?.length} />
24
  </div>
25
  </div>
src/lib/components/community/reactions/Reactions.svelte ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { ReactionType } from "$lib/type";
3
+ import Reaction from "$lib/components/community/reactions/Reaction.svelte";
4
+
5
+ export let reactions: ReactionType[] = [];
6
+
7
+ const groupReactionsByEmoji = (reactions: ReactionType[]) => {
8
+ const grouped = new Set(reactions.map((reaction) => reaction.emoji));
9
+ return Array.from(grouped).map((emoji) => {
10
+ return {
11
+ emoji,
12
+ count: reactions.filter((reaction) => reaction.emoji === emoji).length,
13
+ };
14
+ });
15
+ };
16
+
17
+ const groupedReactions = groupReactionsByEmoji(reactions);
18
+ </script>
19
+
20
+ {#each groupedReactions as reaction}
21
+ <Reaction emoji={reaction.emoji} count={reaction?.count} />
22
+ {/each}
src/lib/components/fields/Textarea.svelte ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let placeholder: string = "Search";
3
+ export let value: string = "";
4
+ export let onChange: (value: string) => void = () => {};
5
+
6
+ const handleChange = (event: any) => {
7
+ const target = event.target as HTMLInputElement;
8
+ onChange(target.value as string);
9
+ }
10
+ </script>
11
+
12
+ <textarea
13
+ {value}
14
+ {placeholder}
15
+ rows="5"
16
+ class="bg-neutral-900 border border-neutral-800 rounded-lg text-neutral-200 text-base outline-none border-none placeholder:text-neutral-500 w-full px-4 py-3"
17
+ on:input={handleChange}
18
+ />
src/lib/components/generate/Banner.svelte ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import Banner from "$lib/assets/banner.webp";
3
+ import Sparkles from "$lib/assets/sparkles.svg";
4
+ import Button from "$lib/components/Button.svelte";
5
+ </script>
6
+
7
+ <div class="w-full rounded-xl mb-8 relative p-6 xl:p-8 flex flex-col xl:flex-row items-start xl:items-center justify-between gap-4 ring-4 ring-white/20">
8
+ <div
9
+ class="bg-cover bg-center brightness-75 absolute left-0 top-0 h-full w-full rounded-xl"
10
+ style={`
11
+ background-image: url(${Banner});
12
+ background-position: 0% 0%;
13
+ `}
14
+ />
15
+ <div>
16
+ <p class="text-xl xl:text-2xl font-extrabold text-white drop-shadow">
17
+ Share with community!
18
+ </p>
19
+ <p class="text-base font-medium text-white drop-shadow mt-1">
20
+ Once you generate an image, you can share it with the community!
21
+ <span class="hidden xl:block">
22
+ You can also see what others have generated, and like their images.
23
+ </span>
24
+ </p>
25
+ </div>
26
+ <Button href="/gallery">
27
+ See gallery
28
+ </Button>
29
+ <img src={Sparkles} alt="" class="absolute -top-4 -left-2 w-8 object-contain brightness-200 grayscale" />
30
+ <img src={Sparkles} alt="" class="absolute -rotate-180 -right-4 -bottom-2 w-8 object-contain brightness-200 grayscale" />
31
+ </div>
src/lib/components/generate/Response.svelte ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import Button from "../Button.svelte";
3
+
4
+ export let response: string | ArrayBuffer | null = '';
5
+ export let form: { model: any, inputs: string };
6
+
7
+ let loading: boolean = false;
8
+ let already_saved: boolean = false;
9
+
10
+ const saveImage = () => {
11
+ const link = document.createElement('a');
12
+ link.href = response as string;
13
+ link.download = `${form?.inputs?.slice(0, 20)}.png`;
14
+ document.body.appendChild(link);
15
+ link.click();
16
+ document.body.removeChild(link);
17
+ }
18
+
19
+ const share = () => {
20
+ if (loading) return;
21
+ loading = true;
22
+ fetch(`/api/generate/share`, {
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/json"
26
+ },
27
+ body: JSON.stringify({ image: response, generation: form })
28
+ }).then(() => {
29
+ loading = false;
30
+ already_saved = true;
31
+ })
32
+ }
33
+
34
+ </script>
35
+
36
+ <div class="w-full border-l border-neutral-800 h-full col-span-2" class:!border-black={!response}>
37
+ {#if response}
38
+ {#if typeof response === "string"}
39
+ <img src={response} alt="Generation" class="w-full mx-auto object-contain" />
40
+ <div class="p-8 w-full">
41
+ <div class="w-full flex items-center justify-end gap-4">
42
+ <Button size="lg" theme="light" icon="material-symbols:save" iconPosition="right" onClick={saveImage}>Save</Button>
43
+ <Button
44
+ size="lg"
45
+ theme="blue"
46
+ icon="bxs:share"
47
+ iconPosition="right"
48
+ loading={loading}
49
+ disabled={loading || already_saved}
50
+ onClick={share}
51
+ >
52
+ {#if already_saved}
53
+ Shared!
54
+ {:else}
55
+ Share with community
56
+ {/if}
57
+ </Button>
58
+ </div>
59
+ <p class="text-neutral-500 text-sm text-right mt-2.5">
60
+ All images not shared with the community are deleted right after generation.
61
+ <br>
62
+ Your informations are not shared with anyone.
63
+ </p>
64
+ {#if form}
65
+ <div class="mt-6 grid grid-cols-1 gap-4">
66
+ <div>
67
+ <p class="text-neutral-400 font-semibold text-xs uppercase">
68
+ Model selected
69
+ </p>
70
+ <div class="flex items-center justify-start gap-4 px-2 py-2.5 hover:bg-neutral-800/60 transition-all duration-200 rounded-lg cursor-pointer w-full text-left">
71
+ <img src={form?.model.image} alt={form?.model.title} class="w-14 h-14 rounded-lg object-cover" />
72
+ <div>
73
+ <p class="text-neutral-200 text-base font-medium">{form?.model.title}</p>
74
+ <p class="text-neutral-400 text-sm">{form?.model.id}</p>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <div>
79
+ <p class="text-neutral-400 font-semibold text-xs uppercase">
80
+ Prompt
81
+ </p>
82
+ <p class="text-neutral-200 text-base font-medium mt-2">"{form.inputs}"</p>
83
+ </div>
84
+ </div>
85
+ {/if}
86
+ </div>
87
+ {:else}
88
+ <div>
89
+ error displayed.
90
+ </div>
91
+ {/if}
92
+ {/if}
93
+ </div>
src/lib/components/models/Card.svelte CHANGED
@@ -8,16 +8,16 @@
8
  <div
9
  class="w-full cursor-pointer group bg-neutral-900 rounded-xl relative flex items-start justify-between flex-col p-3 border border-neutral-800 transition-all duration-200 brightness-75 hover:brightness-100 z-[1]"
10
  >
11
- <div class="w-full h-[270px] relative z-[1] mb-3 overflow-hidden">
12
  <img src="{card.image}" class="w-full h-full bg-center bg-cover rounded-lg object-cover object-center bg-neutral-800" alt="{card?.title}" />
13
  <div class="group-hover:opacity-100 opacity-0 translate-x-full group-hover:translate-x-0 transition-all duration-200 absolute right-3 bottom-3">
14
- <Button theme="light" size="md" href={`/generate?model=${card.repo}`}>
15
  Try it now
16
  </Button>
17
  </div>
18
  </div>
19
  <div class="flex items-center justify-between w-full gap-4 py-1">
20
- <p class="text-white font-semibold text-base mb-1 truncate">{card?.title ?? card?.repo}</p>
21
  <div class="flex items-center justify-end gap-3">
22
  <div class="text-white text-sm flex items-center justify-end gap-1.5">
23
  <Icon icon="solar:heart-bold" class="w-5 h-5 text-red-500" />
 
8
  <div
9
  class="w-full cursor-pointer group bg-neutral-900 rounded-xl relative flex items-start justify-between flex-col p-3 border border-neutral-800 transition-all duration-200 brightness-75 hover:brightness-100 z-[1]"
10
  >
11
+ <div class="w-full h-[350px] relative z-[1] mb-3 overflow-hidden">
12
  <img src="{card.image}" class="w-full h-full bg-center bg-cover rounded-lg object-cover object-center bg-neutral-800" alt="{card?.title}" />
13
  <div class="group-hover:opacity-100 opacity-0 translate-x-full group-hover:translate-x-0 transition-all duration-200 absolute right-3 bottom-3">
14
+ <Button theme="light" size="md" href={`/generate?model=${card.id}`}>
15
  Try it now
16
  </Button>
17
  </div>
18
  </div>
19
  <div class="flex items-center justify-between w-full gap-4 py-1">
20
+ <p class="text-white font-semibold text-base mb-1 truncate">{card?.title ?? card?.id}</p>
21
  <div class="flex items-center justify-end gap-3">
22
  <div class="text-white text-sm flex items-center justify-end gap-1.5">
23
  <Icon icon="solar:heart-bold" class="w-5 h-5 text-red-500" />
src/lib/components/models/Submit.svelte CHANGED
@@ -8,12 +8,12 @@
8
  let user = get(userStore);
9
  export let onClose: () => void;
10
  let model = {
11
- repo: '',
12
  title: '',
13
  image: '',
14
  }
15
  let error = {
16
- repo: '',
17
  title: '',
18
  image: ''
19
  }
@@ -33,7 +33,7 @@
33
  } else {
34
  console.log('Success:', data);
35
  error = {
36
- repo: '',
37
  title: '',
38
  image: ''
39
  }
@@ -70,14 +70,14 @@
70
  <span class="text-red-500">*</span>
71
  </p>
72
  <Input
73
- value={model.repo}
74
  placeholder="{`${user?.preferred_username ?? 'enzostvs'}/`}"
75
  prefix="huggingface.co/"
76
- onChange={(value) => model.repo = value}
77
  />
78
- {#if error.repo}
79
  <p class="text-xs text-red-500 mt-1">
80
- {error.repo}
81
  </p>
82
  {/if}
83
  </div>
 
8
  let user = get(userStore);
9
  export let onClose: () => void;
10
  let model = {
11
+ id: '',
12
  title: '',
13
  image: '',
14
  }
15
  let error = {
16
+ id: '',
17
  title: '',
18
  image: ''
19
  }
 
33
  } else {
34
  console.log('Success:', data);
35
  error = {
36
+ id: '',
37
  title: '',
38
  image: ''
39
  }
 
70
  <span class="text-red-500">*</span>
71
  </p>
72
  <Input
73
+ value={model.id}
74
  placeholder="{`${user?.preferred_username ?? 'enzostvs'}/`}"
75
  prefix="huggingface.co/"
76
+ onChange={(value) => model.id = value}
77
  />
78
+ {#if error.id}
79
  <p class="text-xs text-red-500 mt-1">
80
+ {error.id}
81
  </p>
82
  {/if}
83
  </div>
src/lib/components/models/autocomplete/Autocomplete.svelte CHANGED
@@ -8,8 +8,6 @@
8
  export let onChange: (model: ModelCard |Β null) => void;
9
  export let value: ModelCard | null = null;
10
 
11
- console.log(value)
12
-
13
  let models: ModelCard[] = [];
14
  let search: string = "";
15
  let open: boolean = false;
@@ -81,7 +79,13 @@
81
  {#if search?.length >= 3}
82
  {#if models?.length > 0}
83
  {#each models as model}
84
- <Item model={model} onClick={() => onChange(model)} />
 
 
 
 
 
 
85
  {/each}
86
  {:else}
87
  <div class="flex items-center justify-center flex-col gap-2 p-3">
@@ -91,7 +95,13 @@
91
  {/if}
92
  {:else}
93
  {#each defaultModels as model}
94
- <Item model={model} onClick={() => onChange(model)} />
 
 
 
 
 
 
95
  {/each}
96
  {/if}
97
  </div>
 
8
  export let onChange: (model: ModelCard |Β null) => void;
9
  export let value: ModelCard | null = null;
10
 
 
 
11
  let models: ModelCard[] = [];
12
  let search: string = "";
13
  let open: boolean = false;
 
79
  {#if search?.length >= 3}
80
  {#if models?.length > 0}
81
  {#each models as model}
82
+ <Item
83
+ model={model}
84
+ onClick={() => {
85
+ open = false;
86
+ onChange(model)
87
+ }}
88
+ />
89
  {/each}
90
  {:else}
91
  <div class="flex items-center justify-center flex-col gap-2 p-3">
 
95
  {/if}
96
  {:else}
97
  {#each defaultModels as model}
98
+ <Item
99
+ model={model}
100
+ onClick={() => {
101
+ open = false;
102
+ onChange(model)
103
+ }}
104
+ />
105
  {/each}
106
  {/if}
107
  </div>
src/lib/components/models/autocomplete/Item.svelte CHANGED
@@ -13,6 +13,6 @@
13
  <img src={model.image} alt={model.title} class="w-14 h-14 rounded-lg object-cover" />
14
  <div>
15
  <p class="text-neutral-200 text-base font-medium">{model.title}</p>
16
- <p class="text-neutral-400 text-sm">{model.repo}</p>
17
  </div>
18
  </button>
 
13
  <img src={model.image} alt={model.title} class="w-14 h-14 rounded-lg object-cover" />
14
  <div>
15
  <p class="text-neutral-200 text-base font-medium">{model.title}</p>
16
+ <p class="text-neutral-400 text-sm">{model.id}</p>
17
  </div>
18
  </button>
src/lib/type.ts CHANGED
@@ -8,20 +8,22 @@ export interface OptionRadio {
8
  export interface CommunityCard {
9
  reactions: ReactionType[],
10
  id: string,
11
- model_name: string,
12
  prompt: string,
13
  image: string,
14
  }
15
 
16
  export interface ModelCard {
17
  title: string,
18
- repo: string,
19
  likes: number,
20
  downloads: number,
21
  image: string,
22
  }
23
 
24
  export interface ReactionType {
 
 
25
  emoji: string
26
- users: string[]
27
  }
 
8
  export interface CommunityCard {
9
  reactions: ReactionType[],
10
  id: string,
11
+ model: ModelCard,
12
  prompt: string,
13
  image: string,
14
  }
15
 
16
  export interface ModelCard {
17
  title: string,
18
+ id: string,
19
  likes: number,
20
  downloads: number,
21
  image: string,
22
  }
23
 
24
  export interface ReactionType {
25
+ id: string
26
+ hf_user_id: string;
27
  emoji: string
28
+ galleryId: string
29
  }
src/lib/utils/index.ts CHANGED
@@ -30,14 +30,14 @@ export const MODELS_FILTER_OPTIONS = [
30
  },
31
  ];
32
 
33
- export const SIDEBAR_MENUS = [{
34
- icon: "solar:gallery-bold-duotone",
35
- label: "Gallery",
36
- href: "/",
37
- }, {
38
  icon: "uim:cube",
39
  label: "Models",
40
- href: "/models",
 
 
 
 
41
  }, {
42
  icon: "fluent:glance-horizontal-sparkles-16-filled",
43
  label: "Generate",
 
30
  },
31
  ];
32
 
33
+ export const SIDEBAR_MENUS = [ {
 
 
 
 
34
  icon: "uim:cube",
35
  label: "Models",
36
+ href: "/",
37
+ }, {
38
+ icon: "solar:gallery-bold-duotone",
39
+ label: "Gallery",
40
+ href: "/gallery",
41
  }, {
42
  icon: "fluent:glance-horizontal-sparkles-16-filled",
43
  label: "Generate",
src/lib/utils/uploader.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { uploadFile } from "@huggingface/hub";
2
+ import type { RepoDesignation, Credentials } from "@huggingface/hub";
3
+ import { env } from '$env/dynamic/private'
4
+
5
+ const TOTAL_FOLDERS = 10
6
+ // const MAX_IMAGES_PER_FOLDER = 10_000
7
+
8
+ export const UploaderDataset = async (blob: Blob, name: string) => {
9
+ const repo: RepoDesignation = { type: "dataset", name: "enzostvs/loras-studio" };
10
+ const credentials: Credentials = { accessToken: env.SECRET_HF_TOKEN as string };
11
+
12
+ const folder_key = Math.floor(Math.random() * TOTAL_FOLDERS)
13
+ const formatted_name = name.replace(/[^a-zA-Z0-9]/g, "-")
14
+ const path = `images-${folder_key}/${formatted_name}.png`
15
+
16
+ try {
17
+ await uploadFile({
18
+ repo,
19
+ credentials,
20
+ file:
21
+ {
22
+ path,
23
+ content: blob,
24
+ },
25
+ })
26
+ return { path, ok: true }
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ } catch (e: any) {
29
+ console.error(e)
30
+ return { ok: false }
31
+ }
32
+ }
src/routes/+layout.server.ts CHANGED
@@ -6,5 +6,5 @@ export async function load({ fetch }) {
6
  }
7
  })
8
  const user = await response.json()
9
- return user
10
  }
 
6
  }
7
  })
8
  const user = await response.json()
9
+ return { user }
10
  }
src/routes/+layout.svelte CHANGED
@@ -9,7 +9,7 @@
9
 
10
  <div class="flex items-start">
11
  <Sidebar />
12
- <main id="app" class="px-6 py-10 lg:px-10 lg:py-12 flex-1 h-screen overflow-y-auto">
13
  <slot />
14
  </main>
15
  </div>
 
9
 
10
  <div class="flex items-start">
11
  <Sidebar />
12
+ <main id="app" class="flex-1 h-screen overflow-y-auto">
13
  <slot />
14
  </main>
15
  </div>
src/routes/+page.svelte CHANGED
@@ -1,62 +1,100 @@
1
  <script lang="ts">
2
  import { browser } from "$app/environment";
3
  import InfiniteScroll from "svelte-infinite-scroll";
4
-
5
  import Button from "$lib/components/Button.svelte";
6
- import Card from "$lib/components/community/Card.svelte";
7
  import Input from "$lib/components/fields/Input.svelte";
8
  import Radio from "$lib/components/fields/Radio.svelte";
9
- import { COMMUNITY_FILTER_OPTIONS } from "$lib/utils/index.js";
10
  import GoTop from "$lib/components/GoTop.svelte";
 
 
11
 
12
- export let data;
13
 
14
  let form = {
15
- filter: "likes",
 
16
  page: "0",
17
  }
 
18
 
19
  $: elementScroll = browser ? document?.getElementById('app') : undefined;
20
 
21
- const fetchMore = async () => {
22
  form = {...form, page: (Number(form.page) + 1).toString()};
23
- const request = await fetch(`/api/community?${new URLSearchParams(form)}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  const response = await request.json();
25
- data = {...data, cards: [...data.cards, ...response.cards ]};
 
26
  }
27
  </script>
28
 
29
  <svelte:head>
30
- <title>Community Gallery</title>
31
  <meta name="description" content="Svelte demo app" />
32
  </svelte:head>
33
 
34
- <h1 class="text-white font-semibold text-2xl">
35
- Community Gallery
36
- </h1>
37
- <div class="flex items-center justify-between mt-5">
38
- <Radio options={COMMUNITY_FILTER_OPTIONS} value="{form.filter}" onChange={(filter) => form = {...form, filter }} />
39
- <div class="items-center justify-end gap-5 hidden lg:flex">
40
- <Button icon="ic:round-plus" theme="dark" size="lg">Upload own Image</Button>
41
- <Button icon="fluent:glance-horizontal-sparkles-16-filled" href="/generate" theme="pink" size="lg">Generate</Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  </div>
43
- <div class="items-center justify-end gap-3 flex lg:hidden">
44
- <Button icon="ic:round-plus" theme="dark" size="md">Upload own Image</Button>
45
- <Button icon="fluent:glance-horizontal-sparkles-16-filled" href="/generate" theme="pink" size="md">Generate</Button>
 
 
 
 
 
 
 
 
46
  </div>
47
- </div>
48
- <div class="mt-5 max-w-sm">
49
- <Input placeholder="Filter by prompt, model..." />
50
- </div>
51
- <div class="mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-5 mt-8 lg:mt-10">
52
- {#each data.cards as card}
53
- <Card card={card} />
54
- {/each}
55
- <InfiniteScroll
56
- elementScroll="{elementScroll ?? undefined}"
57
- threshold={100}
58
- hasMore={data.total_items > data.cards.length}
59
- on:loadMore={fetchMore}
60
- />
61
- <GoTop />
62
- </div>
 
1
  <script lang="ts">
2
  import { browser } from "$app/environment";
3
  import InfiniteScroll from "svelte-infinite-scroll";
 
4
  import Button from "$lib/components/Button.svelte";
5
+ import Card from "$lib/components/models/Card.svelte";
6
  import Input from "$lib/components/fields/Input.svelte";
7
  import Radio from "$lib/components/fields/Radio.svelte";
8
+ import { MODELS_FILTER_OPTIONS } from "$lib/utils/index.js";
9
  import GoTop from "$lib/components/GoTop.svelte";
10
+ import Dialog from "$lib/components/dialog/Dialog.svelte";
11
+ import SubmitModel from "$lib/components/models/Submit.svelte";
12
 
13
+ export let data
14
 
15
  let form = {
16
+ filter: "hotest",
17
+ search: "",
18
  page: "0",
19
  }
20
+ let submitModelDialog = false;
21
 
22
  $: elementScroll = browser ? document?.getElementById('app') : undefined;
23
 
24
+ const handleFetchMore = async () => {
25
  form = {...form, page: (Number(form.page) + 1).toString()};
26
+ refetch(true);
27
+ }
28
+ const handleChangeFilter = async (filter: string) => {
29
+ form = { ...form, filter, page: (0).toString()};
30
+ refetch(false)
31
+ }
32
+ let timeout: any;
33
+ const handleChangeSearch = async (search: string) => {
34
+ clearTimeout(timeout);
35
+ form = { ...form, search, page: (0).toString()};
36
+ timeout = setTimeout(() => refetch(false), 500);
37
+ }
38
+
39
+ const refetch = async (add: boolean) => {
40
+ const request = await fetch(`/api/models?${new URLSearchParams(form)}`);
41
  const response = await request.json();
42
+ if (add) data = {...data, cards: [...data.cards, ...response.cards ]};
43
+ else data = response;
44
  }
45
  </script>
46
 
47
  <svelte:head>
48
+ <title>Explore Models</title>
49
  <meta name="description" content="Svelte demo app" />
50
  </svelte:head>
51
 
52
+ <main class="px-6 py-10 lg:px-10 lg:py-12">
53
+ <Dialog open={submitModelDialog} onClose={() => submitModelDialog = false}>
54
+ <SubmitModel onClose={() => submitModelDialog = false} />
55
+ </Dialog>
56
+ <h1 class="text-white font-semibold text-2xl">
57
+ Explore Models ({data.total_items})
58
+ </h1>
59
+ <div class="flex items-start sm:items-center justify-between mt-5 flex-col sm:flex-row gap-5 sm:justify-between">
60
+ <Radio options={MODELS_FILTER_OPTIONS} value="{form.filter}" onChange={handleChangeFilter} />
61
+ <div class="items-center justify-end gap-5 hidden lg:flex">
62
+ <Button href="https://huggingface.co/new/stable-diffusion-lora" target="_blank" icon="ic:round-plus" theme="dark" size="lg">Create</Button>
63
+ <Button
64
+ icon="octicon:upload-16"
65
+ theme="blue"
66
+ size="lg"
67
+ onClick={() => submitModelDialog = true}
68
+ >
69
+ Upload model
70
+ </Button>
71
+ </div>
72
+ <div class="items-center justify-end gap-3 flex lg:hidden">
73
+ <Button href="https://huggingface.co/new/stable-diffusion-lora" target="_blank" icon="ic:round-plus" theme="dark" size="md">Create</Button>
74
+ <Button
75
+ icon="octicon:upload-16"
76
+ theme="blue"
77
+ size="md"
78
+ onClick={() => submitModelDialog = true}
79
+ >
80
+ Upload model
81
+ </Button>
82
+ </div>
83
+ </div>
84
+ <div class="mt-5 max-w-sm">
85
+ <Input value={form.search} placeholder="Search a model" onChange={handleChangeSearch} />
86
  </div>
87
+ <div class="mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-5 mt-8 lg:mt-10">
88
+ {#each data.cards as card}
89
+ <Card card={card} />
90
+ {/each}
91
+ <InfiniteScroll
92
+ elementScroll="{elementScroll ?? undefined}"
93
+ threshold={100}
94
+ hasMore={data.total_items > data.cards.length}
95
+ on:loadMore={handleFetchMore}
96
+ />
97
+ <GoTop />
98
  </div>
99
+
100
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/+page.ts CHANGED
@@ -1,5 +1,5 @@
1
  export async function load({ fetch }) {
2
- const response = await fetch("/api/community?page=1", {
3
  method: "GET",
4
  headers: {
5
  "Content-Type": "application/json"
 
1
  export async function load({ fetch }) {
2
+ const response = await fetch("/api/models?page=0&filter=hotest", {
3
  method: "GET",
4
  headers: {
5
  "Content-Type": "application/json"
src/routes/api/bulk-create-models/+server.ts CHANGED
@@ -1,16 +1,13 @@
 
 
1
  import { json } from '@sveltejs/kit';
2
  import prisma from '$lib/prisma';
3
- // import { env } from '$env/dynamic/private'
4
-
5
- // import jsonData from "$lib/utils/loras.json";
6
- // import type { ModelCard } from '$lib/type';
7
-
8
- /** @type {import('./$types').RequestHandler} */
9
 
10
  export async function POST({ request }) {
11
  const headers = Object.fromEntries(request.headers.entries());
12
 
13
- if (headers["x-hf-token"] !== process.env.HF_TOKEN) {
14
  return Response.json({
15
  message: "Wrong castle fam :^)"
16
  }, { status: 401 });
@@ -19,11 +16,11 @@ export async function POST({ request }) {
19
  const { models } = await request.json();
20
 
21
  const cards = await Promise.all(models.map(async (model: Record<string, string>) => {
22
- const res = await fetch(`https://huggingface.co/api/models/${model.repo}`)
23
  const data = await res.json();
24
  const mergedData = {
25
  image: model.image,
26
- repo: model.repo,
27
  title: model.title,
28
  likes: data.likes,
29
  downloads: data.downloads,
@@ -35,7 +32,7 @@ export async function POST({ request }) {
35
  for (const model of cards) {
36
  await prisma.model.create({
37
  data: {
38
- repo: model.repo,
39
  image: model.image,
40
  title: model.title,
41
  likes: model.likes,
 
1
+ /** @type {import('./$types').RequestHandler} */
2
+
3
  import { json } from '@sveltejs/kit';
4
  import prisma from '$lib/prisma';
5
+ import { env } from '$env/dynamic/private'
 
 
 
 
 
6
 
7
  export async function POST({ request }) {
8
  const headers = Object.fromEntries(request.headers.entries());
9
 
10
+ if (headers["x-hf-token"] !== env.SECRET_HF_TOKEN) {
11
  return Response.json({
12
  message: "Wrong castle fam :^)"
13
  }, { status: 401 });
 
16
  const { models } = await request.json();
17
 
18
  const cards = await Promise.all(models.map(async (model: Record<string, string>) => {
19
+ const res = await fetch(`https://huggingface.co/api/models/${model.id}`)
20
  const data = await res.json();
21
  const mergedData = {
22
  image: model.image,
23
+ id: model.repo,
24
  title: model.title,
25
  likes: data.likes,
26
  downloads: data.downloads,
 
32
  for (const model of cards) {
33
  await prisma.model.create({
34
  data: {
35
+ id: model.id,
36
  image: model.image,
37
  title: model.title,
38
  likes: model.likes,
src/routes/api/community/+server.ts CHANGED
@@ -1,22 +1,47 @@
1
- import { error, json, type RequestEvent } from '@sveltejs/kit';
2
- // import { env } from '$env/dynamic/private'
3
-
4
- import jsonData from "$lib/cards.json";
5
 
6
  /** @type {import('./$types').RequestHandler} */
7
 
8
  export async function GET(request : RequestEvent) {
9
- const hasError = false
10
-
11
  const page = parseInt(request.url.searchParams.get('page') || '0')
12
- if (hasError) {
13
- return error(500, 'Internal Server Error')
14
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- const cards = jsonData.slice(page * 25, page * 25 + 25)
 
 
 
 
 
 
17
 
18
  return json({
19
  cards,
20
- total_items: jsonData.length,
21
  })
22
  }
 
1
+ import { json, type RequestEvent } from '@sveltejs/kit';
2
+ import prisma from '$lib/prisma';
 
 
3
 
4
  /** @type {import('./$types').RequestHandler} */
5
 
6
  export async function GET(request : RequestEvent) {
 
 
7
  const page = parseInt(request.url.searchParams.get('page') || '0')
8
+ const filter = request.url.searchParams.get('filter') || 'new'
9
+ const search = request.url.searchParams.get('search') || ''
10
+ const limit = parseInt(request.url.searchParams.get('limit') || '20')
11
+
12
+ const cards = await prisma.gallery.findMany({
13
+ where: {
14
+ OR: [
15
+ { prompt: { contains: search } },
16
+ ]
17
+ },
18
+ orderBy: {
19
+ ...(filter === 'new' ? {
20
+ createdAt: 'desc'
21
+ } : {}
22
+ )
23
+ },
24
+ select: {
25
+ reactions: true,
26
+ id: true,
27
+ prompt: true,
28
+ image: true,
29
+ model: true,
30
+ },
31
+ skip: limit * page,
32
+ take: limit,
33
+ })
34
 
35
+ const total_reposId = await prisma.gallery.count({
36
+ where: {
37
+ OR: [
38
+ { prompt: { contains: search } },
39
+ ]
40
+ },
41
+ })
42
 
43
  return json({
44
  cards,
45
+ total_items: total_reposId
46
  })
47
  }
src/routes/api/generate/+server.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('./$types').RequestHandler} */
2
+
3
+ import { json, type RequestEvent } from '@sveltejs/kit';
4
+ import { env } from '$env/dynamic/private'
5
+
6
+ export async function POST({ request } : RequestEvent) {
7
+ const generation = await request.json()
8
+
9
+ if (!generation?.model?.id) {
10
+ return json({
11
+ error: {
12
+ token: "A model id is required"
13
+ }
14
+ }, { status: 400 })
15
+ }
16
+
17
+ if (!generation?.inputs) {
18
+ return json({
19
+ error: {
20
+ token: "An inputs is required"
21
+ }
22
+ }, { status: 400 })
23
+ }
24
+
25
+ const response = await fetch(env.SECRET_INFERENCE_API_URL + "/models/" + generation?.model?.id, {
26
+ method: "POST",
27
+ headers: {
28
+ Authorization: `Bearer ${env.SECRET_HF_TOKEN}`,
29
+ 'Content-Type': 'application/json',
30
+ ['x-use-cache']: "0"
31
+ },
32
+ body: JSON.stringify(generation),
33
+ })
34
+ .then((res) => res.blob())
35
+ .then((blob) => blob)
36
+ .catch((error) => {
37
+ return {
38
+ error: error.message,
39
+ }
40
+ })
41
+
42
+ if ("error" in response) {
43
+ return json({
44
+ error: {
45
+ token: response.error
46
+ }
47
+ }, { status: 400 })
48
+ }
49
+
50
+ return new Response(response, {
51
+ headers: {
52
+ 'Content-Type': 'image/png',
53
+ },
54
+ })
55
+ }
src/routes/api/generate/share/+server.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('./$types').RequestHandler} */
2
+
3
+ import { UploaderDataset } from '$lib/utils/uploader';
4
+ import { json, type RequestEvent } from '@sveltejs/kit';
5
+
6
+ import prisma from '$lib/prisma';
7
+
8
+ export async function POST({ request } : RequestEvent) {
9
+ const { generation, image } = await request.json()
10
+
11
+ if (!generation?.model?.id) {
12
+ return json({
13
+ error: {
14
+ token: "A model id is required"
15
+ }
16
+ }, { status: 400 })
17
+ }
18
+
19
+ if (!generation?.inputs) {
20
+ return json({
21
+ error: {
22
+ token: "An inputs is required"
23
+ }
24
+ }, { status: 400 })
25
+ }
26
+
27
+ const blob = await fetch(image)
28
+ .then((res) => res.blob())
29
+ .then((blob) => blob)
30
+ .catch((error) => {
31
+ return json({
32
+ error: error.message,
33
+ }, { status: 400 })
34
+ })
35
+
36
+ const success: {
37
+ ok: boolean,
38
+ path?: string | undefined
39
+ } = await UploaderDataset(blob as Blob, generation.inputs)
40
+
41
+ if (!success.ok) {
42
+ return json({
43
+ error: {
44
+ token: "Error uploading image"
45
+ }
46
+ }, { status: 400 })
47
+ }
48
+
49
+ const gallery = prisma.gallery.create({
50
+ data: {
51
+ image: success.path as string,
52
+ prompt: generation.inputs,
53
+ model: {
54
+ connect: {
55
+ id: generation.model.id
56
+ }
57
+ },
58
+ }
59
+ })
60
+ .catch((error) => {
61
+ console.log(error)
62
+ })
63
+
64
+ return json({
65
+ message: "Successfully generated image",
66
+ gallery
67
+ })
68
+ }
src/routes/api/models/+server.ts CHANGED
@@ -1,12 +1,8 @@
1
- import { error, json, type RequestEvent } from '@sveltejs/kit';
2
  import prisma from '$lib/prisma';
3
 
4
  /** @type {import('./$types').RequestHandler} */
5
 
6
- // TODO
7
- // run hooks to update each model by repo using hugging face api.
8
- // refer to bulk-create-models +server.ts for example
9
-
10
  export async function GET(request : RequestEvent) {
11
  const page = parseInt(request.url.searchParams.get('page') || '0')
12
  const filter = request.url.searchParams.get('filter') || 'hotest'
@@ -18,7 +14,7 @@ export async function GET(request : RequestEvent) {
18
  isPublic: true,
19
  OR: [
20
  { title: { contains: search } },
21
- { repo: { contains: search } },
22
  ]
23
  },
24
  orderBy: {
@@ -33,16 +29,11 @@ export async function GET(request : RequestEvent) {
33
  isPublic: true,
34
  OR: [
35
  { title: { contains: search } },
36
- { repo: { contains: search } },
37
  ]
38
  },
39
  })
40
 
41
- const hasError = false
42
- if (hasError) {
43
- return error(500, 'Internal Server Error')
44
- }
45
-
46
  return json({
47
  cards,
48
  total_items: total_reposId
 
1
+ import { json, type RequestEvent } from '@sveltejs/kit';
2
  import prisma from '$lib/prisma';
3
 
4
  /** @type {import('./$types').RequestHandler} */
5
 
 
 
 
 
6
  export async function GET(request : RequestEvent) {
7
  const page = parseInt(request.url.searchParams.get('page') || '0')
8
  const filter = request.url.searchParams.get('filter') || 'hotest'
 
14
  isPublic: true,
15
  OR: [
16
  { title: { contains: search } },
17
+ { id: { contains: search } },
18
  ]
19
  },
20
  orderBy: {
 
29
  isPublic: true,
30
  OR: [
31
  { title: { contains: search } },
32
+ { id: { contains: search } },
33
  ]
34
  },
35
  })
36
 
 
 
 
 
 
37
  return json({
38
  cards,
39
  total_items: total_reposId
src/routes/api/models/{[repo] β†’ [id]}/+server.ts RENAMED
@@ -4,11 +4,11 @@ import prisma from '$lib/prisma';
4
  /** @type {import('./$types').RequestHandler} */
5
 
6
  export async function GET(request : RequestEvent) {
7
- const repo = request.params.repo?.replace("@", "/")
8
 
9
  const model = await prisma.model.findFirst({
10
  where: {
11
- repo
12
  }
13
  })
14
 
 
4
  /** @type {import('./$types').RequestHandler} */
5
 
6
  export async function GET(request : RequestEvent) {
7
+ const id = request.params.id?.replace("@", "/")
8
 
9
  const model = await prisma.model.findFirst({
10
  where: {
11
+ id
12
  }
13
  })
14
 
src/routes/api/models/submit/+server.ts CHANGED
@@ -27,13 +27,13 @@ export async function POST({ request, fetch, cookies }) {
27
  }
28
 
29
  // get model on hugging face
30
- const res = await fetch(`https://huggingface.co/api/models/${model.repo}`)
31
  const data = await res.json();
32
 
33
  if (data?.error) {
34
  return json({
35
  error: {
36
- repo: "Model not found on Hugging Face"
37
  }
38
  }, { status: 404 })
39
  }
@@ -54,7 +54,7 @@ export async function POST({ request, fetch, cookies }) {
54
 
55
  await prisma.model.create({
56
  data: {
57
- repo: model.repo,
58
  image: model.image,
59
  title: model.title,
60
  likes: data.likes,
 
27
  }
28
 
29
  // get model on hugging face
30
+ const res = await fetch(`https://huggingface.co/api/models/${model.id}`)
31
  const data = await res.json();
32
 
33
  if (data?.error) {
34
  return json({
35
  error: {
36
+ id: "Model not found on Hugging Face"
37
  }
38
  }, { status: 404 })
39
  }
 
54
 
55
  await prisma.model.create({
56
  data: {
57
+ id: model.id,
58
  image: model.image,
59
  title: model.title,
60
  likes: data.likes,
src/routes/{models β†’ gallery}/+page.svelte RENAMED
@@ -1,23 +1,21 @@
1
  <script lang="ts">
2
  import { browser } from "$app/environment";
3
  import InfiniteScroll from "svelte-infinite-scroll";
 
4
  import Button from "$lib/components/Button.svelte";
5
- import Card from "$lib/components/models/Card.svelte";
6
  import Input from "$lib/components/fields/Input.svelte";
7
  import Radio from "$lib/components/fields/Radio.svelte";
8
- import { MODELS_FILTER_OPTIONS } from "$lib/utils/index.js";
9
  import GoTop from "$lib/components/GoTop.svelte";
10
- import Dialog from "$lib/components/dialog/Dialog.svelte";
11
- import SubmitModel from "$lib/components/models/Submit.svelte";
12
 
13
- export let data;
14
 
15
  let form = {
16
- filter: "hotest",
17
- search: "",
18
  page: "0",
 
19
  }
20
- let submitModelDialog = false;
21
 
22
  $: elementScroll = browser ? document?.getElementById('app') : undefined;
23
 
@@ -37,7 +35,7 @@
37
  }
38
 
39
  const refetch = async (add: boolean) => {
40
- const request = await fetch(`/api/models?${new URLSearchParams(form)}`);
41
  const response = await request.json();
42
  if (add) data = {...data, cards: [...data.cards, ...response.cards ]};
43
  else data = response;
@@ -45,53 +43,38 @@
45
  </script>
46
 
47
  <svelte:head>
48
- <title>Explore Models</title>
49
  <meta name="description" content="Svelte demo app" />
50
  </svelte:head>
51
 
52
- <Dialog open={submitModelDialog} onClose={() => submitModelDialog = false}>
53
- <SubmitModel onClose={() => submitModelDialog = false} />
54
- </Dialog>
55
- <h1 class="text-white font-semibold text-2xl">
56
- Explore Models ({data.total_items})
57
- </h1>
58
- <div class="flex items-start sm:items-center justify-between mt-5 flex-col sm:flex-row gap-5 sm:justify-between">
59
- <Radio options={MODELS_FILTER_OPTIONS} value="{form.filter}" onChange={handleChangeFilter} />
60
- <div class="items-center justify-end gap-5 hidden lg:flex">
61
- <Button href="https://huggingface.co/new" target="_blank" icon="ic:round-plus" theme="dark" size="lg">Create</Button>
62
- <Button
63
- icon="octicon:upload-16"
64
- theme="blue"
65
- size="lg"
66
- onClick={() => submitModelDialog = true}
67
- >
68
- Submit model
69
- </Button>
70
  </div>
71
- <div class="items-center justify-end gap-3 flex lg:hidden">
72
- <Button href="https://huggingface.co/new" target="_blank" icon="ic:round-plus" theme="dark" size="md">Create</Button>
73
- <Button
74
- icon="octicon:upload-16"
75
- theme="blue"
76
- size="md"
77
- onClick={() => submitModelDialog = true}
78
- >
79
- Submit model
80
- </Button>
 
81
  </div>
82
- </div>
83
- <div class="mt-5 max-w-sm">
84
- <Input value={form.search} placeholder="Search a model" onChange={handleChangeSearch} />
85
- </div>
86
- <div class="mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-5 mt-8 lg:mt-10">
87
- {#each data.cards as card}
88
- <Card card={card} />
89
- {/each}
90
- <InfiniteScroll
91
- elementScroll="{elementScroll ?? undefined}"
92
- threshold={100}
93
- hasMore={data.total_items > data.cards.length}
94
- on:loadMore={handleFetchMore}
95
- />
96
- <GoTop />
97
- </div>
 
1
  <script lang="ts">
2
  import { browser } from "$app/environment";
3
  import InfiniteScroll from "svelte-infinite-scroll";
4
+
5
  import Button from "$lib/components/Button.svelte";
6
+ import Card from "$lib/components/community/Card.svelte";
7
  import Input from "$lib/components/fields/Input.svelte";
8
  import Radio from "$lib/components/fields/Radio.svelte";
9
+ import { COMMUNITY_FILTER_OPTIONS } from "$lib/utils/index.js";
10
  import GoTop from "$lib/components/GoTop.svelte";
 
 
11
 
12
+ export let data
13
 
14
  let form = {
15
+ filter: "new",
 
16
  page: "0",
17
+ search: ""
18
  }
 
19
 
20
  $: elementScroll = browser ? document?.getElementById('app') : undefined;
21
 
 
35
  }
36
 
37
  const refetch = async (add: boolean) => {
38
+ const request = await fetch(`/api/community?${new URLSearchParams(form)}`);
39
  const response = await request.json();
40
  if (add) data = {...data, cards: [...data.cards, ...response.cards ]};
41
  else data = response;
 
43
  </script>
44
 
45
  <svelte:head>
46
+ <title>Community Gallery</title>
47
  <meta name="description" content="Svelte demo app" />
48
  </svelte:head>
49
 
50
+ <main class="px-6 py-10 lg:px-10 lg:py-12">
51
+ <h1 class="text-white font-semibold text-2xl">
52
+ Community Gallery ({data.total_items})
53
+ </h1>
54
+ <div class="flex items-center justify-between mt-5">
55
+ <Radio options={COMMUNITY_FILTER_OPTIONS} value="{form.filter}" onChange={handleChangeFilter} />
56
+ <div class="items-center justify-end gap-5 hidden lg:flex">
57
+ <Button icon="ic:round-plus" theme="dark" size="lg">Upload own Image</Button>
58
+ <Button icon="fluent:glance-horizontal-sparkles-16-filled" href="/generate" theme="pink" size="lg">Generate</Button>
59
+ </div>
60
+ <div class="items-center justify-end gap-3 flex lg:hidden">
61
+ <Button icon="ic:round-plus" theme="dark" size="md">Upload own Image</Button>
62
+ <Button icon="fluent:glance-horizontal-sparkles-16-filled" href="/generate" theme="pink" size="md">Generate</Button>
63
+ </div>
64
+ </div>
65
+ <div class="mt-5 max-w-sm">
66
+ <Input value={form.search} placeholder="Search an image" onChange={handleChangeSearch} />
 
67
  </div>
68
+ <div class="mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-5 mt-8 lg:mt-10">
69
+ {#each data.cards as card}
70
+ <Card card={card} />
71
+ {/each}
72
+ <InfiniteScroll
73
+ elementScroll="{elementScroll ?? undefined}"
74
+ threshold={100}
75
+ hasMore={data.total_items > data.cards.length}
76
+ on:loadMore={handleFetchMore}
77
+ />
78
+ <GoTop />
79
  </div>
80
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/{models β†’ gallery}/+page.ts RENAMED
@@ -1,5 +1,5 @@
1
  export async function load({ fetch }) {
2
- const response = await fetch("/api/models?page=0&filter=hotest", {
3
  method: "GET",
4
  headers: {
5
  "Content-Type": "application/json"
 
1
  export async function load({ fetch }) {
2
+ const response = await fetch("/api/community?page=0&filter=hotest", {
3
  method: "GET",
4
  headers: {
5
  "Content-Type": "application/json"
src/routes/generate/+page.svelte CHANGED
@@ -4,22 +4,63 @@
4
  </svelte:head>
5
 
6
  <script lang="ts">
 
 
 
 
7
  import Autocomplete from "$lib/components/models/autocomplete/Autocomplete.svelte";
8
 
9
  export let data
10
 
 
 
 
11
  let form = {
12
  model: data?.model ?? null,
13
- prompt: ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
  </script>
16
 
17
- <div class="flex items-start w-full h-full gap-16">
18
- <div class="w-full">
 
19
  <h1 class="text-white font-semibold text-2xl">
20
  Start generating
21
  </h1>
22
- <div class="mt-5 grid grid-cols-1 gap-5">
23
  <div>
24
  <p class="text-neutral-300 mb-2.5 text-base">Models</p>
25
  <Autocomplete
@@ -28,11 +69,34 @@
28
  onChange={(model) => form.model = model}
29
  />
30
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </div>
32
  </div>
33
- <div class="w-full bg-neutral-100 rounded-2xl h-full p-8">
34
- <p class="text-neutral-900 font-semibold text-xl">
35
- Result of your generation
36
- </p>
37
- </div>
38
- </div>
 
4
  </svelte:head>
5
 
6
  <script lang="ts">
7
+ import Button from "$lib/components/Button.svelte";
8
+ import Textarea from "$lib/components/fields/Textarea.svelte";
9
+ import Banner from "$lib/components/generate/Banner.svelte";
10
+ import Response from "$lib/components/generate/Response.svelte";
11
  import Autocomplete from "$lib/components/models/autocomplete/Autocomplete.svelte";
12
 
13
  export let data
14
 
15
+ let loading: boolean = false;
16
+ let response: string | ArrayBuffer | null = '';
17
+
18
  let form = {
19
  model: data?.model ?? null,
20
+ inputs: "",
21
+ parameters: {
22
+ negative_prompt: ""
23
+ }
24
+ }
25
+
26
+ const handleSubmit = async () => {
27
+ if (loading) return
28
+ loading = true
29
+
30
+ const request = await fetch(`/api/generate`, {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json"
34
+ },
35
+ body: JSON.stringify(form)
36
+ });
37
+ const blob = await request?.clone()?.blob()
38
+
39
+ if (blob) {
40
+ const reader = new FileReader()
41
+ reader.readAsDataURL(blob)
42
+ reader.onloadend = () => {
43
+ const base64data = reader.result
44
+ response = base64data
45
+ }
46
+ }
47
+
48
+ const res = await request.clone().json().catch(() => ({}))
49
+ if (res) {
50
+ response = res
51
+ }
52
+
53
+ loading = false
54
  }
55
  </script>
56
 
57
+ <main class="grid grid-cols-5 w-full h-full gap-10">
58
+ <div class="w-full px-6 py-10 lg:px-10 lg:py-12 col-span-3">
59
+ <Banner />
60
  <h1 class="text-white font-semibold text-2xl">
61
  Start generating
62
  </h1>
63
+ <div class="mt-5 grid grid-cols-1 gap-6">
64
  <div>
65
  <p class="text-neutral-300 mb-2.5 text-base">Models</p>
66
  <Autocomplete
 
69
  onChange={(model) => form.model = model}
70
  />
71
  </div>
72
+ <div>
73
+ <p class="text-neutral-300 mb-2.5 text-base">Prompt</p>
74
+ <Textarea
75
+ value={form?.inputs}
76
+ placeholder="Aerial photography of a desert through autumn forests, with vibrant red and orange foliage"
77
+ onChange={(inputs) => form.inputs = inputs}
78
+ />
79
+ </div>
80
+ <div>
81
+ <p class="text-neutral-300 mb-2.5 text-base">Negative Prompt</p>
82
+ <Textarea
83
+ value={form?.parameters?.negative_prompt}
84
+ placeholder="Write your negative prompt here"
85
+ onChange={(negative_prompt) => form.parameters.negative_prompt = negative_prompt}
86
+ />
87
+ </div>
88
+ <div class="flex justify-end">
89
+ <Button
90
+ icon="fluent:glance-horizontal-sparkles-16-filled"
91
+ theme="pink"
92
+ size="lg"
93
+ {loading}
94
+ onClick={handleSubmit}
95
+ >
96
+ Generate
97
+ </Button>
98
+ </div>
99
  </div>
100
  </div>
101
+ <Response response={response} form={form} />
102
+ </main>