File size: 8,840 Bytes
0e2b5bb
 
 
 
 
1d06c9e
0e2b5bb
f9554df
 
bb145f1
f9554df
0e2b5bb
719dfbd
0e2b5bb
294c4d8
d92c610
 
 
 
 
0e2b5bb
d92c610
 
eb7ea4d
38dbd7d
f9554df
d92c610
 
 
2fe2cfd
294c4d8
f9554df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294c4d8
f9554df
 
 
 
294c4d8
 
 
eb7ea4d
294c4d8
 
 
 
 
 
 
 
 
 
 
 
0e2b5bb
294c4d8
 
 
 
 
0e2b5bb
294c4d8
0e2b5bb
 
 
3060840
90f987a
 
3060840
1d06c9e
90f987a
0e2b5bb
3060840
 
1d06c9e
3060840
 
0e2b5bb
90f987a
 
 
0e2b5bb
 
 
 
 
 
 
 
 
 
 
3060840
 
 
0e2b5bb
 
3060840
0e2b5bb
 
 
 
 
 
 
 
88e9166
0e2b5bb
 
 
 
 
85c974d
 
 
 
 
 
 
 
 
 
0e2b5bb
 
 
 
294c4d8
0e2b5bb
 
 
836ca1d
0e2b5bb
 
 
 
 
 
13fd302
 
0e2b5bb
90f987a
0e2b5bb
 
85c974d
90f987a
85c974d
90f987a
 
 
 
 
 
 
 
85c974d
90f987a
 
 
 
 
0e2b5bb
 
 
daf00a0
 
bb145f1
8319790
daf00a0
2e2c646
 
 
 
 
 
0e2b5bb
 
88e9166
4bbc9a0
836ca1d
 
 
 
 
 
 
 
 
 
 
 
 
d92c610
88e9166
0e2b5bb
719dfbd
836ca1d
0e2b5bb
836ca1d
0e2b5bb
 
 
 
 
 
836ca1d
0e2b5bb
 
836ca1d
0e2b5bb
 
 
 
 
294c4d8
 
 
 
 
 
 
 
 
 
 
 
 
 
0e2b5bb
 
 
836ca1d
2fe2cfd
 
 
836ca1d
 
 
f9554df
836ca1d
f9554df
daf00a0
88e9166
0e2b5bb
 
f9554df
 
 
 
 
 
 
 
 
2e2c646
f9554df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e2c646
f9554df
 
 
 
 
 
 
 
0e2b5bb
 
836ca1d
 
 
 
 
 
1cfaf7e
836ca1d
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
<script lang="ts">
	import { onMount } from 'svelte';
	import type { ColorsPrompt, ColorsImage } from '$lib/types';
	import { randomSeed, extractPalette, uploadImage } from '$lib/utils';
	import { isLoading, loadingState } from '$lib/store';
	import { PUBLIC_WS_ENDPOINT, PUBLIC_API } from '$env/static/public';
	import Pallette from '$lib/Palette.svelte';
	import ArrowRight from '$lib/ArrowRight.svelte';
	import ArrowLeft from '$lib/ArrowLeft.svelte';

	let promptsData: ColorsPrompt[] = [];
	let prompt: string;
	let promptInputEl: HTMLElement;

	onMount(() => {
		fetchData();
		const interval = window.setInterval(fetchData, 5000);
		return () => {
			clearInterval(interval);
		};
	});

	async function fetchData() {
		const palettes = await fetch(PUBLIC_API + '/data').then((d) => d.json());
		if (!promptsData || palettes?.length > promptsData?.length) {
			promptsData = sortData(palettes);
		}
	}

	$: promptsTotal = promptsData?.length || null;

	let page: number = 0;
	const maxPerPage: number = 10;
	$: totalPages = Math.ceil(promptsData?.length / maxPerPage) || 0;
	$: promptsDataPage = [...promptsData].slice(page * maxPerPage, (page + 1) * maxPerPage);
	let pagesLinks: number[] = [];
	$: if (totalPages) {
		const pagesNums = Array(totalPages)
			.fill([])
			.map((_, i) => ({ value: i, label: i + 1 }));
		pagesLinks = pagesNums
			.slice(0, 3)
			.concat([{ value: -1, label: '...' }])
			.concat(pagesNums.length > 3 ? pagesNums.slice(-1) : []);
		console.log(pagesLinks);
	}

	function sortData(_promptData: ColorsPrompt[]) {
		return _promptData
			.sort((a, b) => b.id - a.id)
			.map((p) => p.data)
			.filter((d) => d.images.length > 0);
	}
	async function savePaletteDB(colorPrompt: ColorsPrompt) {
		try {
			const newPalettes: ColorsPrompt[] = await fetch(PUBLIC_API + '/new_palette', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify({
					prompt: colorPrompt.prompt,
					images: colorPrompt.images.map((i) => ({
						imgURL: i.imgURL,
						colors: i.colors.map((c) => c.formatHex())
					}))
				})
			}).then((d) => d.json());

			promptsData = sortData(newPalettes);
		} catch (e) {
			console.error(e);
		}
	}

	async function generatePalette(_prompt: string) {
		if (!_prompt || $isLoading == true) return;
		$loadingState = 'Pending';
		$isLoading = true;

		const sessionHash = crypto.randomUUID();

		const hashpayload = {
			fn_index: 3,
			session_hash: sessionHash
		};

		const datapayload = {
			data: [_prompt, '', 9]
		};

		const websocket = new WebSocket(PUBLIC_WS_ENDPOINT);
		// websocket.onopen = async function (event) {
		// 	websocket.send(JSON.stringify({ hash: sessionHash }));
		// };
		websocket.onclose = (evt) => {
			if (!evt.wasClean) {
				$loadingState = 'Error';
				$isLoading = false;
			}
		};
		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();
						$isLoading = false;
						return;
					case 'estimation':
						const { msg, 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 images = await extractColorsImages(data.output.data[0], _prompt);
							savePaletteDB({
								prompt: _prompt,
								images
							});
							$loadingState = data.success ? 'Complete' : 'Error';
						} catch (e) {
							$loadingState = e.message;
						}
						websocket.close();
						$isLoading = false;
						return;
					case 'process_starts':
						$loadingState = 'Processing';
						break;
				}
			} catch (e) {
				console.error(e);
				$isLoading = false;
				$loadingState = 'Error';
			}
		};
	}
	async function extractColorsImages(images: string[], _prompt: string): Promise<ColorsImage[]> {
		const nsfwColors = ['#040404', '#B7B7B7', '#565656', '#747474', '#6C6C6C'];

		const colorImages = [];
		let isNSFW = false;
		for (const base64img of images) {
			const { colors, imgBlob } = await extractPalette(base64img);
			if (
				!colors.map((color) => color.formatHex().toUpperCase()).every((c) => nsfwColors.includes(c))
			) {
				const url = await uploadImage(imgBlob, _prompt);
				const colorsImage: ColorsImage = {
					colors,
					imgURL: url
				};
				colorImages.push(colorsImage);
			} else {
				isNSFW = true;
			}
		}

		if (colorImages.length === 0 && isNSFW) {
			console.error('Possible NSFW image');
			throw new Error('Possible NSFW image');
		}
		return colorImages;
	}
	function remix(e: CustomEvent) {
		prompt = e.detail.prompt;
		promptInputEl.scrollIntoView({ behavior: 'smooth' });
		scrollTop();
	}
	function scrollTop() {
		window.scrollTo(0, 0);
		if ('parentIFrame' in window) {
			window.parentIFrame.scrollTo(0, promptInputEl.offsetTop);
		}
	}
