Spaces:
Runtime error
Runtime error
prompt submssion
Browse files- frontend/package.json +1 -0
- frontend/src/lib/App.svelte +98 -3
- frontend/src/lib/Canvas.svelte +9 -3
- frontend/src/lib/PromptModal.svelte +42 -0
- frontend/src/lib/store.ts +2 -0
- frontend/src/lib/utils.ts +21 -0
- frontend/src/routes/+page.svelte +2 -30
frontend/package.json
CHANGED
@@ -22,6 +22,7 @@
|
|
22 |
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
23 |
"@typescript-eslint/parser": "^5.38.0",
|
24 |
"autoprefixer": "^10.4.12",
|
|
|
25 |
"eslint": "^8.24.0",
|
26 |
"eslint-config-prettier": "^8.3.0",
|
27 |
"eslint-plugin-svelte3": "^4.0.0",
|
|
|
22 |
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
23 |
"@typescript-eslint/parser": "^5.38.0",
|
24 |
"autoprefixer": "^10.4.12",
|
25 |
+
"d3-scale": "^4.0.2",
|
26 |
"eslint": "^8.24.0",
|
27 |
"eslint-config-prettier": "^8.3.0",
|
28 |
"eslint-plugin-svelte3": "^4.0.0",
|
frontend/src/lib/App.svelte
CHANGED
@@ -3,16 +3,103 @@
|
|
3 |
import Frame from '$lib/Frame.svelte';
|
4 |
import Canvas from '$lib/Canvas.svelte';
|
5 |
import Menu from '$lib/Menu.svelte';
|
|
|
6 |
import type { Room } from '@liveblocks/client';
|
7 |
import { COLORS, EMOJIS } from '$lib/constants';
|
8 |
-
import {
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
/**
|
11 |
* The main Liveblocks code for the example.
|
12 |
* Check in src/routes/index.svelte to see the setup code.
|
13 |
*/
|
14 |
|
15 |
export let room: Room;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
</script>
|
17 |
|
18 |
<!-- Show the current user's cursor location -->
|
@@ -20,11 +107,19 @@
|
|
20 |
{$myPresence?.cursor
|
21 |
? `${$myPresence.cursor.x} × ${$myPresence.cursor.y}`
|
22 |
: 'Move your cursor to broadcast its position to other people in the room.'}
|
|
|
|
|
23 |
</div>
|
|
|
|
|
|
|
24 |
<div class="fixed left-0 z-0 w-screen h-screen cursor-none">
|
25 |
<Canvas />
|
26 |
|
27 |
<main class="z-10 relative">
|
|
|
|
|
|
|
28 |
{#if $myPresence?.cursor}
|
29 |
<Frame color={COLORS[0]} position={$myPresence.cursor} transform={$currZoomTransform} />
|
30 |
<Cursor
|
@@ -56,7 +151,7 @@
|
|
56 |
{/if}
|
57 |
</main>
|
58 |
</div>
|
59 |
-
<div class="fixed bottom-0 left-0 right-0 z-
|
60 |
<Menu />
|
61 |
</div>
|
62 |
|
|
|
3 |
import Frame from '$lib/Frame.svelte';
|
4 |
import Canvas from '$lib/Canvas.svelte';
|
5 |
import Menu from '$lib/Menu.svelte';
|
6 |
+
import PromptModal from '$lib/PromptModal.svelte';
|
7 |
import type { Room } from '@liveblocks/client';
|
8 |
import { COLORS, EMOJIS } from '$lib/constants';
|
9 |
+
import { PUBLIC_WS_ENDPOINT } from '$env/static/public';
|
10 |
+
import {
|
11 |
+
isLoading,
|
12 |
+
loadingState,
|
13 |
+
currZoomTransform,
|
14 |
+
myPresence,
|
15 |
+
others,
|
16 |
+
isPrompting,
|
17 |
+
clickedPosition
|
18 |
+
} from '$lib/store';
|
19 |
+
import { base64ToBlob, uploadImage } from '$lib/utils';
|
20 |
/**
|
21 |
* The main Liveblocks code for the example.
|
22 |
* Check in src/routes/index.svelte to see the setup code.
|
23 |
*/
|
24 |
|
25 |
export let room: Room;
|
26 |
+
|
27 |
+
async function onClose(e: CustomEvent) {
|
28 |
+
$isPrompting = false;
|
29 |
+
}
|
30 |
+
async function onPrompt(e: CustomEvent) {
|
31 |
+
const prompt = e.detail.prompt;
|
32 |
+
const imgURLs = await generateImage(prompt);
|
33 |
+
$isPrompting = false;
|
34 |
+
console.log('prompt', prompt, imgURLs);
|
35 |
+
}
|
36 |
+
async function generateImage(_prompt: string) {
|
37 |
+
if (!_prompt || $isLoading == true) return;
|
38 |
+
$loadingState = 'Pending';
|
39 |
+
$isLoading = true;
|
40 |
+
const sessionHash = crypto.randomUUID();
|
41 |
+
|
42 |
+
const payload = {
|
43 |
+
fn_index: 2,
|
44 |
+
data: [_prompt],
|
45 |
+
session_hash: sessionHash
|
46 |
+
};
|
47 |
+
const websocket = new WebSocket(PUBLIC_WS_ENDPOINT);
|
48 |
+
// websocket.onopen = async function (event) {
|
49 |
+
// websocket.send(JSON.stringify({ hash: sessionHash }));
|
50 |
+
// };
|
51 |
+
websocket.onclose = (evt) => {
|
52 |
+
if (!evt.wasClean) {
|
53 |
+
$loadingState = 'Error';
|
54 |
+
$isLoading = false;
|
55 |
+
}
|
56 |
+
};
|
57 |
+
websocket.onmessage = async function (event) {
|
58 |
+
try {
|
59 |
+
const data = JSON.parse(event.data);
|
60 |
+
$loadingState = '';
|
61 |
+
switch (data.msg) {
|
62 |
+
case 'send_data':
|
63 |
+
$loadingState = 'Sending Data';
|
64 |
+
websocket.send(JSON.stringify(payload));
|
65 |
+
break;
|
66 |
+
case 'queue_full':
|
67 |
+
$loadingState = 'Queue full';
|
68 |
+
websocket.close();
|
69 |
+
$isLoading = false;
|
70 |
+
return;
|
71 |
+
case 'estimation':
|
72 |
+
const { msg, rank, queue_size } = data;
|
73 |
+
$loadingState = `On queue ${rank}/${queue_size}`;
|
74 |
+
break;
|
75 |
+
case 'process_generating':
|
76 |
+
$loadingState = data.success ? 'Generating' : 'Error';
|
77 |
+
break;
|
78 |
+
case 'process_completed':
|
79 |
+
try {
|
80 |
+
const imgsBase64 = data.output.data[0] as string[];
|
81 |
+
const imgBlobs = await Promise.all(imgsBase64.map((base64) => base64ToBlob(base64)));
|
82 |
+
const imgURLs = await Promise.all(imgBlobs.map((blob) => uploadImage(blob, _prompt)));
|
83 |
+
console.log(imgURLs);
|
84 |
+
$loadingState = data.success ? 'Complete' : 'Error';
|
85 |
+
} catch (e) {
|
86 |
+
$loadingState = e.message;
|
87 |
+
}
|
88 |
+
websocket.close();
|
89 |
+
$isLoading = false;
|
90 |
+
return;
|
91 |
+
case 'process_starts':
|
92 |
+
$loadingState = 'Processing';
|
93 |
+
break;
|
94 |
+
}
|
95 |
+
} catch (e) {
|
96 |
+
console.error(e);
|
97 |
+
$isLoading = false;
|
98 |
+
$loadingState = 'Error';
|
99 |
+
}
|
100 |
+
};
|
101 |
+
}
|
102 |
+
let modal = false;
|
103 |
</script>
|
104 |
|
105 |
<!-- Show the current user's cursor location -->
|
|
|
107 |
{$myPresence?.cursor
|
108 |
? `${$myPresence.cursor.x} × ${$myPresence.cursor.y}`
|
109 |
: 'Move your cursor to broadcast its position to other people in the room.'}
|
110 |
+
{$loadingState}
|
111 |
+
{$isLoading}
|
112 |
</div>
|
113 |
+
{#if $isPrompting}
|
114 |
+
<PromptModal on:prompt={onPrompt} on:close={onClose} />
|
115 |
+
{/if}
|
116 |
<div class="fixed left-0 z-0 w-screen h-screen cursor-none">
|
117 |
<Canvas />
|
118 |
|
119 |
<main class="z-10 relative">
|
120 |
+
{#if $clickedPosition}
|
121 |
+
<Frame color={COLORS[0]} position={$clickedPosition} transform={$currZoomTransform} />
|
122 |
+
{/if}
|
123 |
{#if $myPresence?.cursor}
|
124 |
<Frame color={COLORS[0]} position={$myPresence.cursor} transform={$currZoomTransform} />
|
125 |
<Cursor
|
|
|
151 |
{/if}
|
152 |
</main>
|
153 |
</div>
|
154 |
+
<div class="fixed bottom-0 left-0 right-0 z-10 my-2">
|
155 |
<Menu />
|
156 |
</div>
|
157 |
|
frontend/src/lib/Canvas.svelte
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
import { select } from 'd3-selection';
|
4 |
import { scaleLinear } from 'd3-scale';
|
5 |
import { onMount } from 'svelte';
|
6 |
-
import { currZoomTransform, myPresence,
|
7 |
|
8 |
const height = 512 * 5;
|
9 |
const width = 512 * 5;
|
@@ -26,7 +26,7 @@
|
|
26 |
|
27 |
const scale = width / containerEl.clientWidth;
|
28 |
const zoomHandler = zoom()
|
29 |
-
.scaleExtent([1/scale,1])
|
30 |
// .translateExtent([
|
31 |
// [0, 0],
|
32 |
// [width, height]
|
@@ -39,7 +39,13 @@
|
|
39 |
.call(zoomHandler as any)
|
40 |
// .call(zoomHandler.scaleTo as any, 1 / scale)
|
41 |
.on('pointermove', handlePointerMove)
|
42 |
-
.on('pointerleave', handlePointerLeave)
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
canvasCtx = canvasEl.getContext('2d') as CanvasRenderingContext2D;
|
45 |
canvasCtx.fillStyle = 'red';
|
|
|
3 |
import { select } from 'd3-selection';
|
4 |
import { scaleLinear } from 'd3-scale';
|
5 |
import { onMount } from 'svelte';
|
6 |
+
import { currZoomTransform, myPresence, isPrompting, clickedPosition } from '$lib/store';
|
7 |
|
8 |
const height = 512 * 5;
|
9 |
const width = 512 * 5;
|
|
|
26 |
|
27 |
const scale = width / containerEl.clientWidth;
|
28 |
const zoomHandler = zoom()
|
29 |
+
.scaleExtent([1 / scale, 1])
|
30 |
// .translateExtent([
|
31 |
// [0, 0],
|
32 |
// [width, height]
|
|
|
39 |
.call(zoomHandler as any)
|
40 |
// .call(zoomHandler.scaleTo as any, 1 / scale)
|
41 |
.on('pointermove', handlePointerMove)
|
42 |
+
.on('pointerleave', handlePointerLeave)
|
43 |
+
.on('dblclick.zoom', null)
|
44 |
+
.on('click', () => {
|
45 |
+
$isPrompting = true;
|
46 |
+
$clickedPosition = $myPresence.cursor;
|
47 |
+
console.log($clickedPosition);
|
48 |
+
});
|
49 |
|
50 |
canvasCtx = canvasEl.getContext('2d') as CanvasRenderingContext2D;
|
51 |
canvasCtx.fillStyle = 'red';
|
frontend/src/lib/PromptModal.svelte
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { createEventDispatcher, onMount } from 'svelte';
|
3 |
+
const dispatch = createEventDispatcher();
|
4 |
+
let prompt: string;
|
5 |
+
|
6 |
+
const onKeyup = (e: KeyboardEvent) => {
|
7 |
+
if (e.key === 'Escape') {
|
8 |
+
dispatch('close');
|
9 |
+
}
|
10 |
+
};
|
11 |
+
onMount(() => {
|
12 |
+
window.addEventListener('keyup', onKeyup);
|
13 |
+
return () => {
|
14 |
+
window.removeEventListener('keyup', onKeyup);
|
15 |
+
};
|
16 |
+
});
|
17 |
+
</script>
|
18 |
+
|
19 |
+
<form
|
20 |
+
class="fixed w-screen h-screen top-0 left-0 z-50 flex items-center justify-center bg-black bg-opacity-80 px-3"
|
21 |
+
on:submit|preventDefault={() => dispatch('prompt', { prompt })}
|
22 |
+
on:click={() => dispatch('close')}
|
23 |
+
>
|
24 |
+
<input
|
25 |
+
on:click|stopPropagation
|
26 |
+
class="input"
|
27 |
+
placeholder="Type a prompt..."
|
28 |
+
title="Input prompt to generate image and obtain palette"
|
29 |
+
type="text"
|
30 |
+
name="prompt"
|
31 |
+
bind:value={prompt}
|
32 |
+
/>
|
33 |
+
</form>
|
34 |
+
|
35 |
+
<style lang="postcss" scoped>
|
36 |
+
.link {
|
37 |
+
@apply text-xs underline font-bold hover:no-underline hover:text-gray-500 visited:text-gray-500;
|
38 |
+
}
|
39 |
+
.input {
|
40 |
+
@apply w-full max-w-sm text-sm disabled:opacity-50 italic placeholder:text-white text-white placeholder:text-opacity-50 bg-slate-900 border-2 border-white rounded-2xl px-2 shadow-sm focus:outline-none focus:border-gray-400 focus:ring-1;
|
41 |
+
}
|
42 |
+
</style>
|
frontend/src/lib/store.ts
CHANGED
@@ -5,6 +5,8 @@ import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
|
5 |
|
6 |
export const loadingState = writable<string>('');
|
7 |
export const isLoading = writable<boolean>(false);
|
|
|
|
|
8 |
|
9 |
export const currZoomTransform = writable<ZoomTransform>(zoomIdentity);
|
10 |
|
|
|
5 |
|
6 |
export const loadingState = writable<string>('');
|
7 |
export const isLoading = writable<boolean>(false);
|
8 |
+
export const isPrompting = writable<boolean>(false);
|
9 |
+
export const clickedPosition = writable<{ x: number; y: number }>();
|
10 |
|
11 |
export const currZoomTransform = writable<ZoomTransform>(zoomIdentity);
|
12 |
|
frontend/src/lib/utils.ts
CHANGED
@@ -3,6 +3,27 @@ import { dev } from '$app/environment';
|
|
3 |
export function randomSeed() {
|
4 |
return BigInt(13248873089935215612 & (((1 << 63) - 1) * Math.random()));
|
5 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
export async function uploadImage(imagBlob: Blob, prompt: string): string {
|
7 |
// simple regex slugify string for file name
|
8 |
const promptSlug = slugify(prompt);
|
|
|
3 |
export function randomSeed() {
|
4 |
return BigInt(13248873089935215612 & (((1 << 63) - 1) * Math.random()));
|
5 |
}
|
6 |
+
|
7 |
+
export function base64ToBlob(base64image: string): Promise<Blob> {
|
8 |
+
return new Promise((resolve) => {
|
9 |
+
const img = new Image();
|
10 |
+
img.onload = async () => {
|
11 |
+
const w = img.width;
|
12 |
+
const h = img.height;
|
13 |
+
const canvas = document.createElement('canvas');
|
14 |
+
canvas.width = w;
|
15 |
+
canvas.height = h;
|
16 |
+
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
17 |
+
ctx.drawImage(img, 0, 0, w, h);
|
18 |
+
|
19 |
+
const imgBlob: Blob = await new Promise((_resolve) =>
|
20 |
+
canvas.toBlob(_resolve, 'image/jpeg', 0.95)
|
21 |
+
);
|
22 |
+
resolve(imgBlob);
|
23 |
+
};
|
24 |
+
img.src = base64image;
|
25 |
+
});
|
26 |
+
}
|
27 |
export async function uploadImage(imagBlob: Blob, prompt: string): string {
|
28 |
// simple regex slugify string for file name
|
29 |
const promptSlug = slugify(prompt);
|
frontend/src/routes/+page.svelte
CHANGED
@@ -46,40 +46,12 @@
|
|
46 |
</script>
|
47 |
|
48 |
<div class="max-w-screen-md mx-auto px-3 py-8 relative">
|
49 |
-
<div class="relative
|
50 |
<h1 class="text-3xl font-bold leading-normal">Stable Diffussion Outpainting Multiplayer</h1>
|
51 |
-
<p class="text-sm" />
|
52 |
-
<div class="relative bg-white dark:bg-black py-3">
|
53 |
-
<form class="grid grid-cols-6">
|
54 |
-
<input
|
55 |
-
class="input"
|
56 |
-
placeholder="A photo of a beautiful sunset in San Francisco"
|
57 |
-
title="Input prompt to generate image and obtain palette"
|
58 |
-
type="text"
|
59 |
-
name="prompt"
|
60 |
-
disabled={$isLoading}
|
61 |
-
/>
|
62 |
-
<button class="button" disabled={$isLoading} title="Generate Palette">
|
63 |
-
Create Palette
|
64 |
-
</button>
|
65 |
-
</form>
|
66 |
-
</div>
|
67 |
</div>
|
68 |
-
<div class="relative
|
69 |
{#if room}
|
70 |
<App {room} />
|
71 |
{/if}
|
72 |
</div>
|
73 |
</div>
|
74 |
-
|
75 |
-
<style lang="postcss" scoped>
|
76 |
-
.link {
|
77 |
-
@apply text-xs underline font-bold hover:no-underline hover:text-gray-500 visited:text-gray-500;
|
78 |
-
}
|
79 |
-
.input {
|
80 |
-
@apply text-sm disabled:opacity-50 col-span-4 md:col-span-5 italic dark:placeholder:text-black placeholder:text-white text-white dark:text-black placeholder:text-opacity-30 dark:placeholder:text-opacity-10 dark:bg-white bg-slate-900 border-2 border-black rounded-2xl px-2 shadow-sm focus:outline-none focus:border-gray-400 focus:ring-1;
|
81 |
-
}
|
82 |
-
.button {
|
83 |
-
@apply disabled:opacity-50 col-span-2 md:col-span-1 dark:bg-white dark:text-black border-2 border-black rounded-2xl ml-2 px-2 py-2 text-xs shadow-sm font-bold focus:outline-none focus:border-gray-400;
|
84 |
-
}
|
85 |
-
</style>
|
|
|
46 |
</script>
|
47 |
|
48 |
<div class="max-w-screen-md mx-auto px-3 py-8 relative">
|
49 |
+
<div class="relative">
|
50 |
<h1 class="text-3xl font-bold leading-normal">Stable Diffussion Outpainting Multiplayer</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
</div>
|
52 |
+
<div class="relative">
|
53 |
{#if room}
|
54 |
<App {room} />
|
55 |
{/if}
|
56 |
</div>
|
57 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|