Spaces:
Running
Running
Merge branch 'main' of https://github.com/Digital-Transformation-TNT/website_content_tnt
Browse files- .gitignore +1 -0
- backend/app/api/flux2.py +287 -0
- backend/app/api/video.py +1 -1
- backend/app/main.py +2 -1
- frontend/src/App.jsx +2 -0
- frontend/src/components/Layout.jsx +2 -1
- frontend/src/pages/CreateVideo.jsx +0 -10
- frontend/src/pages/Flux2Gen.jsx +536 -0
.gitignore
CHANGED
|
@@ -3,3 +3,4 @@ frontend/dist/
|
|
| 3 |
__pycache__/
|
| 4 |
*.pyc
|
| 5 |
.env
|
|
|
|
|
|
| 3 |
__pycache__/
|
| 4 |
*.pyc
|
| 5 |
.env
|
| 6 |
+
server_*.log
|
backend/app/api/flux2.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import threading
|
| 5 |
+
import time
|
| 6 |
+
import uuid
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import httpx
|
| 11 |
+
from fastapi import APIRouter, HTTPException
|
| 12 |
+
from fastapi.responses import FileResponse
|
| 13 |
+
from pydantic import BaseModel
|
| 14 |
+
from gradio_client import Client, handle_file
|
| 15 |
+
|
| 16 |
+
WANGP_URL = "https://00114dcf4c286c050a.gradio.live/"
|
| 17 |
+
OUTPUT_DIR = Path(__file__).resolve().parent.parent / "flux2_outputs"
|
| 18 |
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
| 19 |
+
|
| 20 |
+
_client = None
|
| 21 |
+
_client_lock = threading.Lock()
|
| 22 |
+
|
| 23 |
+
def _get_client():
|
| 24 |
+
global _client
|
| 25 |
+
if _client is not None:
|
| 26 |
+
return _client
|
| 27 |
+
with _client_lock:
|
| 28 |
+
if _client is not None:
|
| 29 |
+
return _client
|
| 30 |
+
try:
|
| 31 |
+
_client = Client(WANGP_URL)
|
| 32 |
+
except Exception as e:
|
| 33 |
+
raise RuntimeError(f"Cannot connect to Wangp API at {WANGP_URL}: {e}")
|
| 34 |
+
return _client
|
| 35 |
+
|
| 36 |
+
DEFAULT_VIDEO = {
|
| 37 |
+
"video": handle_file(
|
| 38 |
+
"https://github.com/gradio-app/gradio/raw/main/demo/video_component/files/world.mp4"
|
| 39 |
+
)
|
| 40 |
+
}
|
| 41 |
+
DEFAULT_IMAGE = handle_file(
|
| 42 |
+
"https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
|
| 43 |
+
)
|
| 44 |
+
DEFAULT_AUDIO = handle_file(
|
| 45 |
+
"https://github.com/gradio-app/gradio/raw/main/test/test_files/audio_sample.wav"
|
| 46 |
+
)
|
| 47 |
+
DEFAULT_FILE = handle_file(
|
| 48 |
+
"https://github.com/gradio-app/gradio/raw/main/test/test_files/sample_file.pdf"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
_tasks: dict[str, dict[str, Any]] = {}
|
| 52 |
+
_tasks_lock = threading.Lock()
|
| 53 |
+
|
| 54 |
+
router = APIRouter(prefix="/api/flux2", tags=["flux2"])
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class GenRequest(BaseModel):
|
| 58 |
+
prompt: str
|
| 59 |
+
negative_prompt: str = ""
|
| 60 |
+
resolution: str = "1024x1024"
|
| 61 |
+
num_inference_steps: int = 4
|
| 62 |
+
guidance_scale: float = 5.0
|
| 63 |
+
seed: int = -1
|
| 64 |
+
batch_size: int = 1
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _extract_images(result) -> list:
|
| 68 |
+
if not result:
|
| 69 |
+
return []
|
| 70 |
+
if isinstance(result, (list, tuple)):
|
| 71 |
+
for item in result:
|
| 72 |
+
if isinstance(item, list):
|
| 73 |
+
return item
|
| 74 |
+
if isinstance(item, dict) and ("image" in item or "video" in item):
|
| 75 |
+
return [item]
|
| 76 |
+
if all(isinstance(x, dict) for x in result if x):
|
| 77 |
+
return result
|
| 78 |
+
return []
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _download_image(img_info: Any, dest: Path):
|
| 82 |
+
if isinstance(img_info, dict):
|
| 83 |
+
img = img_info.get("image", img_info.get("video", {}))
|
| 84 |
+
if isinstance(img, dict):
|
| 85 |
+
path = img.get("path", img.get("url", ""))
|
| 86 |
+
else:
|
| 87 |
+
path = str(img) if img else ""
|
| 88 |
+
else:
|
| 89 |
+
path = str(img_info)
|
| 90 |
+
|
| 91 |
+
if not path:
|
| 92 |
+
raise ValueError("No image path found")
|
| 93 |
+
|
| 94 |
+
if path.startswith("/"):
|
| 95 |
+
url = f"{WANGP_URL.rstrip('/')}/file={path.lstrip('/')}"
|
| 96 |
+
elif path.startswith("http"):
|
| 97 |
+
url = path
|
| 98 |
+
else:
|
| 99 |
+
url = f"{WANGP_URL.rstrip('/')}/file={path}"
|
| 100 |
+
|
| 101 |
+
with httpx.Client(timeout=120.0, follow_redirects=True) as c:
|
| 102 |
+
resp = c.get(url)
|
| 103 |
+
resp.raise_for_status()
|
| 104 |
+
dest.write_bytes(resp.content)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _generate(task_id: str, params: dict):
|
| 108 |
+
try:
|
| 109 |
+
with _tasks_lock:
|
| 110 |
+
_tasks[task_id] = {"status": "processing"}
|
| 111 |
+
|
| 112 |
+
result = _get_client().predict(
|
| 113 |
+
prompt=params["prompt"],
|
| 114 |
+
negative_prompt=params.get("negative_prompt", ""),
|
| 115 |
+
alt_prompt="",
|
| 116 |
+
resolution=params.get("resolution", "1024x1024"),
|
| 117 |
+
video_length=81,
|
| 118 |
+
duration_seconds=0,
|
| 119 |
+
pause_seconds=0,
|
| 120 |
+
batch_size=params.get("batch_size", 1),
|
| 121 |
+
seed=params.get("seed", -1),
|
| 122 |
+
num_inference_steps=params.get("num_inference_steps", 4),
|
| 123 |
+
guidance_scale=params.get("guidance_scale", 5),
|
| 124 |
+
guidance2_scale=5,
|
| 125 |
+
guidance3_scale=5,
|
| 126 |
+
guidance_phases="1",
|
| 127 |
+
model_switch_phase="1",
|
| 128 |
+
flow_shift=5,
|
| 129 |
+
sample_solver="",
|
| 130 |
+
temperature=0.8,
|
| 131 |
+
top_p=0.9,
|
| 132 |
+
top_k=50,
|
| 133 |
+
loras_choices=[],
|
| 134 |
+
loras_multipliers="",
|
| 135 |
+
image_refs=[],
|
| 136 |
+
image_refs_relative_size=50,
|
| 137 |
+
remove_background_images_ref="0",
|
| 138 |
+
frames_positions="",
|
| 139 |
+
image_prompt_type="",
|
| 140 |
+
image_start=[],
|
| 141 |
+
image_end=[],
|
| 142 |
+
image_guide=DEFAULT_IMAGE,
|
| 143 |
+
image_mask=DEFAULT_IMAGE,
|
| 144 |
+
control_net_weight=1,
|
| 145 |
+
control_net_weight2=1,
|
| 146 |
+
denoising_strength=1,
|
| 147 |
+
masking_strength=0.25,
|
| 148 |
+
mask_expand=0,
|
| 149 |
+
model_mode="0",
|
| 150 |
+
prompt_enhancer="",
|
| 151 |
+
film_grain_intensity=0,
|
| 152 |
+
film_grain_saturation=0.5,
|
| 153 |
+
temporal_upsampling="",
|
| 154 |
+
spatial_upsampling="",
|
| 155 |
+
video_guide_outpainting="#",
|
| 156 |
+
video_guide_outpainting_ratio="",
|
| 157 |
+
override_profile="-1",
|
| 158 |
+
override_attention="auto",
|
| 159 |
+
alt_guidance_scale=1,
|
| 160 |
+
alt_scale=0,
|
| 161 |
+
audio_guidance_scale=1,
|
| 162 |
+
audio_scale=1,
|
| 163 |
+
embedded_guidance_scale=1,
|
| 164 |
+
repeat_generation=1,
|
| 165 |
+
multi_prompts_gen_type="PG",
|
| 166 |
+
multi_images_gen_type="0",
|
| 167 |
+
skip_steps_cache_type="",
|
| 168 |
+
skip_steps_multiplier="1.75",
|
| 169 |
+
skip_steps_start_step_perc=0,
|
| 170 |
+
self_refiner_setting="0",
|
| 171 |
+
self_refiner_f_uncertainty=0,
|
| 172 |
+
self_refiner_certain_percentage=0.999,
|
| 173 |
+
output_filename="",
|
| 174 |
+
mode="",
|
| 175 |
+
video_source=DEFAULT_VIDEO,
|
| 176 |
+
video_guide=DEFAULT_VIDEO,
|
| 177 |
+
video_mask=DEFAULT_VIDEO,
|
| 178 |
+
keep_frames_video_guide="",
|
| 179 |
+
keep_frames_video_source="",
|
| 180 |
+
input_video_strength=1,
|
| 181 |
+
audio_guide=DEFAULT_AUDIO,
|
| 182 |
+
audio_guide2=DEFAULT_AUDIO,
|
| 183 |
+
audio_source=DEFAULT_AUDIO,
|
| 184 |
+
replace_voice_sample=DEFAULT_AUDIO,
|
| 185 |
+
replace_voice_sample2=DEFAULT_AUDIO,
|
| 186 |
+
custom_guide=DEFAULT_FILE,
|
| 187 |
+
replace_voice_method="",
|
| 188 |
+
postprocess_audio="",
|
| 189 |
+
postprocess_audio_prompt="",
|
| 190 |
+
postprocess_audio_neg_prompt="",
|
| 191 |
+
speakers_locations="0:45 55:100",
|
| 192 |
+
sliding_window_size=81,
|
| 193 |
+
sliding_window_overlap=5,
|
| 194 |
+
sub_parallel_window_size=0,
|
| 195 |
+
sub_parallel_window_overlap=17,
|
| 196 |
+
sliding_window_color_correction_strength=0,
|
| 197 |
+
sliding_window_overlap_noise=0,
|
| 198 |
+
sliding_window_discard_last_frames=0,
|
| 199 |
+
sliding_window_trim_first_frames=0,
|
| 200 |
+
NAG_scale=1,
|
| 201 |
+
NAG_tau=3.5,
|
| 202 |
+
NAG_alpha=0.5,
|
| 203 |
+
perturbation_switch="0",
|
| 204 |
+
perturbation_layers=[9],
|
| 205 |
+
perturbation_start_perc=10,
|
| 206 |
+
perturbation_end_perc=90,
|
| 207 |
+
apg_switch="0",
|
| 208 |
+
cfg_star_switch="0",
|
| 209 |
+
cfg_zero_step=-1,
|
| 210 |
+
min_frames_if_references="1",
|
| 211 |
+
RIFLEx_setting="0",
|
| 212 |
+
force_fps="",
|
| 213 |
+
api_name="/save_inputs_1",
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
images = _extract_images(result)
|
| 217 |
+
saved = []
|
| 218 |
+
for i, img_info in enumerate(images):
|
| 219 |
+
try:
|
| 220 |
+
path = OUTPUT_DIR / f"{task_id}_{i}.png"
|
| 221 |
+
_download_image(img_info, path)
|
| 222 |
+
saved.append(
|
| 223 |
+
{
|
| 224 |
+
"index": i,
|
| 225 |
+
"url": f"/api/flux2/file/{task_id}_{i}.png",
|
| 226 |
+
"width": img_info.get("image", {}).get("width"),
|
| 227 |
+
"height": img_info.get("image", {}).get("height"),
|
| 228 |
+
}
|
| 229 |
+
if isinstance(img_info, dict)
|
| 230 |
+
else {"index": i, "url": f"/api/flux2/file/{task_id}_{i}.png"}
|
| 231 |
+
)
|
| 232 |
+
except Exception as e:
|
| 233 |
+
saved.append({"index": i, "error": str(e)})
|
| 234 |
+
|
| 235 |
+
with _tasks_lock:
|
| 236 |
+
_tasks[task_id] = {"status": "done", "images": saved}
|
| 237 |
+
|
| 238 |
+
except Exception as e:
|
| 239 |
+
import traceback
|
| 240 |
+
|
| 241 |
+
with _tasks_lock:
|
| 242 |
+
_tasks[task_id] = {
|
| 243 |
+
"status": "error",
|
| 244 |
+
"error": str(e),
|
| 245 |
+
"traceback": traceback.format_exc(),
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
@router.get("/health")
|
| 250 |
+
async def api_health():
|
| 251 |
+
try:
|
| 252 |
+
c = _get_client()
|
| 253 |
+
endpoints = c.view_api(all_endpoints=True, return_format="dict") if hasattr(c, "view_api") else {}
|
| 254 |
+
return {"connected": True, "url": WANGP_URL}
|
| 255 |
+
except Exception as e:
|
| 256 |
+
return {"connected": False, "url": WANGP_URL, "error": str(e)}
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
@router.post("/generate")
|
| 260 |
+
async def api_generate(req: GenRequest):
|
| 261 |
+
task_id = str(uuid.uuid4())
|
| 262 |
+
with _tasks_lock:
|
| 263 |
+
_tasks[task_id] = {"status": "queued"}
|
| 264 |
+
|
| 265 |
+
thread = threading.Thread(
|
| 266 |
+
target=_generate, args=(task_id, req.model_dump()), daemon=True
|
| 267 |
+
)
|
| 268 |
+
thread.start()
|
| 269 |
+
|
| 270 |
+
return {"task_id": task_id, "status": "queued"}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
@router.get("/status/{task_id}")
|
| 274 |
+
async def api_status(task_id: str):
|
| 275 |
+
with _tasks_lock:
|
| 276 |
+
task = _tasks.get(task_id)
|
| 277 |
+
if not task:
|
| 278 |
+
raise HTTPException(404, "Task not found")
|
| 279 |
+
return task
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
@router.get("/file/{filename:path}")
|
| 283 |
+
async def api_file(filename: str):
|
| 284 |
+
path = OUTPUT_DIR / filename
|
| 285 |
+
if not path.exists():
|
| 286 |
+
raise HTTPException(404, "File not found")
|
| 287 |
+
return FileResponse(str(path), media_type="image/png")
|
backend/app/api/video.py
CHANGED
|
@@ -8,7 +8,7 @@ import time
|
|
| 8 |
from urllib.parse import unquote, urlparse
|
| 9 |
|
| 10 |
import httpx
|
| 11 |
-
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
| 12 |
from fastapi.responses import FileResponse, Response, StreamingResponse
|
| 13 |
from pydantic import BaseModel
|
| 14 |
|
|
|
|
| 8 |
from urllib.parse import unquote, urlparse
|
| 9 |
|
| 10 |
import httpx
|
| 11 |
+
from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Request
|
| 12 |
from fastapi.responses import FileResponse, Response, StreamingResponse
|
| 13 |
from pydantic import BaseModel
|
| 14 |
|
backend/app/main.py
CHANGED
|
@@ -31,7 +31,7 @@ _here = os.path.dirname(os.path.abspath(__file__))
|
|
| 31 |
_parent = os.path.dirname(_here)
|
| 32 |
sys.path.insert(0, _parent)
|
| 33 |
|
| 34 |
-
from .api import common, download, cut, shuffle, dub, listing, video, auth, drive, history_api
|
| 35 |
|
| 36 |
app = FastAPI(title="TNT Group - AI Content Create", version="1.0.0")
|
| 37 |
|
|
@@ -53,6 +53,7 @@ app.include_router(video.router)
|
|
| 53 |
app.include_router(auth.router)
|
| 54 |
app.include_router(drive.router)
|
| 55 |
app.include_router(history_api.router)
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
@app.get("/api/health")
|
|
|
|
| 31 |
_parent = os.path.dirname(_here)
|
| 32 |
sys.path.insert(0, _parent)
|
| 33 |
|
| 34 |
+
from .api import common, download, cut, shuffle, dub, listing, video, auth, drive, history_api, flux2
|
| 35 |
|
| 36 |
app = FastAPI(title="TNT Group - AI Content Create", version="1.0.0")
|
| 37 |
|
|
|
|
| 53 |
app.include_router(auth.router)
|
| 54 |
app.include_router(drive.router)
|
| 55 |
app.include_router(history_api.router)
|
| 56 |
+
app.include_router(flux2.router)
|
| 57 |
|
| 58 |
|
| 59 |
@app.get("/api/health")
|
frontend/src/App.jsx
CHANGED
|
@@ -9,6 +9,7 @@ import Guide from "./pages/Guide";
|
|
| 9 |
import Support from "./pages/Support";
|
| 10 |
import Placeholder from "./pages/Placeholder";
|
| 11 |
import ListingImage from "./pages/ListingImage";
|
|
|
|
| 12 |
import CreateVideo from "./pages/CreateVideo";
|
| 13 |
import AdminUsers from "./pages/AdminUsers";
|
| 14 |
import Login from "./pages/Login";
|
|
@@ -28,6 +29,7 @@ const router = createBrowserRouter([
|
|
| 28 |
{ path: "/xao", element: <ShuffleVideo /> },
|
| 29 |
{ path: "/dub", element: <DubVideo /> },
|
| 30 |
{ path: "/listing-anh", element: <ListingImage /> },
|
|
|
|
| 31 |
{ path: "/tao-video", element: <CreateVideo /> },
|
| 32 |
{ path: "/huong-dan", element: <Guide /> },
|
| 33 |
{ path: "/ho-tro", element: <Support /> },
|
|
|
|
| 9 |
import Support from "./pages/Support";
|
| 10 |
import Placeholder from "./pages/Placeholder";
|
| 11 |
import ListingImage from "./pages/ListingImage";
|
| 12 |
+
import Flux2Gen from "./pages/Flux2Gen";
|
| 13 |
import CreateVideo from "./pages/CreateVideo";
|
| 14 |
import AdminUsers from "./pages/AdminUsers";
|
| 15 |
import Login from "./pages/Login";
|
|
|
|
| 29 |
{ path: "/xao", element: <ShuffleVideo /> },
|
| 30 |
{ path: "/dub", element: <DubVideo /> },
|
| 31 |
{ path: "/listing-anh", element: <ListingImage /> },
|
| 32 |
+
{ path: "/flux2", element: <Flux2Gen /> },
|
| 33 |
{ path: "/tao-video", element: <CreateVideo /> },
|
| 34 |
{ path: "/huong-dan", element: <Guide /> },
|
| 35 |
{ path: "/ho-tro", element: <Support /> },
|
frontend/src/components/Layout.jsx
CHANGED
|
@@ -73,7 +73,7 @@ export default function Layout() {
|
|
| 73 |
}}>
|
| 74 |
<Outlet />
|
| 75 |
<iframe
|
| 76 |
-
src={connected && url ?
|
| 77 |
style={{
|
| 78 |
flex: 1,
|
| 79 |
width: "100%",
|
|
@@ -81,6 +81,7 @@ export default function Layout() {
|
|
| 81 |
display: connected ? "block" : "none",
|
| 82 |
}}
|
| 83 |
allow="clipboard-read; clipboard-write; fullscreen"
|
|
|
|
| 84 |
title="Gradio Console"
|
| 85 |
/>
|
| 86 |
</div>
|
|
|
|
| 73 |
}}>
|
| 74 |
<Outlet />
|
| 75 |
<iframe
|
| 76 |
+
src={connected && url ? url : "about:blank"}
|
| 77 |
style={{
|
| 78 |
flex: 1,
|
| 79 |
width: "100%",
|
|
|
|
| 81 |
display: connected ? "block" : "none",
|
| 82 |
}}
|
| 83 |
allow="clipboard-read; clipboard-write; fullscreen"
|
| 84 |
+
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
| 85 |
title="Gradio Console"
|
| 86 |
/>
|
| 87 |
</div>
|
frontend/src/pages/CreateVideo.jsx
CHANGED
|
@@ -94,16 +94,6 @@ export default function CreateVideo() {
|
|
| 94 |
)}
|
| 95 |
</div>
|
| 96 |
|
| 97 |
-
{/* Iframe Gradio UI khi đã kết nối */}
|
| 98 |
-
{connected && (
|
| 99 |
-
<iframe
|
| 100 |
-
src={url}
|
| 101 |
-
style={{ flex: 1, width: "100%", border: "none" }}
|
| 102 |
-
allow="clipboard-read; clipboard-write"
|
| 103 |
-
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
| 104 |
-
/>
|
| 105 |
-
)}
|
| 106 |
-
|
| 107 |
{/* Hướng dẫn khi chưa kết nối */}
|
| 108 |
{!connected && (
|
| 109 |
<div style={{
|
|
|
|
| 94 |
)}
|
| 95 |
</div>
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
{/* Hướng dẫn khi chưa kết nối */}
|
| 98 |
{!connected && (
|
| 99 |
<div style={{
|
frontend/src/pages/Flux2Gen.jsx
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useCallback, useRef } from "react";
|
| 2 |
+
import { IcBolt, IcDownload as IcDl, IcImage, IcSliders } from "../components/Icons";
|
| 3 |
+
|
| 4 |
+
const RESOLUTIONS = [
|
| 5 |
+
{ label: "1024×1024 (Square)", value: "1024x1024" },
|
| 6 |
+
{ label: "1280×720 (Landscape)", value: "1280x720" },
|
| 7 |
+
{ label: "720×1280 (Portrait)", value: "720x1280" },
|
| 8 |
+
{ label: "1600×400 (Panorama)", value: "1600x400" },
|
| 9 |
+
{ label: "1280×544 (Cinema)", value: "1280x544" },
|
| 10 |
+
{ label: "1104×832 (3:2)", value: "1104x832" },
|
| 11 |
+
{ label: "960×960 (Sq. small)", value: "960x960" },
|
| 12 |
+
];
|
| 13 |
+
|
| 14 |
+
const STYLES = {
|
| 15 |
+
container: {
|
| 16 |
+
maxWidth: 960,
|
| 17 |
+
margin: "0 auto",
|
| 18 |
+
},
|
| 19 |
+
header: {
|
| 20 |
+
marginBottom: 28,
|
| 21 |
+
},
|
| 22 |
+
headerTitle: {
|
| 23 |
+
fontSize: 22,
|
| 24 |
+
fontWeight: 800,
|
| 25 |
+
margin: "0 0 4px",
|
| 26 |
+
display: "flex",
|
| 27 |
+
alignItems: "center",
|
| 28 |
+
gap: 10,
|
| 29 |
+
},
|
| 30 |
+
headerBadge: {
|
| 31 |
+
fontSize: 11,
|
| 32 |
+
fontWeight: 700,
|
| 33 |
+
padding: "3px 9px",
|
| 34 |
+
borderRadius: 10,
|
| 35 |
+
background: "var(--orange-soft)",
|
| 36 |
+
color: "var(--orange-700)",
|
| 37 |
+
},
|
| 38 |
+
headerSub: {
|
| 39 |
+
color: "var(--ink-2)",
|
| 40 |
+
fontSize: 14,
|
| 41 |
+
margin: 0,
|
| 42 |
+
},
|
| 43 |
+
card: {
|
| 44 |
+
background: "var(--white)",
|
| 45 |
+
border: "1px solid var(--line)",
|
| 46 |
+
borderRadius: "var(--radius)",
|
| 47 |
+
padding: 24,
|
| 48 |
+
boxShadow: "var(--shadow)",
|
| 49 |
+
marginBottom: 20,
|
| 50 |
+
},
|
| 51 |
+
label: {
|
| 52 |
+
fontSize: 13,
|
| 53 |
+
fontWeight: 600,
|
| 54 |
+
color: "var(--ink-2)",
|
| 55 |
+
marginBottom: 8,
|
| 56 |
+
display: "block",
|
| 57 |
+
},
|
| 58 |
+
textarea: {
|
| 59 |
+
width: "100%",
|
| 60 |
+
fontFamily: "inherit",
|
| 61 |
+
fontSize: 14,
|
| 62 |
+
color: "var(--ink)",
|
| 63 |
+
border: "1px solid var(--line)",
|
| 64 |
+
borderRadius: "var(--radius-sm)",
|
| 65 |
+
padding: "12px 14px",
|
| 66 |
+
background: "var(--white)",
|
| 67 |
+
outline: "none",
|
| 68 |
+
resize: "vertical",
|
| 69 |
+
minHeight: 120,
|
| 70 |
+
lineHeight: 1.6,
|
| 71 |
+
},
|
| 72 |
+
paramGrid: {
|
| 73 |
+
display: "grid",
|
| 74 |
+
gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))",
|
| 75 |
+
gap: 16,
|
| 76 |
+
},
|
| 77 |
+
paramLabel: {
|
| 78 |
+
fontSize: 12,
|
| 79 |
+
fontWeight: 600,
|
| 80 |
+
color: "var(--muted)",
|
| 81 |
+
marginBottom: 4,
|
| 82 |
+
display: "block",
|
| 83 |
+
},
|
| 84 |
+
input: {
|
| 85 |
+
width: "100%",
|
| 86 |
+
fontFamily: "inherit",
|
| 87 |
+
fontSize: 14,
|
| 88 |
+
color: "var(--ink)",
|
| 89 |
+
border: "1px solid var(--line)",
|
| 90 |
+
borderRadius: "var(--radius-sm)",
|
| 91 |
+
padding: "9px 11px",
|
| 92 |
+
background: "var(--white)",
|
| 93 |
+
outline: "none",
|
| 94 |
+
},
|
| 95 |
+
select: {
|
| 96 |
+
width: "100%",
|
| 97 |
+
fontFamily: "inherit",
|
| 98 |
+
fontSize: 14,
|
| 99 |
+
color: "var(--ink)",
|
| 100 |
+
border: "1px solid var(--line)",
|
| 101 |
+
borderRadius: "var(--radius-sm)",
|
| 102 |
+
padding: "9px 11px",
|
| 103 |
+
background: "var(--white)",
|
| 104 |
+
outline: "none",
|
| 105 |
+
cursor: "pointer",
|
| 106 |
+
},
|
| 107 |
+
generateBtn: {
|
| 108 |
+
display: "inline-flex",
|
| 109 |
+
alignItems: "center",
|
| 110 |
+
justifyContent: "center",
|
| 111 |
+
gap: 8,
|
| 112 |
+
border: "none",
|
| 113 |
+
background: "var(--orange)",
|
| 114 |
+
color: "#fff",
|
| 115 |
+
padding: "12px 28px",
|
| 116 |
+
borderRadius: "var(--radius-sm)",
|
| 117 |
+
fontSize: 15,
|
| 118 |
+
fontWeight: 700,
|
| 119 |
+
cursor: "pointer",
|
| 120 |
+
transition: "all .15s ease",
|
| 121 |
+
width: "100%",
|
| 122 |
+
marginTop: 18,
|
| 123 |
+
},
|
| 124 |
+
gallery: {
|
| 125 |
+
display: "grid",
|
| 126 |
+
gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
|
| 127 |
+
gap: 16,
|
| 128 |
+
},
|
| 129 |
+
imgCard: {
|
| 130 |
+
border: "1px solid var(--line)",
|
| 131 |
+
borderRadius: "var(--radius)",
|
| 132 |
+
padding: 12,
|
| 133 |
+
background: "var(--white)",
|
| 134 |
+
},
|
| 135 |
+
img: {
|
| 136 |
+
width: "100%",
|
| 137 |
+
height: "auto",
|
| 138 |
+
borderRadius: "var(--radius-sm)",
|
| 139 |
+
display: "block",
|
| 140 |
+
background: "#f5f0ea",
|
| 141 |
+
},
|
| 142 |
+
downloadBtn: {
|
| 143 |
+
display: "inline-flex",
|
| 144 |
+
alignItems: "center",
|
| 145 |
+
gap: 6,
|
| 146 |
+
border: "1px solid var(--line)",
|
| 147 |
+
background: "var(--white)",
|
| 148 |
+
color: "var(--ink)",
|
| 149 |
+
padding: "7px 14px",
|
| 150 |
+
borderRadius: "var(--radius-sm)",
|
| 151 |
+
fontSize: 13,
|
| 152 |
+
fontWeight: 600,
|
| 153 |
+
cursor: "pointer",
|
| 154 |
+
marginTop: 8,
|
| 155 |
+
width: "100%",
|
| 156 |
+
justifyContent: "center",
|
| 157 |
+
},
|
| 158 |
+
statusBox: {
|
| 159 |
+
padding: "14px 18px",
|
| 160 |
+
borderRadius: "var(--radius-sm)",
|
| 161 |
+
fontSize: 14,
|
| 162 |
+
marginBottom: 18,
|
| 163 |
+
display: "flex",
|
| 164 |
+
alignItems: "center",
|
| 165 |
+
gap: 10,
|
| 166 |
+
},
|
| 167 |
+
spinner: {
|
| 168 |
+
width: 20,
|
| 169 |
+
height: 20,
|
| 170 |
+
border: "3px solid var(--line)",
|
| 171 |
+
borderTopColor: "var(--orange)",
|
| 172 |
+
borderRadius: "50%",
|
| 173 |
+
animation: "proc-spin .8s linear infinite",
|
| 174 |
+
flex: "0 0 20px",
|
| 175 |
+
},
|
| 176 |
+
detailBtn: {
|
| 177 |
+
border: "none",
|
| 178 |
+
background: "none",
|
| 179 |
+
color: "var(--orange)",
|
| 180 |
+
cursor: "pointer",
|
| 181 |
+
fontSize: 12,
|
| 182 |
+
fontWeight: 600,
|
| 183 |
+
padding: 0,
|
| 184 |
+
marginTop: 6,
|
| 185 |
+
textDecoration: "underline",
|
| 186 |
+
},
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
function formatTime(s) {
|
| 190 |
+
const m = Math.floor(s / 60);
|
| 191 |
+
const sec = s % 60;
|
| 192 |
+
return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
export default function Flux2Gen() {
|
| 196 |
+
const [prompt, setPrompt] = useState("a cozy reading nook with warm sunlight");
|
| 197 |
+
const [negativePrompt, setNegativePrompt] = useState("");
|
| 198 |
+
const [resolution, setResolution] = useState("1024x1024");
|
| 199 |
+
const [steps, setSteps] = useState(4);
|
| 200 |
+
const [guidance, setGuidance] = useState(5);
|
| 201 |
+
const [seed, setSeed] = useState(-1);
|
| 202 |
+
const [batch, setBatch] = useState(1);
|
| 203 |
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
| 204 |
+
|
| 205 |
+
const [running, setRunning] = useState(false);
|
| 206 |
+
const [status, setStatus] = useState(null);
|
| 207 |
+
const [images, setImages] = useState([]);
|
| 208 |
+
const [error, setError] = useState(null);
|
| 209 |
+
const pollRef = useRef(null);
|
| 210 |
+
const startTimeRef = useRef(null);
|
| 211 |
+
|
| 212 |
+
const pollStatus = useCallback(async (taskId) => {
|
| 213 |
+
try {
|
| 214 |
+
const res = await fetch(`/api/flux2/status/${taskId}`);
|
| 215 |
+
const data = await res.json();
|
| 216 |
+
|
| 217 |
+
if (data.status === "done") {
|
| 218 |
+
setRunning(false);
|
| 219 |
+
setStatus("done");
|
| 220 |
+
setImages(data.images || []);
|
| 221 |
+
clearInterval(pollRef.current);
|
| 222 |
+
return;
|
| 223 |
+
}
|
| 224 |
+
if (data.status === "error") {
|
| 225 |
+
setRunning(false);
|
| 226 |
+
setStatus("error");
|
| 227 |
+
setError(data.error || "Generation failed");
|
| 228 |
+
clearInterval(pollRef.current);
|
| 229 |
+
return;
|
| 230 |
+
}
|
| 231 |
+
if (data.status === "processing") {
|
| 232 |
+
const elapsed = startTimeRef.current
|
| 233 |
+
? Math.floor((Date.now() - startTimeRef.current) / 1000)
|
| 234 |
+
: 0;
|
| 235 |
+
setStatus(`Generating... (${formatTime(elapsed)})`);
|
| 236 |
+
}
|
| 237 |
+
} catch (e) {
|
| 238 |
+
setRunning(false);
|
| 239 |
+
setStatus("error");
|
| 240 |
+
setError(e.message);
|
| 241 |
+
clearInterval(pollRef.current);
|
| 242 |
+
}
|
| 243 |
+
}, []);
|
| 244 |
+
|
| 245 |
+
const handleGenerate = useCallback(async () => {
|
| 246 |
+
if (!prompt.trim()) return;
|
| 247 |
+
|
| 248 |
+
setRunning(true);
|
| 249 |
+
setStatus("Queued...");
|
| 250 |
+
setImages([]);
|
| 251 |
+
setError(null);
|
| 252 |
+
startTimeRef.current = Date.now();
|
| 253 |
+
|
| 254 |
+
try {
|
| 255 |
+
const res = await fetch("/api/flux2/generate", {
|
| 256 |
+
method: "POST",
|
| 257 |
+
headers: { "Content-Type": "application/json" },
|
| 258 |
+
body: JSON.stringify({
|
| 259 |
+
prompt: prompt.trim(),
|
| 260 |
+
negative_prompt: negativePrompt.trim(),
|
| 261 |
+
resolution,
|
| 262 |
+
num_inference_steps: steps,
|
| 263 |
+
guidance_scale: guidance,
|
| 264 |
+
seed,
|
| 265 |
+
batch_size: batch,
|
| 266 |
+
}),
|
| 267 |
+
});
|
| 268 |
+
const data = await res.json();
|
| 269 |
+
|
| 270 |
+
if (!data.task_id) throw new Error(data.error || "No task_id returned");
|
| 271 |
+
|
| 272 |
+
setStatus("Queued...");
|
| 273 |
+
pollRef.current = setInterval(() => pollStatus(data.task_id), 1500);
|
| 274 |
+
pollStatus(data.task_id);
|
| 275 |
+
} catch (e) {
|
| 276 |
+
setRunning(false);
|
| 277 |
+
setStatus("error");
|
| 278 |
+
setError(e.message);
|
| 279 |
+
}
|
| 280 |
+
}, [prompt, negativePrompt, resolution, steps, guidance, seed, batch, pollStatus]);
|
| 281 |
+
|
| 282 |
+
const downloadAll = useCallback(() => {
|
| 283 |
+
images.forEach((img, i) => {
|
| 284 |
+
if (img.url) {
|
| 285 |
+
const a = document.createElement("a");
|
| 286 |
+
a.href = img.url;
|
| 287 |
+
a.download = `flux2_${i + 1}.png`;
|
| 288 |
+
a.click();
|
| 289 |
+
}
|
| 290 |
+
});
|
| 291 |
+
}, [images]);
|
| 292 |
+
|
| 293 |
+
return (
|
| 294 |
+
<div style={styles.container}>
|
| 295 |
+
<div style={styles.header}>
|
| 296 |
+
<div style={styles.headerTitle}>
|
| 297 |
+
<IcBolt size={22} />
|
| 298 |
+
Flux2 Klein 9B
|
| 299 |
+
<span style={styles.headerBadge}>Text-to-Image</span>
|
| 300 |
+
</div>
|
| 301 |
+
<p style={styles.headerSub}>
|
| 302 |
+
Generate images using Flux2 Klein 9B via Wangp API
|
| 303 |
+
</p>
|
| 304 |
+
</div>
|
| 305 |
+
|
| 306 |
+
<div style={styles.card}>
|
| 307 |
+
<div style={{ marginBottom: 16 }}>
|
| 308 |
+
<label style={styles.label}>Prompt</label>
|
| 309 |
+
<textarea
|
| 310 |
+
style={styles.textarea}
|
| 311 |
+
value={prompt}
|
| 312 |
+
onChange={(e) => setPrompt(e.target.value)}
|
| 313 |
+
placeholder="Describe what you want to see..."
|
| 314 |
+
disabled={running}
|
| 315 |
+
/>
|
| 316 |
+
</div>
|
| 317 |
+
|
| 318 |
+
<div>
|
| 319 |
+
<button
|
| 320 |
+
onClick={() => setShowAdvanced(!showAdvanced)}
|
| 321 |
+
style={{
|
| 322 |
+
...styles.detailBtn,
|
| 323 |
+
display: "flex",
|
| 324 |
+
alignItems: "center",
|
| 325 |
+
gap: 6,
|
| 326 |
+
marginBottom: showAdvanced ? 16 : 0,
|
| 327 |
+
}}
|
| 328 |
+
>
|
| 329 |
+
<IcSliders size={14} />
|
| 330 |
+
{showAdvanced ? "Hide" : "Show"} advanced parameters
|
| 331 |
+
</button>
|
| 332 |
+
</div>
|
| 333 |
+
|
| 334 |
+
{showAdvanced && (
|
| 335 |
+
<>
|
| 336 |
+
<div style={{ marginBottom: 16 }}>
|
| 337 |
+
<label style={styles.label}>Negative Prompt</label>
|
| 338 |
+
<textarea
|
| 339 |
+
style={{ ...styles.textarea, minHeight: 60 }}
|
| 340 |
+
value={negativePrompt}
|
| 341 |
+
onChange={(e) => setNegativePrompt(e.target.value)}
|
| 342 |
+
placeholder="What to avoid in the image..."
|
| 343 |
+
disabled={running}
|
| 344 |
+
/>
|
| 345 |
+
</div>
|
| 346 |
+
|
| 347 |
+
<div style={styles.paramGrid}>
|
| 348 |
+
<div>
|
| 349 |
+
<label style={styles.paramLabel}>Resolution</label>
|
| 350 |
+
<select
|
| 351 |
+
style={styles.select}
|
| 352 |
+
value={resolution}
|
| 353 |
+
onChange={(e) => setResolution(e.target.value)}
|
| 354 |
+
disabled={running}
|
| 355 |
+
>
|
| 356 |
+
{RESOLUTIONS.map((r) => (
|
| 357 |
+
<option key={r.value} value={r.value}>
|
| 358 |
+
{r.label}
|
| 359 |
+
</option>
|
| 360 |
+
))}
|
| 361 |
+
</select>
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
<div>
|
| 365 |
+
<label style={styles.paramLabel}>Steps</label>
|
| 366 |
+
<input
|
| 367 |
+
style={styles.input}
|
| 368 |
+
type="number"
|
| 369 |
+
value={steps}
|
| 370 |
+
onChange={(e) => setSteps(Number(e.target.value))}
|
| 371 |
+
min={1}
|
| 372 |
+
max={50}
|
| 373 |
+
disabled={running}
|
| 374 |
+
/>
|
| 375 |
+
</div>
|
| 376 |
+
|
| 377 |
+
<div>
|
| 378 |
+
<label style={styles.paramLabel}>Guidance</label>
|
| 379 |
+
<input
|
| 380 |
+
style={styles.input}
|
| 381 |
+
type="number"
|
| 382 |
+
value={guidance}
|
| 383 |
+
onChange={(e) => setGuidance(Number(e.target.value))}
|
| 384 |
+
min={1}
|
| 385 |
+
max={20}
|
| 386 |
+
step={0.5}
|
| 387 |
+
disabled={running}
|
| 388 |
+
/>
|
| 389 |
+
</div>
|
| 390 |
+
|
| 391 |
+
<div>
|
| 392 |
+
<label style={styles.paramLabel}>Seed (-1 = random)</label>
|
| 393 |
+
<input
|
| 394 |
+
style={styles.input}
|
| 395 |
+
type="number"
|
| 396 |
+
value={seed}
|
| 397 |
+
onChange={(e) => setSeed(Number(e.target.value))}
|
| 398 |
+
min={-1}
|
| 399 |
+
disabled={running}
|
| 400 |
+
/>
|
| 401 |
+
</div>
|
| 402 |
+
|
| 403 |
+
<div>
|
| 404 |
+
<label style={styles.paramLabel}>Batch Size</label>
|
| 405 |
+
<input
|
| 406 |
+
style={styles.input}
|
| 407 |
+
type="number"
|
| 408 |
+
value={batch}
|
| 409 |
+
onChange={(e) => setBatch(Number(e.target.value))}
|
| 410 |
+
min={1}
|
| 411 |
+
max={4}
|
| 412 |
+
disabled={running}
|
| 413 |
+
/>
|
| 414 |
+
</div>
|
| 415 |
+
</div>
|
| 416 |
+
</>
|
| 417 |
+
)}
|
| 418 |
+
|
| 419 |
+
<button
|
| 420 |
+
style={{
|
| 421 |
+
...styles.generateBtn,
|
| 422 |
+
opacity: running || !prompt.trim() ? 0.6 : 1,
|
| 423 |
+
cursor:
|
| 424 |
+
running || !prompt.trim() ? "not-allowed" : "pointer",
|
| 425 |
+
}}
|
| 426 |
+
onClick={handleGenerate}
|
| 427 |
+
disabled={running || !prompt.trim()}
|
| 428 |
+
>
|
| 429 |
+
{running ? (
|
| 430 |
+
<>
|
| 431 |
+
<div style={styles.spinner} />
|
| 432 |
+
Generating...
|
| 433 |
+
</>
|
| 434 |
+
) : (
|
| 435 |
+
<>
|
| 436 |
+
<IcBolt size={18} />
|
| 437 |
+
Generate
|
| 438 |
+
</>
|
| 439 |
+
)}
|
| 440 |
+
</button>
|
| 441 |
+
</div>
|
| 442 |
+
|
| 443 |
+
{status && !error && !images.length && (
|
| 444 |
+
<div
|
| 445 |
+
style={{
|
| 446 |
+
...styles.statusBox,
|
| 447 |
+
background: "var(--orange-tint)",
|
| 448 |
+
border: "1px solid #f6d9c2",
|
| 449 |
+
color: "var(--ink-2)",
|
| 450 |
+
}}
|
| 451 |
+
>
|
| 452 |
+
<div style={styles.spinner} />
|
| 453 |
+
{status}
|
| 454 |
+
</div>
|
| 455 |
+
)}
|
| 456 |
+
|
| 457 |
+
{error && (
|
| 458 |
+
<div
|
| 459 |
+
style={{
|
| 460 |
+
...styles.statusBox,
|
| 461 |
+
background: "var(--red-soft)",
|
| 462 |
+
border: "1px solid #f5c6c6",
|
| 463 |
+
color: "var(--red)",
|
| 464 |
+
}}
|
| 465 |
+
>
|
| 466 |
+
<span>✕</span>
|
| 467 |
+
{error}
|
| 468 |
+
</div>
|
| 469 |
+
)}
|
| 470 |
+
|
| 471 |
+
{images.length > 0 && (
|
| 472 |
+
<div>
|
| 473 |
+
<div
|
| 474 |
+
style={{
|
| 475 |
+
display: "flex",
|
| 476 |
+
justifyContent: "space-between",
|
| 477 |
+
alignItems: "center",
|
| 478 |
+
marginBottom: 14,
|
| 479 |
+
}}
|
| 480 |
+
>
|
| 481 |
+
<div className="section-title">
|
| 482 |
+
Results ({images.length})
|
| 483 |
+
</div>
|
| 484 |
+
<button
|
| 485 |
+
style={styles.downloadBtn}
|
| 486 |
+
onClick={downloadAll}
|
| 487 |
+
>
|
| 488 |
+
<IcDl size={14} />
|
| 489 |
+
Download All
|
| 490 |
+
</button>
|
| 491 |
+
</div>
|
| 492 |
+
|
| 493 |
+
<div style={styles.gallery}>
|
| 494 |
+
{images.map((img, i) =>
|
| 495 |
+
img.error ? (
|
| 496 |
+
<div key={i} style={styles.imgCard}>
|
| 497 |
+
<div
|
| 498 |
+
style={{
|
| 499 |
+
padding: 40,
|
| 500 |
+
textAlign: "center",
|
| 501 |
+
color: "var(--red)",
|
| 502 |
+
fontSize: 13,
|
| 503 |
+
}}
|
| 504 |
+
>
|
| 505 |
+
Failed: {img.error}
|
| 506 |
+
</div>
|
| 507 |
+
</div>
|
| 508 |
+
) : (
|
| 509 |
+
<div key={i} style={styles.imgCard}>
|
| 510 |
+
<img
|
| 511 |
+
src={img.url}
|
| 512 |
+
alt={`Generated ${i + 1}`}
|
| 513 |
+
style={styles.img}
|
| 514 |
+
loading="lazy"
|
| 515 |
+
/>
|
| 516 |
+
<button
|
| 517 |
+
style={styles.downloadBtn}
|
| 518 |
+
onClick={() => {
|
| 519 |
+
const a = document.createElement("a");
|
| 520 |
+
a.href = img.url;
|
| 521 |
+
a.download = `flux2_${i + 1}.png`;
|
| 522 |
+
a.click();
|
| 523 |
+
}}
|
| 524 |
+
>
|
| 525 |
+
<IcDl size={14} />
|
| 526 |
+
Download
|
| 527 |
+
</button>
|
| 528 |
+
</div>
|
| 529 |
+
)
|
| 530 |
+
)}
|
| 531 |
+
</div>
|
| 532 |
+
</div>
|
| 533 |
+
)}
|
| 534 |
+
</div>
|
| 535 |
+
);
|
| 536 |
+
}
|