ejfox's picture
Duplicate from huggingface-projects/stable-diffusion-multiplayer
bb88c4d
<script lang="ts">
import Cursor from '$lib/Cursor.svelte';
import Frame from '$lib/Frame.svelte';
import PaintFrame from '$lib/PaintFrame.svelte';
import PaintCanvas from '$lib/PaintCanvas.svelte';
import Menu from '$lib/Menu.svelte';
import PromptModal from '$lib/PromptModal.svelte';
import { COLORS, FRAME_SIZE } from '$lib/constants';
import { PUBLIC_WS_INPAINTING } from '$env/static/public';
import type { PromptImgKey } from '$lib/types';
import { Status } from '$lib/types';
import { LiveObject } from '@liveblocks/client';
import {
loadingState,
currZoomTransform,
maskEl,
selectedRoomID,
isRenderingCanvas
} from '$lib/store';
import { useMyPresence, useObject, useOthers } from '$lib/liveblocks';
const myPresence = useMyPresence();
const others = useOthers();
let showModal = false;
function getKey(position: { x: number; y: number }): PromptImgKey {
return `${position.x}_${position.y}`;
}
const promptImgStorage = useObject('promptImgStorage');
$: isLoading = $myPresence?.status === Status.loading || $isRenderingCanvas || false;
function onShowModal(e: CustomEvent) {
if (isLoading) return;
showModal = e.detail.showModal;
if (showModal) {
myPresence.update({
status: Status.prompting
});
} else {
myPresence.update({
status: Status.ready
});
}
}
function onPaint() {
showModal = false;
generateImage();
}
function canPaint(position: { x: number; y: number }): boolean {
if (!$others) return true;
let canPaint = true;
for (const { presence } of $others) {
if (
position.x < presence.frame.x + FRAME_SIZE &&
position.x + FRAME_SIZE > presence.frame.x &&
position.y < presence.frame.y + FRAME_SIZE &&
position.y + FRAME_SIZE > presence.frame.y
) {
// can paint if presence is only dragging
if (presence.status === Status.ready || presence.status === Status.dragging) {
canPaint = true;
continue;
}
canPaint = false;
break;
}
}
return canPaint;
}
function clearStateMsg(t = 5000) {
setTimeout(() => {
$loadingState = '';
}, t);
}
async function generateImage() {
if (isLoading) return;
const prompt = $myPresence.currentPrompt;
const position = $myPresence.frame;
$loadingState = 'Pending';
if (!canPaint(position)) {
$loadingState = 'Someone is already painting here';
myPresence.update({
status: Status.ready
});
clearStateMsg();
return;
}
const imageKey = getKey(position);
const room = $selectedRoomID || 'default';
console.log('Generating...', prompt, position);
myPresence.update({
status: Status.loading
});
const sessionHash = crypto.randomUUID();
const base64Crop = $maskEl.toDataURL('image/webp');
const hashpayload = {
fn_index: 0,
session_hash: sessionHash
};
const datapayload = {
data: [base64Crop, prompt, 0.75, 7.5, 40, 'patchmatch', room, imageKey]
};
const websocket = new WebSocket(PUBLIC_WS_INPAINTING);
// websocket.onopen = async function (event) {
// websocket.send(JSON.stringify({ hash: sessionHash }));
// };
websocket.onclose = (evt) => {
if (!evt.wasClean) {
$loadingState = 'Error';
myPresence.update({
status: Status.ready
});
}
};
websocket.onmessage = async function (event) {
try {
const data = JSON.parse(event.data);
$loadingState = '';
switch (data.msg) {
case 'send_hash':
websocket.send(JSON.stringify(hashpayload));
break;
case 'send_data':
$loadingState = 'Sending Data';
websocket.send(JSON.stringify({ ...hashpayload, ...datapayload }));
break;
case 'queue_full':
$loadingState = 'Queue full';
websocket.close();
myPresence.update({
status: Status.ready
});
return;
case 'estimation':
const { rank, queue_size } = data;
$loadingState = `On queue ${rank}/${queue_size}`;
break;
case 'process_generating':
$loadingState = data.success ? 'Generating' : 'Error';
break;
case 'process_completed':
try {
const params = data.output.data[0] as {
is_nsfw: boolean;
image: {
url: string;
filename: string;
};
};
const isNSWF = params.is_nsfw;
if (isNSWF) {
throw new Error('NFSW');
}
// const imgBlob = await base64ToBlob(imgBase64);
const promptImgParams = {
imgURL: params.image.filename
};
// const imgURL = await uploadImage(imgBlob, promptImgParams);
$promptImgStorage.set(imageKey, new LiveObject(promptImgParams));
// $promptImgStorage.set(imageKey, promptImgParams);
console.log(params.image.url);
$loadingState = data.success ? 'Complete' : 'Error';
clearStateMsg();
myPresence.update({
status: Status.ready,
currentPrompt: ''
});
} catch (err) {
const tError = err as Error;
$loadingState = tError?.message;
myPresence.update({
status: Status.ready
});
clearStateMsg(10000);
}
websocket.close();
return;
case 'process_starts':
$loadingState = 'Processing';
break;
}
} catch (e) {
console.error(e);
$loadingState = 'Error';
}
};
}
</script>
<!-- Show the current user's cursor location -->
<div class="text touch-none pointer-events-none">
{$loadingState}
</div>
{#if showModal}
<PromptModal
on:paint={onPaint}
initPrompt={$myPresence?.currentPrompt}
on:showModal={onShowModal}
/>
{/if}
<div class="fixed top-0 left-0 z-0 w-screen h-screen min-h-[600px]">
<PaintCanvas />
<main class="z-10 relative">
<!-- When others connected, iterate through others and show their cursors -->
{#if $others}
{#each [...$others] as { connectionId, presence } (connectionId)}
{#if (presence?.status === Status.loading || presence?.status === Status.prompting || presence?.status === Status.masking) && presence?.frame}
<Frame
status={presence.status}
position={presence?.frame}
prompt={presence?.currentPrompt}
transform={$currZoomTransform}
/>
{/if}
{#if presence?.cursor}
<Cursor
color={COLORS[1 + (connectionId % (COLORS.length - 1))]}
position={presence?.cursor}
transform={$currZoomTransform}
/>
{/if}
{/each}
{/if}
<PaintFrame transform={$currZoomTransform} on:showModal={onShowModal} />
</main>
</div>
<div class="fixed bottom-0 md:bottom-16 left-0 right-0 z-10 my-2">
<Menu {isLoading} on:showModal={onShowModal} />
</div>
<style lang="postcss" scoped>
</style>