Spaces:
Running
Running
| import os | |
| import base64 | |
| import io | |
| import json | |
| import asyncio | |
| import threading | |
| import gradio as gr | |
| import httpx | |
| import numpy as np | |
| from PIL import Image | |
| # ================= CONFIG ================= | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| HF_API_URL = "https://api-inference.huggingface.co" | |
| # ================= MODEL CONFIG ================= | |
| DEFAULT_TEXT_MODEL = "Qwen/Qwen2.5-72B-Instruct" | |
| FALLBACK_TEXT_MODEL = "Qwen/Qwen3-0.6B" | |
| DEFAULT_VISION_MODEL = "Qwen/Qwen2.5-VL-7B-Instruct" | |
| FALLBACK_VISION_MODEL = "llava-hf/llava-1.5-7b-hf" | |
| DEFAULT_IMAGE_MODEL = "stabilityai/stable-diffusion-xl-base-1.0" | |
| FALLBACK_IMAGE_MODEL = "runwayml/stable-diffusion-v1-5" | |
| # ================= UTILS ================= | |
| def get_auth_headers(): | |
| token = HF_TOKEN | |
| if not token: | |
| return {} | |
| return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} | |
| def _run_async(coro): | |
| """Run async coroutine safely, handling nested event loops.""" | |
| try: | |
| loop = asyncio.get_running_loop() | |
| except RuntimeError: | |
| return asyncio.run(coro) | |
| result = [None] | |
| def _worker(): | |
| new_loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(new_loop) | |
| try: | |
| result[0] = new_loop.run_until_complete(coro) | |
| finally: | |
| new_loop.close() | |
| t = threading.Thread(target=_worker) | |
| t.start() | |
| t.join() | |
| return result[0] | |
| # ================= HF INFERENCE API HELPERS ================= | |
| async def _hf_text_generation(prompt, model=DEFAULT_TEXT_MODEL, max_tokens=2048, temperature=0.7, system_prompt=""): | |
| headers = get_auth_headers() | |
| if not headers: | |
| return "❌ LỖI: HF_TOKEN chưa được cấu hình. Vui lòng vào Space Settings → Secrets và thêm HF_TOKEN." | |
| messages = [] | |
| if system_prompt: | |
| messages.append({"role": "system", "content": system_prompt}) | |
| messages.append({"role": "user", "content": prompt}) | |
| payload = { | |
| "model": model, | |
| "messages": messages, | |
| "max_tokens": max_tokens, | |
| "temperature": temperature, | |
| "stream": False | |
| } | |
| async with httpx.AsyncClient(timeout=120.0) as client: | |
| response = await client.post( | |
| f"{HF_API_URL}/models/{model}/v1/chat/completions", | |
| headers=headers, | |
| json=payload, | |
| timeout=120 | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| return data["choices"][0]["message"]["content"] | |
| elif response.status_code in (404, 503) and model != FALLBACK_TEXT_MODEL: | |
| return await _hf_text_generation(prompt, FALLBACK_TEXT_MODEL, max_tokens, temperature, system_prompt) | |
| else: | |
| return f"❌ LỖI HF API ({response.status_code}): {response.text[:500]}" | |
| async def _hf_image_generation(prompt, model=DEFAULT_IMAGE_MODEL, width=1024, height=1024, negative_prompt="", seed=None): | |
| headers = get_auth_headers() | |
| if not headers: | |
| return "❌ LỖI: HF_TOKEN chưa được cấu hình. Vui lòng vào Space Settings → Secrets và thêm HF_TOKEN." | |
| payload = { | |
| "inputs": prompt, | |
| "parameters": { | |
| "negative_prompt": negative_prompt or "blurry, low quality, watermark, text, signature, ugly, deformed", | |
| "width": width, | |
| "height": height, | |
| "guidance_scale": 7.5, | |
| "num_inference_steps": 50 | |
| } | |
| } | |
| if seed is not None: | |
| payload["parameters"]["seed"] = seed | |
| async with httpx.AsyncClient(timeout=120.0) as client: | |
| response = await client.post( | |
| f"{HF_API_URL}/models/{model}", | |
| headers=headers, | |
| json=payload, | |
| timeout=120 | |
| ) | |
| if response.status_code == 200: | |
| try: | |
| img = Image.open(io.BytesIO(response.content)) | |
| return img | |
| except Exception as e: | |
| return f"❌ Lỗi decode ảnh: {e}" | |
| elif response.status_code in (404, 503) and model != FALLBACK_IMAGE_MODEL: | |
| return await _hf_image_generation(prompt, FALLBACK_IMAGE_MODEL, min(width, 512), min(height, 512), negative_prompt, seed) | |
| else: | |
| return f"❌ LỖI HF Image API ({response.status_code}): {response.text[:500]}" | |
| async def _hf_vision_chat(messages, model=DEFAULT_VISION_MODEL, max_tokens=1024, temperature=0.3): | |
| headers = get_auth_headers() | |
| if not headers: | |
| return "❌ LỖI: HF_TOKEN chưa được cấu hình. Vui lòng vào Space Settings → Secrets và thêm HF_TOKEN." | |
| payload = { | |
| "model": model, | |
| "messages": messages, | |
| "max_tokens": max_tokens, | |
| "temperature": temperature, | |
| "stream": False | |
| } | |
| async with httpx.AsyncClient(timeout=120.0) as client: | |
| response = await client.post( | |
| f"{HF_API_URL}/models/{model}/v1/chat/completions", | |
| headers=headers, | |
| json=payload, | |
| timeout=120 | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| return data["choices"][0]["message"]["content"] | |
| elif response.status_code in (404, 503) and model != FALLBACK_VISION_MODEL: | |
| return await _hf_vision_chat(messages, FALLBACK_VISION_MODEL, max_tokens, temperature) | |
| else: | |
| return f"❌ LỖI HF Vision API ({response.status_code}): {response.text[:500]}" | |
| # ================= SYNC WRAPPERS ================= | |
| def sync_text_gen(prompt, model, max_tokens, temperature, system_prompt): | |
| if not HF_TOKEN: | |
| return "❌ LỖI: HF_TOKEN chưa được cấu hình. Vui lòng vào Space Settings → Secrets và thêm HF_TOKEN (token HuggingFace của bạn)." | |
| return _run_async(_hf_text_generation(prompt, model, max_tokens, temperature, system_prompt)) | |
| def sync_image_gen(prompt, model, width, height, negative_prompt, seed): | |
| if not HF_TOKEN: | |
| return "❌ LỖI: HF_TOKEN chưa được cấu hình. Vui lòng vào Space Settings → Secrets và thêm HF_TOKEN (token HuggingFace của bạn)." | |
| result = _run_async(_hf_image_generation(prompt, model, width, height, negative_prompt, seed)) | |
| return result | |
| def sync_vision_chat(messages_json, model, max_tokens, temperature): | |
| if not HF_TOKEN: | |
| return "❌ LỖI: HF_TOKEN chưa được cấu hình. Vui lòng vào Space Settings → Secrets và thêm HF_TOKEN (token HuggingFace của bạn)." | |
| messages = json.loads(messages_json) | |
| return _run_async(_hf_vision_chat(messages, model, max_tokens, temperature)) | |
| # ================= GRADIO UI ================= | |
| with gr.Blocks(title="Comic AI Generator") as demo: | |
| gr.Markdown("# 🔥 Comic AI Generator - Tạo Truyện Tranh Bằng AI") | |
| gr.Markdown("Sử dụng Hugging Face Inference API để tạo văn bản, hình ảnh, và phân tích ảnh.") | |
| if not HF_TOKEN: | |
| gr.Markdown( | |
| "⚠️ **Cảnh báo**: HF_TOKEN chưa được cấu hình. " | |
| "Vui lòng vào [Space Settings → Secrets](https://huggingface.co/spaces/bep40/comic-ai-generator/settings/secrets) " | |
| "và thêm secret `HF_TOKEN` với giá trị là token HuggingFace của bạn." | |
| ) | |
| with gr.Tab("📝 Tạo Văn Bản / Cốt Truyện"): | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| text_prompt = gr.Textbox(label="Prompt", lines=4, placeholder="Nhập ý tưởng cốt truyện hoặc yêu cầu văn bản...") | |
| text_model = gr.Textbox(label="Model", value=DEFAULT_TEXT_MODEL) | |
| text_system = gr.Textbox(label="System Prompt (tùy chọn)", lines=2, placeholder="Bạn là một tác giả truyện tranh chuyên nghiệp...") | |
| with gr.Column(scale=1): | |
| text_max_tokens = gr.Slider(label="Max Tokens", minimum=64, maximum=4096, value=2048, step=64) | |
| text_temperature = gr.Slider(label="Temperature", minimum=0.1, maximum=1.5, value=0.7, step=0.1) | |
| text_gen_btn = gr.Button("🚀 Tạo Văn Bản", variant="primary") | |
| text_output = gr.Textbox(label="Kết quả", lines=12) | |
| text_gen_btn.click( | |
| fn=sync_text_gen, | |
| inputs=[text_prompt, text_model, text_max_tokens, text_temperature, text_system], | |
| outputs=text_output | |
| ) | |
| with gr.Tab("🎨 Tạo Hình Ảnh"): | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| img_prompt = gr.Textbox(label="Prompt", lines=3, placeholder="Mô tả hình ảnh bạn muốn tạo...") | |
| img_neg_prompt = gr.Textbox(label="Negative Prompt", lines=2, value="blurry, low quality, watermark, text, signature, ugly, deformed") | |
| with gr.Column(scale=1): | |
| img_model = gr.Textbox(label="Model", value=DEFAULT_IMAGE_MODEL) | |
| img_width = gr.Slider(label="Width", minimum=256, maximum=1024, value=1024, step=64) | |
| img_height = gr.Slider(label="Height", minimum=256, maximum=1024, value=1024, step=64) | |
| img_seed = gr.Number(label="Seed (tùy chọn)", value=None, precision=0) | |
| img_gen_btn = gr.Button("🎨 Tạo Hình Ảnh", variant="primary") | |
| img_output = gr.Image(label="Ảnh tạo ra", type="pil") | |
| img_gen_btn.click( | |
| fn=sync_image_gen, | |
| inputs=[img_prompt, img_model, img_width, img_height, img_neg_prompt, img_seed], | |
| outputs=img_output | |
| ) | |
| with gr.Tab("👁️ Phân Tích Ảnh (Vision)"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| vision_image = gr.Image(label="Upload ảnh", type="pil") | |
| vision_model = gr.Textbox(label="Vision Model", value=DEFAULT_VISION_MODEL) | |
| vision_task = gr.Dropdown( | |
| label="Tác vụ", | |
| choices=["detect_characters", "detect_items", "extract_setting", "custom"], | |
| value="detect_characters" | |
| ) | |
| vision_custom = gr.Textbox(label="Câu hỏi tùy chỉnh", lines=2, visible=False) | |
| vision_max_tokens = gr.Slider(label="Max Tokens", minimum=64, maximum=2048, value=1024, step=64) | |
| vision_temperature = gr.Slider(label="Temperature", minimum=0.1, maximum=1.0, value=0.3, step=0.1) | |
| vision_btn = gr.Button("🔍 Phân Tích", variant="primary") | |
| with gr.Column(scale=2): | |
| vision_output = gr.Textbox(label="Kết quả phân tích", lines=12) | |
| def update_vision_visibility(task): | |
| return gr.update(visible=(task == "custom")) | |
| vision_task.change(fn=update_vision_visibility, inputs=vision_task, outputs=vision_custom) | |
| def run_vision(image, task, custom_prompt, model, max_tokens, temperature): | |
| if image is None: | |
| return "Vui lòng upload ảnh trước." | |
| buf = io.BytesIO() | |
| image.save(buf, format="PNG") | |
| b64 = base64.b64encode(buf.getvalue()).decode() | |
| prompts = { | |
| "detect_characters": "Analyze this image carefully. Identify all main characters/people. For each, return JSON with: name (Vietnamese), physical description, gender, age_category. Return ONLY a JSON array.", | |
| "detect_items": "Analyze this image. Identify all distinct objects, accessories, props. For each, return JSON with: vi_name (Vietnamese), en_desc (English description). Return ONLY a JSON array.", | |
| "extract_setting": "Describe the background/setting of this image in detail. Return JSON with: setting (string), isPlainBackground (boolean), key_products (array). Return ONLY a JSON object.", | |
| "custom": custom_prompt | |
| } | |
| task_prompt = prompts.get(task, custom_prompt) | |
| sys_prompt = "You are an image analysis assistant. Always respond with valid JSON only. No markdown, no explanation, no code fences." | |
| messages = [{ | |
| "role": "user", | |
| "content": [ | |
| {"type": "text", "text": f"{sys_prompt}\n\n{task_prompt}\n\nIMPORTANT: Return ONLY valid JSON."}, | |
| {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}} | |
| ] | |
| }] | |
| return sync_vision_chat(json.dumps(messages), model, max_tokens, temperature) | |
| vision_btn.click( | |
| fn=run_vision, | |
| inputs=[vision_image, vision_task, vision_custom, vision_model, vision_max_tokens, vision_temperature], | |
| outputs=vision_output | |
| ) | |
| with gr.Tab("⚙️ Kiểm Tra API"): | |
| api_status = gr.JSON(label="Trạng thái API", value={ | |
| "hf_token_configured": bool(HF_TOKEN), | |
| "hf_token_length": len(HF_TOKEN), | |
| "text_model": DEFAULT_TEXT_MODEL, | |
| "vision_model": DEFAULT_VISION_MODEL, | |
| "image_model": DEFAULT_IMAGE_MODEL, | |
| "message": "Kiểm tra xem HF_TOKEN đã được cấu hình trong Space Settings > Secrets chưa." | |
| }) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False) | |