|
<!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-alpha-two", |
|
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 { |
|
const output = await flux.predict("/infer", [ |
|
prompt, |
|
seed, |
|
randomize, |
|
width, |
|
height, |
|
steps |
|
]); |
|
|
|
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 { |
|
const output = await joyCaption.predict("/stream_chat", [ |
|
imageToCaption, |
|
captionType, |
|
captionLength, |
|
[], |
|
"", |
|
"" |
|
]); |
|
|
|
const [prompt, caption] = output.data; |
|
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(err); |
|
captionResultDiv.innerHTML = `<p style="color:red;">Caption generation failed: ${err}</p>`; |
|
} 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> |
|
|