|
<script lang="ts"> |
|
import type { GradioClient, FluxGenerationParams, FluxGenerationResult } from '$lib/types'; |
|
|
|
interface Props { |
|
client: GradioClient | null; |
|
onImageGenerated?: ((result: FluxGenerationResult, imageBlob: Blob) => void) | null; |
|
} |
|
|
|
let { client = null, onImageGenerated = null }: Props = $props(); |
|
|
|
let params: FluxGenerationParams = $state({ |
|
prompt: "", |
|
seed: 0, |
|
randomizeSeed: true, |
|
width: 1024, |
|
height: 1024, |
|
steps: 4 |
|
}); |
|
|
|
let isGenerating = $state(false); |
|
let result: FluxGenerationResult | null = $state(null); |
|
let error: string | null = $state(null); |
|
|
|
async function handleSubmit(e: Event) { |
|
e.preventDefault(); |
|
if (!client || !params.prompt.trim()) return; |
|
|
|
isGenerating = true; |
|
error = null; |
|
|
|
try { |
|
const output = await client.predict("/infer", [ |
|
params.prompt, |
|
params.seed, |
|
params.randomizeSeed, |
|
params.width, |
|
params.height, |
|
params.steps |
|
]); |
|
|
|
const [image, usedSeed] = output.data; |
|
let url: string | undefined; |
|
|
|
if (typeof image === "string") url = image; |
|
else if (image && image.url) url = image.url; |
|
else if (image && image.path) url = image.path; |
|
|
|
if (url) { |
|
result = { |
|
imageUrl: url, |
|
seed: usedSeed, |
|
prompt: params.prompt |
|
}; |
|
|
|
|
|
if (onImageGenerated) { |
|
try { |
|
const response = await fetch(url); |
|
const blob = await response.blob(); |
|
onImageGenerated(result, blob); |
|
} catch (err) { |
|
console.warn("Could not fetch generated image for captioning:", err); |
|
} |
|
} |
|
} else { |
|
error = "Unexpected image format returned from API"; |
|
} |
|
} catch (err) { |
|
console.error(err); |
|
error = `Generation failed: ${err}`; |
|
} finally { |
|
isGenerating = false; |
|
} |
|
} |
|
</script> |
|
|
|
<form class="generator-form" onsubmit={handleSubmit}> |
|
<h3>Generate Image with FLUX-1 Schnell</h3> |
|
|
|
<label for="prompt">Prompt</label> |
|
<input |
|
type="text" |
|
id="prompt" |
|
bind:value={params.prompt} |
|
placeholder="Enter your prompt" |
|
required |
|
disabled={isGenerating} |
|
/> |
|
|
|
<label for="seed">Seed</label> |
|
<input |
|
type="number" |
|
id="seed" |
|
bind:value={params.seed} |
|
min="0" |
|
max="2147483647" |
|
disabled={isGenerating} |
|
/> |
|
|
|
<label> |
|
<input |
|
type="checkbox" |
|
bind:checked={params.randomizeSeed} |
|
disabled={isGenerating} |
|
/> |
|
Randomize seed |
|
</label> |
|
|
|
<div class="input-row"> |
|
<div class="input-group"> |
|
<label for="width">Width</label> |
|
<input |
|
type="number" |
|
id="width" |
|
bind:value={params.width} |
|
min="256" |
|
max="2048" |
|
step="32" |
|
disabled={isGenerating} |
|
/> |
|
</div> |
|
<div class="input-group"> |
|
<label for="height">Height</label> |
|
<input |
|
type="number" |
|
id="height" |
|
bind:value={params.height} |
|
min="256" |
|
max="2048" |
|
step="32" |
|
disabled={isGenerating} |
|
/> |
|
</div> |
|
</div> |
|
|
|
<label for="steps">Number of inference steps</label> |
|
<input |
|
type="number" |
|
id="steps" |
|
bind:value={params.steps} |
|
min="1" |
|
max="50" |
|
disabled={isGenerating} |
|
/> |
|
|
|
<button |
|
type="submit" |
|
class="generate-button" |
|
disabled={isGenerating || !client} |
|
> |
|
{isGenerating ? 'Generating…' : 'Generate Image'} |
|
</button> |
|
</form> |
|
|
|
{#if error} |
|
<div class="error-message">{error}</div> |
|
{/if} |
|
|
|
{#if isGenerating} |
|
<div class="status-message">Generating image…</div> |
|
{/if} |
|
|
|
{#if result} |
|
<div class="result"> |
|
<img src={result.imageUrl} alt="Generated" /> |
|
<p><strong>Seed</strong>: {result.seed}</p> |
|
<p><strong>Prompt</strong>: {result.prompt}</p> |
|
</div> |
|
{/if} |
|
|
|
<style> |
|
.generator-form { |
|
margin-top: 2rem; |
|
padding-top: 2rem; |
|
border-top: 1px solid #eee; |
|
} |
|
|
|
h3 { |
|
margin-top: 0; |
|
margin-bottom: 1.5rem; |
|
} |
|
|
|
label { |
|
font-weight: 600; |
|
margin-bottom: 0.25rem; |
|
display: block; |
|
} |
|
|
|
input[type="text"], |
|
input[type="number"] { |
|
width: 100%; |
|
padding: 0.5rem 0.75rem; |
|
border: 1px solid #ccc; |
|
border-radius: 4px; |
|
box-sizing: border-box; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
input[type="checkbox"] { |
|
margin-right: 0.5rem; |
|
} |
|
|
|
.input-row { |
|
display: flex; |
|
gap: 1rem; |
|
} |
|
|
|
.input-group { |
|
flex: 1; |
|
} |
|
|
|
.generate-button { |
|
background: #007bff; |
|
color: #fff; |
|
border: none; |
|
padding: 0.6rem 1.4rem; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
font-size: 1rem; |
|
transition: background-color 0.2s; |
|
} |
|
|
|
.generate-button:hover:not(:disabled) { |
|
background: #0056b3; |
|
} |
|
|
|
.generate-button:disabled { |
|
background: #9ac7ff; |
|
cursor: not-allowed; |
|
} |
|
|
|
.result { |
|
margin-top: 1rem; |
|
} |
|
|
|
.result img { |
|
max-width: 100%; |
|
border-radius: 8px; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.error-message { |
|
color: #dc3545; |
|
margin-top: 1rem; |
|
padding: 0.5rem; |
|
background: #f8d7da; |
|
border-radius: 4px; |
|
} |
|
|
|
.status-message { |
|
color: #0c5460; |
|
margin-top: 1rem; |
|
padding: 0.5rem; |
|
background: #d1ecf1; |
|
border-radius: 4px; |
|
} |
|
</style> |