</script>

<div class="max-w-screen-md mx-auto px-3 py-8 relative z-0">
	<h1 class="text-3xl font-bold leading-normal">Palette generation with Stable Diffusion</h1>
	<p class="text-sm">
		Original ideas:

		<a
			class="link"
			target="_blank"
			rel="nofollow noopener"
			href="https://twitter.com/mattdesl/status/1569457653298139136"
		>
			Matt DesLauriers
		</a>,
		<a class="link" href="https://drib.net/homage"> dribnet </a>
	</p>
	<div class="relative top-0 z-50 bg-white dark:bg-black py-3">
		<form class="grid grid-cols-6" on:submit|preventDefault={() => generatePalette(prompt)}>
			<input
				bind:this={promptInputEl}
				class="input"
				placeholder="A photo of a beautiful sunset in San Francisco"
				title="Input prompt to generate image and obtain palette"
				type="text"
				name="prompt"
				bind:value={prompt}
				disabled={$isLoading}
			/>
			<button
				class="button"
				on:click|preventDefault={() => generatePalette(prompt)}
				disabled={$isLoading}
				title="Generate Palette"
			>
				Create Palette
			</button>
		</form>
		{#if $loadingState}
			<h3 class="text-xs font-bold ml-3 inline-block">{$loadingState}</h3>
			{#if $isLoading}
				<svg
					xmlns="http://www.w3.org/2000/svg"
					fill="none"
					viewBox="0 0 24 24"
					class="animate-spin max-w-[1rem] inline-block"
				>
					<path
						fill="currentColor"
						d="M20 12a8 8 0 0 1-8 8v4a12 12 0 0 0 12-12h-4Zm-2-5.3a8 8 0 0 1 2 5.3h4c0-3-1.1-5.8-3-8l-3 2.7Z"
					/>
				</svg>
			{/if}
		{/if}
	</div>

	<div class="flex items-center gap-4 my-10">
		<div class="font-bold text-sm">
			{promptsTotal ? `${promptsTotal} submitted palettes` : 'Loading...'}
		</div>
		<div class="grow border-b border-gray-200" />
	</div>

	{#if promptsDataPage}
		<div>
			{#each promptsDataPage as promptData}
				<Pallette {promptData} on:remix={remix} />
				<div class="border-b border-gray-200 py-2" />
			{/each}
		</div>
		<nav role="navigation">
			<ul
				class="items-center sm:justify-center space-x-2  select-none w-full flex justify-center mt-6 mb-4"
			>
				<li />
				<li>
					<a
						on:click|preventDefault={() => {
							page = page - 1 < 0 ? 0 : page - 1;
							scrollTop();
						}}
						class="px-2.5 py-1 hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center rounded-lg"
						href="#"
						><ArrowLeft /> Previous
					</a>
				</li>
				<li class="text-sm">
					<span class="inline-block min-w-[3ch] text-right">{page + 1} </span>/<span
						class="inline-block min-w-[3ch]"
						>{totalPages}
					</span>
				</li>
				<li>
					<a
						on:click|preventDefault={() => {
							page = page + 1 >= totalPages - 1 ? totalPages - 1 : page + 1;
							scrollTop();
						}}
						class="px-2.5 py-1 hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center rounded-lg"
						href="#"
						>Next <ArrowRight />
					</a>
				</li>
			</ul>
		</nav>
	{/if}
</div>

<style lang="postcss" scoped>
	.link {
		@apply text-xs underline font-bold hover:no-underline hover:text-gray-500 visited:text-gray-500;
	}
	.input {
		@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;
	}
	.button {
		@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;
	}
</style>