| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>FLUX-1 Schnell Demo (HF OAuth)</title> |
| <style> |
| :root { |
| font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; |
| } |
| body { |
| margin: 0; |
| padding: 2rem; |
| background: #f5f5f5; |
| } |
| .card { |
| max-width: 900px; |
| margin: 0 auto; |
| background: #ffffff; |
| padding: 2rem; |
| border-radius: 10px; |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| } |
| h1 { |
| margin-top: 0; |
| } |
| #signin { |
| background: #ff7b7b; |
| color: #fff; |
| border: none; |
| padding: 0.6rem 1.2rem; |
| border-radius: 6px; |
| cursor: pointer; |
| font-size: 1rem; |
| display: none; |
| } |
| #banner { |
| display: none; |
| margin-top: 1rem; |
| padding: 0.8rem 1rem; |
| border-radius: 6px; |
| background: #fff3cd; |
| color: #856404; |
| border: 1px solid #ffeeba; |
| } |
| |
| form { |
| margin-top: 2rem; |
| } |
| 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; |
| } |
| button.generate { |
| background: #007bff; |
| color: #fff; |
| border: none; |
| padding: 0.6rem 1.4rem; |
| border-radius: 6px; |
| cursor: pointer; |
| font-size: 1rem; |
| } |
| button.generate:disabled { |
| background: #9ac7ff; |
| cursor: not-allowed; |
| } |
| img { |
| max-width: 100%; |
| border-radius: 8px; |
| margin-top: 1rem; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="card"> |
| <h1>FLUX-1 Schnell + Joy Caption + Zephyr-7B</h1> |
| <p> |
| This static Space demonstrates how to call a remote Gradio Space while |
| letting <strong>each visitor’s own Hugging Face subscription</strong> cover |
| the compute costs. Generate images with FLUX-1, caption them with Joy Caption, and generate text with Zephyr-7B. |
| </p> |
|
|
| |
| <button id="signin">Sign in with Hugging Face</button> |
| <div id="banner"></div> |
|
|
| |
| <form id="generateForm" style="display:none;"> |
| <label for="prompt">Prompt</label> |
| <input type="text" id="prompt" placeholder="Enter your prompt" required /> |
|
|
| <label for="seed">Seed</label> |
| <input type="number" id="seed" value="0" min="0" max="2147483647" /> |
|
|
| <label> |
| <input type="checkbox" id="randomize_seed" checked /> Randomize seed |
| </label> |
|
|
| <div style="display:flex; gap:1rem;"> |
| <div style="flex:1;"> |
| <label for="width">Width</label> |
| <input type="number" id="width" value="1024" min="256" max="2048" step="32" /> |
| </div> |
| <div style="flex:1;"> |
| <label for="height">Height</label> |
| <input type="number" id="height" value="1024" min="256" max="2048" step="32" /> |
| </div> |
| </div> |
|
|
| <label for="steps">Number of inference steps</label> |
| <input type="number" id="steps" value="4" min="1" max="50" /> |
|
|
| <button type="submit" class="generate">Generate Image</button> |
| </form> |
|
|
| |
| <form id="captionForm" style="display:none; margin-top:2rem; border-top:1px solid #eee; padding-top:2rem;"> |
| <h3>Caption Image</h3> |
| <label for="imageInput">Upload Image or Generate Above</label> |
| <input type="file" id="imageInput" accept="image/*" /> |
| |
| <label for="captionType">Caption Type</label> |
| <select id="captionType"> |
| <option value="Descriptive">Descriptive</option> |
| <option value="Descriptive (Informal)">Descriptive (Informal)</option> |
| <option value="Training Prompt">Training Prompt</option> |
| <option value="MidJourney">MidJourney</option> |
| <option value="Booru tag list">Booru tag list</option> |
| <option value="Booru-like tag list">Booru-like tag list</option> |
| <option value="Art Critic">Art Critic</option> |
| <option value="Product Listing">Product Listing</option> |
| <option value="Social Media Post">Social Media Post</option> |
| </select> |
| |
| <label for="captionLength">Caption Length</label> |
| <select id="captionLength"> |
| <option value="any">Any</option> |
| <option value="very short">Very Short</option> |
| <option value="short">Short</option> |
| <option value="medium-length">Medium</option> |
| <option value="long" selected>Long</option> |
| <option value="very long">Very Long</option> |
| </select> |
| |
| <button type="submit" class="generate">Generate Caption</button> |
| </form> |
|
|
| |
| <div id="result"></div> |
| |
| |
| <div id="captionResult" style="margin-top:1rem;"></div> |
|
|
| |
| <form id="textForm" style="display:none; margin-top:2rem; border-top:1px solid #eee; padding-top:2rem;"> |
| <h3>Generate Text</h3> |
| <label for="textPrompt">Prompt</label> |
| <textarea id="textPrompt" rows="4" placeholder="Enter your prompt here..." style="width:100%; padding:0.5rem; border:1px solid #ccc; border-radius:4px; box-sizing:border-box; margin-bottom:1rem;"></textarea> |
| |
| <label for="systemPrompt">System Prompt</label> |
| <textarea id="systemPrompt" rows="3" placeholder="You are a helpful assistant..." style="width:100%; padding:0.5rem; border:1px solid #ccc; border-radius:4px; box-sizing:border-box; margin-bottom:1rem;"></textarea> |
| |
| <div style="display:flex; gap:1rem;"> |
| <div style="flex:1;"> |
| <label for="maxNewTokens">Max New Tokens</label> |
| <input type="number" id="maxNewTokens" value="1024" min="1" max="2048" step="1" /> |
| </div> |
| <div style="flex:1;"> |
| <label for="temperature">Temperature</label> |
| <input type="number" id="temperature" value="0.7" min="0.1" max="4.0" step="0.1" /> |
| </div> |
| </div> |
| |
| <div style="display:flex; gap:1rem;"> |
| <div style="flex:1;"> |
| <label for="topP">Top P</label> |
| <input type="number" id="topP" value="0.95" min="0.05" max="1.0" step="0.05" /> |
| </div> |
| <div style="flex:1;"> |
| <label for="topK">Top K</label> |
| <input type="number" id="topK" value="50" min="1" max="1000" step="1" /> |
| </div> |
| </div> |
| |
| <label for="repetitionPenalty">Repetition Penalty</label> |
| <input type="number" id="repetitionPenalty" value="1.0" min="1.0" max="2.0" step="0.05" /> |
| |
| <button type="submit" class="generate">Generate Text</button> |
| </form> |
| |
| |
| <div id="textResult" style="margin-top:1rem;"></div> |
|
|
| <hr /> |
| <p> |
| Source & docs: |
| <a href="https://huggingface.co/docs/hub/spaces-oauth" target="_blank">Spaces OAuth</a>, |
| <a href="https://github.com/huggingface/huggingface.js" target="_blank">huggingface.js</a>, |
| <a href="https://js.gradio.app" target="_blank">@gradio/client</a> |
| </p> |
| </div> |
|
|
| |
| <script type="module"> |
| import { |
| oauthLoginUrl, |
| oauthHandleRedirectIfPresent |
| } from "https://cdn.jsdelivr.net/npm/@huggingface/hub@0.21/+esm"; |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
| |
| |
| const signinBtn = document.getElementById("signin"); |
| const banner = document.getElementById("banner"); |
| const form = document.getElementById("generateForm"); |
| const resultDiv = document.getElementById("result"); |
| const captionForm = document.getElementById("captionForm"); |
| const captionResultDiv = document.getElementById("captionResult"); |
| const textForm = document.getElementById("textForm"); |
| const textResultDiv = document.getElementById("textResult"); |
| |
| const BANNER_ANON = |
| "⚠️ You are not logged in – anonymous users get only ~60 seconds of zero-GPU time per day. Sign in for higher limits."; |
| |
| |
| let session = await oauthHandleRedirectIfPresent(); |
| |
| if (!session) { |
| |
| banner.textContent = BANNER_ANON; |
| banner.style.display = "block"; |
| signinBtn.style.display = "inline-block"; |
| signinBtn.onclick = async () => { |
| const url = await oauthLoginUrl({ scopes: ["inference-api"] }); |
| window.location.href = url; |
| }; |
| |
| |
| startApp(null); |
| } else { |
| banner.style.display = "none"; |
| startApp(session.accessToken, session.userInfo); |
| } |
| |
| |
| |
| |
| |
| |
| async function startApp(hfToken, userInfo = null) { |
| try { |
| banner.textContent = "Connecting to FLUX.1-schnell, Joy Caption, and Zephyr-7B…"; |
| banner.style.display = "block"; |
| |
| const opts = hfToken ? { hf_token: hfToken } : {}; |
| const flux = await Client.connect( |
| "black-forest-labs/FLUX.1-schnell", |
| opts |
| ); |
| |
| const joyCaption = await Client.connect( |
| "fancyfeast/joy-caption-beta-one", |
| opts |
| ); |
| |
| |
| const zephyr = await Client.connect( |
| "hysts/zephyr-7b", |
| opts |
| ); |
| |
| banner.style.display = "none"; |
| form.style.display = "block"; |
| captionForm.style.display = "block"; |
| textForm.style.display = "block"; |
| |
| if (userInfo) { |
| const greeting = document.createElement("p"); |
| greeting.textContent = `Hello, ${userInfo.name || userInfo.preferred_username}!`; |
| form.parentNode.insertBefore(greeting, form); |
| } |
| |
| form.addEventListener("submit", async (e) => { |
| e.preventDefault(); |
| const prompt = document.getElementById("prompt").value; |
| const seed = parseInt(document.getElementById("seed").value, 10); |
| const randomize = document.getElementById("randomize_seed").checked; |
| const width = parseInt(document.getElementById("width").value, 10); |
| const height = parseInt(document.getElementById("height").value, 10); |
| const steps = parseInt(document.getElementById("steps").value, 10); |
| |
| const btn = form.querySelector("button.generate"); |
| btn.disabled = true; |
| btn.textContent = "Generating…"; |
| resultDiv.innerHTML = "Generating image…"; |
| |
| try { |
| console.log("=== CALLING FLUX ==="); |
| console.log("Parameters:", { prompt, seed, randomize, width, height, steps }); |
| |
| |
| console.log("Checking FLUX Space status..."); |
| try { |
| const apiInfo = await flux.view_api(); |
| console.log("FLUX Space API info:", apiInfo); |
| } catch (apiErr) { |
| console.warn("Could not get FLUX API info:", apiErr); |
| } |
| |
| console.log("Calling flux.predict..."); |
| const output = await flux.predict("/infer", [ |
| prompt, |
| seed, |
| randomize, |
| width, |
| height, |
| steps |
| ]); |
| |
| console.log("FLUX Raw result:", output); |
| const [image, usedSeed] = output.data; |
| let url; |
| if (typeof image === "string") url = image; |
| else if (image && image.url) url = image.url; |
| else if (image && image.path) url = image.path; |
| |
| if (url) { |
| resultDiv.innerHTML = ` |
| <img src="${url}" alt="Generated" /> |
| <p><strong>Seed</strong>: ${usedSeed}</p> |
| <p><strong>Prompt</strong>: ${prompt}</p> |
| `; |
| |
| |
| try { |
| const response = await fetch(url); |
| currentImage = await response.blob(); |
| } catch (imgErr) { |
| console.warn("Could not fetch generated image for captioning:", imgErr); |
| } |
| } else { |
| resultDiv.textContent = "Unexpected image format – open console."; |
| } |
| } catch (err) { |
| console.error(err); |
| resultDiv.textContent = `Generation failed: ${err}`; |
| } finally { |
| btn.disabled = false; |
| btn.textContent = "Generate Image"; |
| } |
| }); |
| |
| |
| let currentImage = null; |
| |
| captionForm.addEventListener("submit", async (e) => { |
| e.preventDefault(); |
| |
| const imageInput = document.getElementById("imageInput"); |
| const captionType = document.getElementById("captionType").value; |
| const captionLength = document.getElementById("captionLength").value; |
| |
| let imageToCaption = currentImage; |
| |
| |
| if (imageInput.files && imageInput.files[0]) { |
| imageToCaption = imageInput.files[0]; |
| } |
| |
| if (!imageToCaption) { |
| captionResultDiv.innerHTML = "Please upload an image or generate one above."; |
| return; |
| } |
| |
| const btn = captionForm.querySelector("button"); |
| btn.disabled = true; |
| btn.textContent = "Generating Caption…"; |
| captionResultDiv.innerHTML = "Generating caption…"; |
| |
| try { |
| console.log("=== CALLING JOY CAPTION ==="); |
| console.log("imageToCaption:", imageToCaption.name, imageToCaption.size, imageToCaption.type); |
| |
| |
| let promptTemplate; |
| if (captionLength === "any") { |
| promptTemplate = "Write a detailed description for this image."; |
| } else if (captionLength.match(/^\d+$/)) { |
| promptTemplate = `Write a detailed description for this image in ${captionLength} words or less.`; |
| } else { |
| promptTemplate = `Write a ${captionLength} detailed description for this image.`; |
| } |
| |
| |
| if (captionType === "Descriptive (Informal)") { |
| promptTemplate = promptTemplate.replace("detailed description", "descriptive caption in a casual tone"); |
| } else if (captionType === "Training Prompt") { |
| promptTemplate = "Output a stable diffusion prompt that is indistinguishable from a real stable diffusion prompt."; |
| } else if (captionType === "MidJourney") { |
| promptTemplate = "Write a MidJourney prompt for this image."; |
| } else if (captionType === "Booru tag list") { |
| promptTemplate = "Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags."; |
| } else if (captionType === "Booru-like tag list") { |
| promptTemplate = "Write a list of Booru-like tags for this image."; |
| } else if (captionType === "Art Critic") { |
| promptTemplate = "Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc."; |
| } else if (captionType === "Product Listing") { |
| promptTemplate = "Write a caption for this image as though it were a product listing."; |
| } else if (captionType === "Social Media Post") { |
| promptTemplate = "Write a caption for this image as if it were being used for a social media post."; |
| } |
| |
| console.log("Using prompt:", promptTemplate); |
| |
| |
| |
| console.log("Calling joyCaption.predict..."); |
| const result = await joyCaption.predict("/chat_joycaption", [ |
| imageToCaption, |
| promptTemplate, |
| 0.6, |
| 0.9, |
| 512, |
| false |
| ]); |
| |
| console.log("Raw result:", result); |
| |
| |
| let caption; |
| if (result && result.data) { |
| caption = result.data[0] || result.data; |
| } else { |
| caption = result; |
| } |
| |
| console.log("Success! Caption:", caption); |
| captionResultDiv.innerHTML = ` |
| <div style="background:#f8f9fa; padding:1rem; border-radius:6px; margin-top:1rem;"> |
| <h4>Generated Caption</h4> |
| <p><strong>Type:</strong> ${captionType}</p> |
| <p><strong>Length:</strong> ${captionLength}</p> |
| <p><strong>Caption:</strong></p> |
| <p style="font-style:italic;">${caption}</p> |
| </div> |
| `; |
| } catch (err) { |
| console.error("=== DETAILED ERROR INFO ==="); |
| console.error("Error type:", typeof err); |
| console.error("Error constructor:", err.constructor?.name); |
| console.error("Error message:", err.message); |
| console.error("Error stack:", err.stack); |
| console.error("Full error object:", err); |
| console.error("Error keys:", Object.keys(err)); |
| console.error("Error JSON:", JSON.stringify(err, null, 2)); |
| |
| captionResultDiv.innerHTML = ` |
| <div style="color:red; background:#fee; padding:1rem; border-radius:6px; margin-top:1rem;"> |
| <h4>Caption Generation Failed</h4> |
| <p><strong>Error Type:</strong> ${err.constructor?.name || typeof err}</p> |
| <p><strong>Error Message:</strong> ${err.message || err.toString()}</p> |
| <p><strong>Full Error:</strong></p> |
| <pre style="font-size:12px; overflow:auto;">${JSON.stringify(err, null, 2)}</pre> |
| </div> |
| `; |
| } finally { |
| btn.disabled = false; |
| btn.textContent = "Generate Caption"; |
| } |
| }); |
| |
| |
| textForm.addEventListener("submit", async (e) => { |
| e.preventDefault(); |
| |
| const prompt = document.getElementById("textPrompt").value.trim(); |
| const systemPrompt = document.getElementById("systemPrompt").value.trim(); |
| const maxNewTokens = parseInt(document.getElementById("maxNewTokens").value, 10); |
| const temperature = parseFloat(document.getElementById("temperature").value); |
| const topP = parseFloat(document.getElementById("topP").value); |
| const topK = parseInt(document.getElementById("topK").value, 10); |
| const repetitionPenalty = parseFloat(document.getElementById("repetitionPenalty").value); |
| |
| if (!prompt) { |
| textResultDiv.innerHTML = "<p style='color:red;'>Please enter a prompt.</p>"; |
| return; |
| } |
| |
| const btn = textForm.querySelector("button"); |
| btn.disabled = true; |
| btn.textContent = "Generating Text…"; |
| textResultDiv.innerHTML = "Generating text…"; |
| |
| try { |
| |
| |
| const output = await zephyr.predict("/chat", [ |
| prompt, |
| [], |
| systemPrompt, |
| maxNewTokens, |
| temperature, |
| topP, |
| topK, |
| repetitionPenalty |
| ]); |
| |
| |
| const generatedText = output.data; |
| textResultDiv.innerHTML = ` |
| <div style="background:#f8f9fa; padding:1rem; border-radius:6px; margin-top:1rem;"> |
| <h4>Generated Text</h4> |
| <p><strong>Prompt:</strong> ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}</p> |
| <p><strong>Generated:</strong></p> |
| <div style="white-space:pre-wrap; font-family:monospace; background:#fff; padding:1rem; border-radius:4px; border:1px solid #ddd;">${generatedText}</div> |
| </div> |
| `; |
| } catch (err) { |
| console.error(err); |
| textResultDiv.innerHTML = `<p style="color:red;">Text generation failed: ${err}</p>`; |
| } finally { |
| btn.disabled = false; |
| btn.textContent = "Generate Text"; |
| } |
| }); |
| |
| |
| } catch (err) { |
| console.error(err); |
| banner.textContent = `❌ Failed to connect: ${err}`; |
| banner.style.display = "block"; |
| signinBtn.style.display = "none"; |
| } |
| } |
| </script> |
| </body> |
| </html> |
|
|