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