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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|