Download src/inference/api.py from TAI-Research/Lumina_Dev_Legacy: direct link, hf CLI and curl.
- Browser
- Download file 21.8 kB
-
https://huggingface.co/TAI-Research/Lumina_Dev_Legacy/resolve/main/src/inference/api.py
- Command line
-
hf download hf://TAI-Research/Lumina_Dev_Legacy/src/inference/api.py
-
curl -L -o api.py https://huggingface.co/TAI-Research/Lumina_Dev_Legacy/resolve/main/src/inference/api.py
21.8 kB
| from fastapi import FastAPI, UploadFile, File, Form, HTTPException | |
| from fastapi.responses import JSONResponse, FileResponse, StreamingResponse | |
| from pydantic import BaseModel | |
| from typing import Optional, List, Dict, Any | |
| import torch | |
| import io | |
| from PIL import Image | |
| import base64 | |
| import json | |
| import time | |
| from datetime import datetime | |
| import asyncio | |
| from concurrent.futures import ThreadPoolExecutor | |
| import uuid | |
| import os | |
| from .sampler import TextToImagePipeline, SamplerFactory | |
| from ..data.text_encoder import CLIPTextEncoderWrapper | |
| # 请求/响应模型 | |
| class TextToImageRequest(BaseModel): | |
| prompt: str | |
| negative_prompt: Optional[str] = "" | |
| width: int = 512 | |
| height: int = 512 | |
| num_steps: int = 50 | |
| guidance_scale: float = 7.5 | |
| num_images: int = 1 | |
| seed: Optional[int] = None | |
| sampler: str = "ddim" | |
| class ImageResponse(BaseModel): | |
| images: List[str] # base64编码的图像 | |
| metadata: Dict[str, Any] | |
| request_id: str | |
| generation_time: float | |
| class BatchRequest(BaseModel): | |
| requests: List[TextToImageRequest] | |
| priority: int = 0 | |
| class StatusResponse(BaseModel): | |
| status: str | |
| queue_length: int | |
| active_tasks: int | |
| gpu_memory_usage: float | |
| uptime: float | |
| # API应用 | |
| class LuminaAPI: | |
| """Lumina API服务器""" | |
| def __init__( | |
| self, | |
| model, | |
| diffusion, | |
| text_encoder, | |
| vae_decoder=None, | |
| host: str = "0.0.0.0", | |
| port: int = 8000, | |
| max_queue_size: int = 100, | |
| max_workers: int = 2 | |
| ): | |
| self.model = model | |
| self.diffusion = diffusion | |
| self.text_encoder = text_encoder | |
| self.vae_decoder = vae_decoder | |
| self.host = host | |
| self.port = port | |
| # 创建FastAPI应用 | |
| self.app = FastAPI( | |
| title="Lumina Image Generation API", | |
| description="轻量级图像生成模型API", | |
| version="1.0.0" | |
| ) | |
| # 任务队列 | |
| self.task_queue = asyncio.Queue(maxsize=max_queue_size) | |
| self.executor = ThreadPoolExecutor(max_workers=max_workers) | |
| self.active_tasks = 0 | |
| # 请求历史 | |
| self.request_history = [] | |
| self.max_history = 1000 | |
| # 统计信息 | |
| self.start_time = time.time() | |
| self.total_requests = 0 | |
| self.total_images = 0 | |
| # 初始化管道 | |
| self.pipeline = TextToImagePipeline( | |
| model=model, | |
| diffusion=diffusion, | |
| text_encoder=text_encoder, | |
| vae_decoder=vae_decoder, | |
| sampler_type="ddim" | |
| ) | |
| # 设置路由 | |
| self._setup_routes() | |
| def _setup_routes(self): | |
| """设置API路由""" | |
| async def root(): | |
| return { | |
| "message": "Lumina Image Generation API", | |
| "version": "1.0.0", | |
| "docs": "/docs", | |
| "endpoints": [ | |
| "/generate", | |
| "/batch_generate", | |
| "/status", | |
| "/health" | |
| ] | |
| } | |
| async def health_check(): | |
| """健康检查""" | |
| gpu_available = torch.cuda.is_available() | |
| gpu_memory = torch.cuda.memory_allocated() / 1024**3 if gpu_available else 0 | |
| return { | |
| "status": "healthy", | |
| "gpu_available": gpu_available, | |
| "gpu_memory_gb": gpu_memory, | |
| "model_loaded": self.model is not None, | |
| "text_encoder_loaded": self.text_encoder is not None | |
| } | |
| async def get_status(): | |
| """获取服务状态""" | |
| uptime = time.time() - self.start_time | |
| # GPU内存使用 | |
| if torch.cuda.is_available(): | |
| gpu_memory = torch.cuda.memory_allocated() / 1024**3 | |
| else: | |
| gpu_memory = 0 | |
| return StatusResponse( | |
| status="running", | |
| queue_length=self.task_queue.qsize(), | |
| active_tasks=self.active_tasks, | |
| gpu_memory_usage=gpu_memory, | |
| uptime=uptime | |
| ) | |
| async def generate_image(request: TextToImageRequest): | |
| """生成单个图像""" | |
| request_id = str(uuid.uuid4()) | |
| # 记录请求 | |
| self.request_history.append({ | |
| "request_id": request_id, | |
| "prompt": request.prompt, | |
| "timestamp": datetime.now().isoformat() | |
| }) | |
| # 限制历史记录大小 | |
| if len(self.request_history) > self.max_history: | |
| self.request_history = self.request_history[-self.max_history:] | |
| # 生成图像 | |
| start_time = time.time() | |
| try: | |
| # 在线程池中运行生成任务 | |
| loop = asyncio.get_event_loop() | |
| images = await loop.run_in_executor( | |
| self.executor, | |
| self._generate_sync, | |
| request | |
| ) | |
| generation_time = time.time() - start_time | |
| # 转换为base64 | |
| image_b64_list = [] | |
| for img_tensor in images: | |
| if isinstance(img_tensor, torch.Tensor): | |
| # 转换为PIL图像 | |
| if img_tensor.dim() == 4: | |
| img_tensor = img_tensor.squeeze(0) | |
| # 归一化到[0, 255] | |
| img_tensor = torch.clamp(img_tensor * 255, 0, 255).byte() | |
| # 转换为numpy数组 | |
| if img_tensor.shape[0] == 3: # CHW格式 | |
| img_array = img_tensor.permute(1, 2, 0).cpu().numpy() | |
| else: | |
| img_array = img_tensor.cpu().numpy() | |
| # 转换为PIL图像 | |
| img = Image.fromarray(img_array) | |
| # 转换为base64 | |
| buffered = io.BytesIO() | |
| img.save(buffered, format="PNG") | |
| img_b64 = base64.b64encode(buffered.getvalue()).decode() | |
| image_b64_list.append(img_b64) | |
| else: | |
| image_b64_list.append("") | |
| # 更新统计信息 | |
| self.total_requests += 1 | |
| self.total_images += len(images) | |
| return ImageResponse( | |
| images=image_b64_list, | |
| metadata={ | |
| "prompt": request.prompt, | |
| "negative_prompt": request.negative_prompt, | |
| "width": request.width, | |
| "height": request.height, | |
| "num_steps": request.num_steps, | |
| "guidance_scale": request.guidance_scale, | |
| "seed": request.seed, | |
| "sampler": request.sampler | |
| }, | |
| request_id=request_id, | |
| generation_time=generation_time | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def batch_generate(batch_request: BatchRequest): | |
| """批量生成图像""" | |
| request_ids = [str(uuid.uuid4()) for _ in batch_request.requests] | |
| results = [] | |
| # 为每个请求生成任务 | |
| tasks = [] | |
| for req, req_id in zip(batch_request.requests, request_ids): | |
| task = asyncio.create_task(self._process_single_request(req, req_id)) | |
| tasks.append(task) | |
| # 等待所有任务完成 | |
| results = await asyncio.gather(*tasks, return_exceptions=True) | |
| # 处理结果 | |
| successful_results = [] | |
| failed_results = [] | |
| for result in results: | |
| if isinstance(result, Exception): | |
| failed_results.append({"error": str(result)}) | |
| else: | |
| successful_results.append(result) | |
| return { | |
| "successful": successful_results, | |
| "failed": failed_results, | |
| "total_requests": len(batch_request.requests), | |
| "successful_count": len(successful_results), | |
| "failed_count": len(failed_results) | |
| } | |
| async def get_history(limit: int = 50): | |
| """获取请求历史""" | |
| return self.request_history[-limit:] | |
| # Stable Diffusion兼容端点 | |
| async def txt2img( | |
| prompt: str = Form(...), | |
| negative_prompt: str = Form(""), | |
| width: int = Form(512), | |
| height: int = Form(512), | |
| steps: int = Form(50), | |
| cfg_scale: float = Form(7.5), | |
| seed: int = Form(-1) | |
| ): | |
| """兼容Stable Diffusion WebUI的端点""" | |
| request = TextToImageRequest( | |
| prompt=prompt, | |
| negative_prompt=negative_prompt, | |
| width=width, | |
| height=height, | |
| num_steps=steps, | |
| guidance_scale=cfg_scale, | |
| seed=seed if seed != -1 else None | |
| ) | |
| response = await generate_image(request) | |
| # 返回第一个图像 | |
| if response.images: | |
| return StreamingResponse( | |
| io.BytesIO(base64.b64decode(response.images[0])), | |
| media_type="image/png" | |
| ) | |
| else: | |
| raise HTTPException(status_code=500, detail="生成失败") | |
| async def get_stats(): | |
| """获取统计信息""" | |
| uptime = time.time() - self.start_time | |
| return { | |
| "total_requests": self.total_requests, | |
| "total_images": self.total_images, | |
| "requests_per_minute": self.total_requests / (uptime / 60) if uptime > 0 else 0, | |
| "avg_generation_time": None, # 可以添加计时逻辑 | |
| "uptime_seconds": uptime, | |
| "queue_size": self.task_queue.qsize(), | |
| "active_workers": self.active_tasks | |
| } | |
| def _generate_sync(self, request: TextToImageRequest) -> List[torch.Tensor]: | |
| """同步生成图像(在单独的线程中运行)""" | |
| # 更新管道采样器 | |
| self.pipeline.sampler = SamplerFactory.create_sampler( | |
| request.sampler, | |
| self.model, | |
| self.diffusion, | |
| request.num_steps | |
| ) | |
| # 生成图像 | |
| images = self.pipeline( | |
| prompt=request.prompt, | |
| negative_prompt=request.negative_prompt, | |
| height=request.height, | |
| width=request.width, | |
| num_inference_steps=request.num_steps, | |
| guidance_scale=request.guidance_scale, | |
| num_images=request.num_images, | |
| seed=request.seed, | |
| progress_bar=False | |
| ) | |
| return images | |
| async def _process_single_request(self, request: TextToImageRequest, request_id: str) -> Dict: | |
| """处理单个请求""" | |
| try: | |
| # 在队列中添加任务 | |
| await self.task_queue.put((request, request_id)) | |
| # 更新活动任务计数 | |
| self.active_tasks += 1 | |
| # 处理任务 | |
| loop = asyncio.get_event_loop() | |
| images = await loop.run_in_executor( | |
| self.executor, | |
| self._generate_sync, | |
| request | |
| ) | |
| # 转换图像 | |
| image_b64_list = [] | |
| for img_tensor in images: | |
| # 简化的转换逻辑 | |
| img_b64 = "placeholder" # 实际应该转换为base64 | |
| image_b64_list.append(img_b64) | |
| # 更新活动任务计数 | |
| self.active_tasks -= 1 | |
| return { | |
| "request_id": request_id, | |
| "images": image_b64_list, | |
| "success": True | |
| } | |
| except Exception as e: | |
| self.active_tasks -= 1 | |
| return { | |
| "request_id": request_id, | |
| "error": str(e), | |
| "success": False | |
| } | |
| def run(self): | |
| """运行API服务器""" | |
| import uvicorn | |
| print(f"启动Lumina API服务器在 http://{self.host}:{self.port}") | |
| print(f"API文档: http://{self.host}:{self.port}/docs") | |
| uvicorn.run( | |
| self.app, | |
| host=self.host, | |
| port=self.port, | |
| log_level="info" | |
| ) | |
| class SimpleWebUI: | |
| """简单的Web UI(使用Gradio)""" | |
| def __init__(self, pipeline: TextToImagePipeline): | |
| self.pipeline = pipeline | |
| def create_interface(self): | |
| """创建Gradio界面""" | |
| try: | |
| import gradio as gr | |
| except ImportError: | |
| print("Gradio未安装,无法创建Web UI") | |
| return None | |
| def generate_image_ui( | |
| prompt, | |
| negative_prompt, | |
| width, | |
| height, | |
| num_steps, | |
| guidance_scale, | |
| seed, | |
| sampler | |
| ): | |
| """UI生成函数""" | |
| # 设置种子 | |
| if seed and seed > 0: | |
| torch.manual_seed(seed) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed(seed) | |
| # 生成图像 | |
| images = self.pipeline( | |
| prompt=prompt, | |
| negative_prompt=negative_prompt, | |
| height=height, | |
| width=width, | |
| num_inference_steps=num_steps, | |
| guidance_scale=guidance_scale, | |
| num_images=1, | |
| seed=seed if seed > 0 else None, | |
| progress_bar=False | |
| ) | |
| # 转换为PIL图像 | |
| if images: | |
| img_tensor = images[0] | |
| if isinstance(img_tensor, torch.Tensor): | |
| if img_tensor.dim() == 4: | |
| img_tensor = img_tensor.squeeze(0) | |
| # 归一化到[0, 255] | |
| img_tensor = torch.clamp(img_tensor * 255, 0, 255).byte() | |
| # 转换为numpy数组 | |
| if img_tensor.shape[0] == 3: # CHW格式 | |
| img_array = img_tensor.permute(1, 2, 0).cpu().numpy() | |
| else: | |
| img_array = img_tensor.cpu().numpy() | |
| # 转换为PIL图像 | |
| from PIL import Image | |
| img = Image.fromarray(img_array) | |
| return img | |
| return None | |
| # 创建界面 | |
| with gr.Blocks(title="Lumina Image Generator") as interface: | |
| gr.Markdown("# 🎨 Lumina - 轻量级图像生成") | |
| gr.Markdown("基于扩散模型的文本到图像生成系统") | |
| with gr.Row(): | |
| with gr.Column(): | |
| prompt = gr.Textbox( | |
| label="提示词", | |
| placeholder="输入描述图像的文本...", | |
| lines=3 | |
| ) | |
| negative_prompt = gr.Textbox( | |
| label="负面提示词", | |
| placeholder="不想在图像中出现的内容...", | |
| lines=2 | |
| ) | |
| with gr.Row(): | |
| width = gr.Slider( | |
| minimum=256, | |
| maximum=1024, | |
| value=512, | |
| step=64, | |
| label="宽度" | |
| ) | |
| height = gr.Slider( | |
| minimum=256, | |
| maximum=1024, | |
| value=512, | |
| step=64, | |
| label="高度" | |
| ) | |
| with gr.Row(): | |
| num_steps = gr.Slider( | |
| minimum=1, | |
| maximum=100, | |
| value=30, | |
| step=1, | |
| label="采样步数" | |
| ) | |
| guidance_scale = gr.Slider( | |
| minimum=1.0, | |
| maximum=20.0, | |
| value=7.5, | |
| step=0.5, | |
| label="引导强度" | |
| ) | |
| with gr.Row(): | |
| seed = gr.Number( | |
| value=-1, | |
| label="随机种子 (-1为随机)" | |
| ) | |
| sampler = gr.Dropdown( | |
| choices=["ddim", "dpm", "lcm"], | |
| value="ddim", | |
| label="采样器" | |
| ) | |
| generate_btn = gr.Button("生成图像", variant="primary") | |
| with gr.Column(): | |
| output_image = gr.Image( | |
| label="生成的图像", | |
| type="pil" | |
| ) | |
| # 示例 | |
| gr.Markdown("### 示例提示词") | |
| examples = gr.Examples( | |
| examples=[ | |
| ["A beautiful sunset over mountains, digital art", "", 512, 512, 30, 7.5, -1], | |
| ["A cute cat playing with a ball of yarn", "blurry, deformed", 512, 512, 25, 8.0, -1], | |
| ["An astronaut riding a horse on Mars", "cartoon, anime", 512, 512, 40, 7.0, -1] | |
| ], | |
| inputs=[prompt, negative_prompt, width, height, num_steps, guidance_scale, seed] | |
| ) | |
| # 事件处理 | |
| generate_btn.click( | |
| fn=generate_image_ui, | |
| inputs=[prompt, negative_prompt, width, height, num_steps, guidance_scale, seed, sampler], | |
| outputs=output_image | |
| ) | |
| return interface | |
| def launch(self, share: bool = False, server_name: str = "0.0.0.0", server_port: int = 7860): | |
| """启动Web UI""" | |
| interface = self.create_interface() | |
| if interface: | |
| interface.launch( | |
| share=share, | |
| server_name=server_name, | |
| server_port=server_port | |
| ) | |
| def create_api_server(config: dict, model, diffusion, text_encoder, vae_decoder=None): | |
| """创建API服务器""" | |
| # 确定主机和端口 | |
| host = config.get('host', '0.0.0.0') | |
| port = config.get('port', 8000) | |
| # 创建API服务器 | |
| api_server = LuminaAPI( | |
| model=model, | |
| diffusion=diffusion, | |
| text_encoder=text_encoder, | |
| vae_decoder=vae_decoder, | |
| host=host, | |
| port=port, | |
| max_queue_size=config.get('max_queue_size', 100), | |
| max_workers=config.get('max_workers', 2) | |
| ) | |
| return api_server | |
| def test_api(): | |
| """测试API""" | |
| import torch.nn as nn | |
| # 创建模拟组件 | |
| class MockModel(nn.Module): | |
| def forward(self, x, t, context): | |
| return torch.randn_like(x) | |
| class MockDiffusion: | |
| pass | |
| class MockTextEncoder: | |
| def encode(self, texts): | |
| return torch.randn(len(texts), 77, 768) | |
| model = MockModel() | |
| diffusion = MockDiffusion() | |
| text_encoder = MockTextEncoder() | |
| # 创建API服务器 | |
| api = LuminaAPI( | |
| model=model, | |
| diffusion=diffusion, | |
| text_encoder=text_encoder | |
| ) | |
| print("API服务器创建成功") | |
| print("端点:") | |
| print(" POST /generate - 生成图像") | |
| print(" GET /health - 健康检查") | |
| print(" GET /status - 状态信息") | |
| return api | |
| if __name__ == '__main__': | |
| # 测试API | |
| api = test_api() | |
| # 注意:实际运行需要调用 api.run() |