Spaces:
Running
Running
| """ | |
| Agnes Creator Studio - 主应用入口 | |
| """ | |
| import gradio as gr | |
| from config import ( | |
| DEFAULT_API_KEY, | |
| DEFAULT_BASE_URL, | |
| DEFAULT_MODEL, | |
| IMAGE_SIZE_OPTIONS, | |
| VIDEO_RATIO_OPTIONS, | |
| VIDEO_DURATION_CHOICES, | |
| VIDEO_FRAME_RATE_CHOICES, | |
| VIDEO_MODE_OPTIONS, | |
| VIDEO_RESOLUTION_CHOICES, | |
| ) | |
| from api_client import AgnesImageGenerator | |
| from utils import ( | |
| add_to_history, | |
| load_history, | |
| save_config, | |
| parse_size, | |
| parse_ratio, | |
| ) | |
| from styles import CUSTOM_CSS | |
| # ==================== 创建界面 ==================== | |
| def create_ui(): | |
| with gr.Blocks(title="Agnes Creator Studio") as demo: | |
| # ===== Hero Header ===== | |
| gr.HTML(""" | |
| <div class="hero-header"> | |
| <div class="hero-content"> | |
| <div class="hero-eyebrow">Agnes AI · Creative Studio</div> | |
| <h1 class="hero-title">Where ideas<br/>become <span class="accent">images</span>.</h1> | |
| <p class="hero-subtitle">AI 驱动的图像与视频创作工具,支持 4K 高清输出,融合专业级画质增强与创意控制。</p> | |
| <div class="hero-links"> | |
| <a href="https://agnes-ai.com/doc/agnes-image-21-flash" target="_blank">图像 API ↗</a> | |
| <a href="https://agnes-ai.com/doc/agnes-video-v20" target="_blank">视频 API ↗</a> | |
| <a href="https://github.com/you-want/agnes-image-tool" target="_blank">GitHub ↗</a> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # ===== API 配置 ===== | |
| with gr.Accordion("⚙️ API 配置", open=not DEFAULT_API_KEY): | |
| with gr.Row(): | |
| api_key_input = gr.Textbox( | |
| label="API Key", | |
| placeholder="从 agnes-ai.com 获取", | |
| value=DEFAULT_API_KEY, | |
| type="password", | |
| ) | |
| base_url_input = gr.Textbox( | |
| label="Base URL", | |
| value=DEFAULT_BASE_URL, | |
| ) | |
| model_input = gr.Textbox( | |
| label="模型", | |
| value=DEFAULT_MODEL, | |
| ) | |
| save_config_btn = gr.Button("💾 保存配置", size="sm") | |
| save_config_status = gr.Textbox(label="", interactive=False, visible=False) | |
| # ===== 功能标签页 ===== | |
| with gr.Tabs(): | |
| # ===== 文生图 ===== | |
| with gr.TabItem("Text → Image"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| t2i_prompt = gr.Textbox( | |
| label="提示词", | |
| placeholder="描述你想生成的图像,例如:一只可爱的橘猫坐在窗台上,阳光明媚", | |
| lines=4, | |
| max_lines=8 | |
| ) | |
| t2i_negative_prompt = gr.Textbox( | |
| label="负面提示词", | |
| placeholder="描述你不想出现的内容,例如:模糊、低质量、变形", | |
| lines=2 | |
| ) | |
| with gr.Row(): | |
| t2i_size = gr.Dropdown( | |
| label="尺寸", | |
| choices=IMAGE_SIZE_OPTIONS, | |
| value=IMAGE_SIZE_OPTIONS[0] | |
| ) | |
| t2i_num = gr.Slider( | |
| label="生成数量", | |
| minimum=1, | |
| maximum=4, | |
| value=1, | |
| step=1 | |
| ) | |
| t2i_btn = gr.Button("🚀 生成", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| t2i_output = gr.Gallery( | |
| label="生成的图片", | |
| columns=2, | |
| rows=2, | |
| height="auto" | |
| ) | |
| t2i_info = gr.Textbox( | |
| label="状态", | |
| interactive=False, | |
| value="等待生成..." | |
| ) | |
| # ===== 图生图 ===== | |
| with gr.TabItem("Image → Image"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML("<div class='success-box'>✅ 图片API支持本地图片上传<br>上传的图片会自动转换为Base64格式发送给API</div>") | |
| img2img_input = gr.Image( | |
| label="上传参考图片", | |
| type="filepath", | |
| height=300 | |
| ) | |
| img2img_prompt = gr.Textbox( | |
| label="风格描述", | |
| placeholder="例如:油画风格,印象派,温暖色调", | |
| lines=3 | |
| ) | |
| img2img_negative_prompt = gr.Textbox( | |
| label="负面提示词", | |
| placeholder="描述你不想出现的内容", | |
| lines=2 | |
| ) | |
| with gr.Row(): | |
| img2img_size = gr.Dropdown( | |
| label="尺寸", | |
| choices=IMAGE_SIZE_OPTIONS, | |
| value=IMAGE_SIZE_OPTIONS[0] | |
| ) | |
| img2img_strength = gr.Slider( | |
| label="重绘强度", | |
| minimum=0.1, | |
| maximum=1.0, | |
| step=0.05, | |
| value=0.7 | |
| ) | |
| img2img_btn = gr.Button("🚀 生成", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| img2img_output = gr.Gallery( | |
| label="生成的图片", | |
| columns=2, | |
| rows=2, | |
| height="auto" | |
| ) | |
| img2img_info = gr.Textbox( | |
| label="状态", | |
| interactive=False, | |
| value="等待生成..." | |
| ) | |
| # ===== 文生视频 ===== | |
| with gr.TabItem("Text → Video"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| video_prompt = gr.Textbox( | |
| label="视频描述", | |
| placeholder="描述你想生成的视频,例如:夕阳下的海浪拍打礁石,镜头缓慢推近", | |
| lines=4, | |
| max_lines=8 | |
| ) | |
| video_negative_prompt = gr.Textbox( | |
| label="负面提示词", | |
| placeholder="描述你不想出现的内容", | |
| lines=2 | |
| ) | |
| gr.HTML("<div class='section-label'>分辨率设置</div>") | |
| with gr.Row(): | |
| video_resolution_preset = gr.Dropdown( | |
| label="分辨率档位", | |
| choices=VIDEO_RESOLUTION_CHOICES, | |
| value="720p", | |
| ) | |
| video_ratio = gr.Dropdown( | |
| label="比例", | |
| choices=VIDEO_RATIO_OPTIONS, | |
| value=VIDEO_RATIO_OPTIONS[0], | |
| ) | |
| with gr.Row(): | |
| video_width = gr.Number( | |
| label="宽度(可选)", | |
| placeholder="留空则按比例自动计算", | |
| precision=0 | |
| ) | |
| video_height = gr.Number( | |
| label="高度(可选)", | |
| placeholder="留空则按比例自动计算", | |
| precision=0 | |
| ) | |
| gr.HTML("<div class='section-label'>时长与帧率</div>") | |
| with gr.Row(): | |
| video_duration = gr.Dropdown( | |
| label="时长(秒)", | |
| choices=VIDEO_DURATION_CHOICES, | |
| value=5, | |
| ) | |
| video_frame_rate = gr.Dropdown( | |
| label="帧率", | |
| choices=VIDEO_FRAME_RATE_CHOICES, | |
| value=24, | |
| ) | |
| video_num_frames = gr.Number( | |
| label="总帧数(可选,覆盖时长设置)", | |
| placeholder="留空则按时长计算,需满足 8n+1 且 ≤441", | |
| precision=0 | |
| ) | |
| gr.HTML("<div class='section-label'>高级参数</div>") | |
| with gr.Row(): | |
| video_seed = gr.Number( | |
| label="随机种子(可选)", | |
| placeholder="留空则随机", | |
| precision=0 | |
| ) | |
| video_steps = gr.Slider( | |
| label="推理步数(可选)", | |
| minimum=10, | |
| maximum=100, | |
| step=1, | |
| value=50 | |
| ) | |
| video_btn = gr.Button("🚀 生成视频", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| video_output = gr.Video( | |
| label="生成的视频", | |
| height=400, | |
| autoplay=False | |
| ) | |
| video_info = gr.Textbox( | |
| label="状态", | |
| interactive=False, | |
| value="等待生成..." | |
| ) | |
| # ===== 图生视频 ===== | |
| with gr.TabItem("Image → Video"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML("<div class='warning-box'>⚠️ 视频API需要公网可访问的图片URL<br><br>推荐方式:使用下方URL输入框输入公网图片链接<br><br>替代方式:上传本地图片(仅当API服务器在本地时可用)<br>如果API服务器在远程,本地图片将无法访问</div>") | |
| i2v_input = gr.Image( | |
| label="上传本地图片(仅当API服务器在本地时可用)", | |
| type="filepath", | |
| height=300 | |
| ) | |
| i2v_url_input = gr.Textbox( | |
| label="公网图片URL(推荐)", | |
| placeholder="https://example.com/image.png", | |
| lines=1, | |
| info="输入公网可访问的图片URL,如 https://example.com/image.png" | |
| ) | |
| i2v_prompt = gr.Textbox( | |
| label="视频描述", | |
| placeholder="描述你想生成的视频动态,例如:人物缓慢转身,头发随风飘动", | |
| lines=3 | |
| ) | |
| i2v_negative_prompt = gr.Textbox( | |
| label="负面提示词", | |
| placeholder="描述你不想出现的内容", | |
| lines=2 | |
| ) | |
| gr.HTML("<div class='section-label'>分辨率设置</div>") | |
| with gr.Row(): | |
| i2v_resolution_preset = gr.Dropdown( | |
| label="分辨率档位", | |
| choices=VIDEO_RESOLUTION_CHOICES, | |
| value="720p", | |
| ) | |
| i2v_ratio = gr.Dropdown( | |
| label="比例", | |
| choices=VIDEO_RATIO_OPTIONS, | |
| value=VIDEO_RATIO_OPTIONS[0], | |
| ) | |
| with gr.Row(): | |
| i2v_width = gr.Number( | |
| label="宽度(可选)", | |
| placeholder="留空则按比例自动计算", | |
| precision=0 | |
| ) | |
| i2v_height = gr.Number( | |
| label="高度(可选)", | |
| placeholder="留空则按比例自动计算", | |
| precision=0 | |
| ) | |
| gr.HTML("<div class='section-label'>时长与帧率</div>") | |
| with gr.Row(): | |
| i2v_duration = gr.Dropdown( | |
| label="时长(秒)", | |
| choices=VIDEO_DURATION_CHOICES, | |
| value=5, | |
| ) | |
| i2v_frame_rate = gr.Dropdown( | |
| label="帧率", | |
| choices=VIDEO_FRAME_RATE_CHOICES, | |
| value=24, | |
| ) | |
| i2v_num_frames = gr.Number( | |
| label="总帧数(可选,覆盖时长设置)", | |
| placeholder="留空则按时长计算,需满足 8n+1 且 ≤441", | |
| precision=0 | |
| ) | |
| gr.HTML("<div class='section-label'>高级参数</div>") | |
| with gr.Row(): | |
| i2v_seed = gr.Number( | |
| label="随机种子(可选)", | |
| placeholder="留空则随机", | |
| precision=0 | |
| ) | |
| i2v_steps = gr.Slider( | |
| label="推理步数(可选)", | |
| minimum=10, | |
| maximum=100, | |
| step=1, | |
| value=50 | |
| ) | |
| i2v_btn = gr.Button("🚀 生成视频", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| i2v_output = gr.Video( | |
| label="生成的视频", | |
| height=400, | |
| autoplay=False | |
| ) | |
| i2v_info = gr.Textbox( | |
| label="状态", | |
| interactive=False, | |
| value="等待生成..." | |
| ) | |
| # ===== 多图视频 ===== | |
| with gr.TabItem("Multi-Image Video"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.HTML("<div class='warning-box'>⚠️ 视频API需要公网可访问的图片URL<br>不支持本地图片的Base64格式<br>请输入公网图片URL(如 https://example.com/image.png)</div>") | |
| mi_prompt = gr.Textbox( | |
| label="视频描述", | |
| placeholder="描述多图视频或关键帧动画,例如:在两张图片之间生成平滑过渡", | |
| lines=4, | |
| max_lines=8 | |
| ) | |
| mi_negative_prompt = gr.Textbox( | |
| label="负面提示词", | |
| placeholder="描述你不想出现的内容", | |
| lines=2 | |
| ) | |
| gr.HTML("<div class='section-label'>图片输入</div>") | |
| mi_image_urls = gr.Textbox( | |
| label="图片 URL 列表(每行一个)", | |
| placeholder="https://example.com/image1.png\nhttps://example.com/image2.png", | |
| lines=4, | |
| info="输入公网可访问的图片URL,不支持本地图片" | |
| ) | |
| mi_mode = gr.Dropdown( | |
| label="生成模式", | |
| choices=VIDEO_MODE_OPTIONS, | |
| value="ti2vid", | |
| ) | |
| gr.HTML("<div class='section-label'>分辨率设置</div>") | |
| with gr.Row(): | |
| mi_resolution_preset = gr.Dropdown( | |
| label="分辨率档位", | |
| choices=VIDEO_RESOLUTION_CHOICES, | |
| value="720p", | |
| ) | |
| mi_ratio = gr.Dropdown( | |
| label="比例", | |
| choices=VIDEO_RATIO_OPTIONS, | |
| value=VIDEO_RATIO_OPTIONS[0], | |
| ) | |
| with gr.Row(): | |
| mi_width = gr.Number( | |
| label="宽度(可选)", | |
| placeholder="留空则按比例自动计算", | |
| precision=0 | |
| ) | |
| mi_height = gr.Number( | |
| label="高度(可选)", | |
| placeholder="留空则按比例自动计算", | |
| precision=0 | |
| ) | |
| gr.HTML("<div class='section-label'>时长与帧率</div>") | |
| with gr.Row(): | |
| mi_duration = gr.Dropdown( | |
| label="时长(秒)", | |
| choices=VIDEO_DURATION_CHOICES, | |
| value=5, | |
| ) | |
| mi_frame_rate = gr.Dropdown( | |
| label="帧率", | |
| choices=VIDEO_FRAME_RATE_CHOICES, | |
| value=24, | |
| ) | |
| mi_num_frames = gr.Number( | |
| label="总帧数(可选,覆盖时长设置)", | |
| placeholder="留空则按时长计算,需满足 8n+1 且 ≤441", | |
| precision=0 | |
| ) | |
| gr.HTML("<div class='section-label'>高级参数</div>") | |
| with gr.Row(): | |
| mi_seed = gr.Number( | |
| label="随机种子(可选)", | |
| placeholder="留空则随机", | |
| precision=0 | |
| ) | |
| mi_steps = gr.Slider( | |
| label="推理步数(可选)", | |
| minimum=10, | |
| maximum=100, | |
| step=1, | |
| value=50 | |
| ) | |
| mi_btn = gr.Button("🚀 生成视频", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| mi_output = gr.Video( | |
| label="生成的视频", | |
| height=400, | |
| autoplay=False | |
| ) | |
| mi_info = gr.Textbox( | |
| label="状态", | |
| interactive=False, | |
| value="等待生成..." | |
| ) | |
| # ===== 批量生成 ===== | |
| with gr.TabItem("Batch"): | |
| batch_prompts = gr.Textbox( | |
| label="批量提示词(每行一个)", | |
| placeholder="一只猫\n一只狗\n一座山", | |
| lines=10 | |
| ) | |
| with gr.Row(): | |
| batch_size = gr.Dropdown( | |
| label="尺寸", | |
| choices=IMAGE_SIZE_OPTIONS, | |
| value=IMAGE_SIZE_OPTIONS[0] | |
| ) | |
| batch_btn = gr.Button("🚀 批量生成", variant="primary") | |
| batch_output = gr.Gallery( | |
| label="批量结果", | |
| columns=4, | |
| rows=4, | |
| height="auto" | |
| ) | |
| batch_info = gr.Textbox( | |
| label="状态", | |
| interactive=False, | |
| value="等待生成..." | |
| ) | |
| # ===== 历史记录 ===== | |
| with gr.TabItem("History"): | |
| refresh_history_btn = gr.Button("🔄 刷新") | |
| history_gallery = gr.Gallery( | |
| label="历史图片", | |
| columns=4, | |
| rows=4, | |
| height="auto" | |
| ) | |
| history_text = gr.JSON(label="历史详情") | |
| # ==================== 事件处理 ==================== | |
| def do_save_config(api_key, base_url, model): | |
| """保存配置到本地文件""" | |
| save_config({"api_key": api_key, "base_url": base_url, "model": model}) | |
| return gr.Textbox(value="✅ 配置已保存", visible=True) | |
| def generate_text2image( | |
| api_key, base_url, model, | |
| prompt, negative_prompt, size, n | |
| ): | |
| """文生图处理""" | |
| if not api_key: | |
| return [], "❌ 请先配置 API Key" | |
| if not prompt.strip(): | |
| return [], "❌ 请输入提示词" | |
| try: | |
| gen = AgnesImageGenerator(api_key, base_url) | |
| urls = gen.text_to_image( | |
| prompt=prompt, | |
| size=parse_size(size), | |
| n=int(n), | |
| negative_prompt=negative_prompt, | |
| ) | |
| # 下载图片到本地 | |
| local_paths = [gen.download_image(url) for url in urls] | |
| # 保存历史 | |
| add_to_history(prompt, local_paths, "text2image") | |
| return local_paths, f"✅ 生成成功!共 {len(local_paths)} 张图片" | |
| except Exception as e: | |
| return [], f"❌ 错误: {str(e)}" | |
| def generate_image2image( | |
| api_key, base_url, model, | |
| image_path, prompt, negative_prompt, size, strength | |
| ): | |
| """图生图处理""" | |
| if not api_key: | |
| return [], "❌ 请先配置 API Key" | |
| if not image_path: | |
| return [], "❌ 请上传参考图片" | |
| if not prompt.strip(): | |
| return [], "❌ 请输入风格描述" | |
| try: | |
| gen = AgnesImageGenerator(api_key, base_url) | |
| urls = gen.image_to_image( | |
| image_path=image_path, | |
| prompt=prompt, | |
| size=parse_size(size), | |
| strength=strength, | |
| negative_prompt=negative_prompt, | |
| ) | |
| # 下载图片到本地 | |
| local_paths = [gen.download_image(url) for url in urls] | |
| # 保存历史 | |
| add_to_history(prompt, local_paths, "image2image", { | |
| "strength": strength | |
| }) | |
| return local_paths, f"✅ 生成成功!强度: {strength}" | |
| except Exception as e: | |
| return [], f"❌ 错误: {str(e)}" | |
| def generate_text2video( | |
| api_key, base_url, model, | |
| prompt, negative_prompt, resolution_preset, ratio, width, height, | |
| duration, frame_rate, num_frames, seed, steps | |
| ): | |
| """文生视频处理""" | |
| if not api_key: | |
| return None, "❌ 请先配置 API Key" | |
| if not prompt.strip(): | |
| return None, "❌ 请输入视频描述" | |
| try: | |
| gen = AgnesImageGenerator(api_key, base_url) | |
| # 处理可选参数 | |
| seed_val = int(seed) if seed and seed > 0 else None | |
| steps_val = int(steps) if steps and steps != 50 else None | |
| width_val = int(width) if width and width > 0 else None | |
| height_val = int(height) if height and height > 0 else None | |
| num_frames_val = int(num_frames) if num_frames and num_frames > 0 else None | |
| video_url = gen.text_to_video( | |
| prompt=prompt, | |
| duration=int(duration), | |
| aspect_ratio=ratio, | |
| frame_rate=int(frame_rate), | |
| negative_prompt=negative_prompt, | |
| seed=seed_val, | |
| num_inference_steps=steps_val, | |
| width=width_val, | |
| height=height_val, | |
| num_frames=num_frames_val, | |
| ) | |
| # 下载视频到本地 | |
| local_path = gen.download_video(video_url) | |
| # 保存历史 | |
| add_to_history(prompt, [local_path], "text2video", { | |
| "duration": duration, | |
| "ratio": ratio | |
| }) | |
| return local_path, f"✅ 视频生成成功!时长: {duration}秒, 比例: {ratio}" | |
| except Exception as e: | |
| return None, f"❌ 错误: {str(e)}" | |
| def generate_image2video( | |
| api_key, base_url, model, | |
| image_path, image_url_text, prompt, negative_prompt, resolution_preset, ratio, width, height, | |
| duration, frame_rate, num_frames, seed, steps | |
| ): | |
| """图生视频处理""" | |
| if not api_key: | |
| return None, "❌ 请先配置 API Key" | |
| if not prompt.strip(): | |
| return None, "❌ 请输入视频描述" | |
| # 优先使用公网图片URL | |
| if image_url_text and image_url_text.strip(): | |
| print(f" 图生视频: 使用公网图片URL={image_url_text}") | |
| # 使用公网URL直接调用API | |
| try: | |
| gen = AgnesImageGenerator(api_key, base_url) | |
| # 处理可选参数 | |
| seed_val = int(seed) if seed and seed > 0 else None | |
| steps_val = int(steps) if steps and steps != 50 else None | |
| width_val = int(width) if width and width > 0 else None | |
| height_val = int(height) if height and height > 0 else None | |
| num_frames_val = int(num_frames) if num_frames and num_frames > 0 else None | |
| # 直接使用URL调用视频生成API | |
| payload = gen._build_text_to_video_payload( | |
| prompt=prompt, | |
| duration=int(duration), | |
| aspect_ratio=ratio, | |
| frame_rate=int(frame_rate), | |
| negative_prompt=negative_prompt, | |
| seed=seed_val, | |
| num_inference_steps=steps_val, | |
| width=width_val, | |
| height=height_val, | |
| num_frames=num_frames_val, | |
| ) | |
| # 添加图片URL | |
| payload["image"] = image_url_text.strip() | |
| video_url = gen._generate_video(payload) | |
| # 下载视频到本地 | |
| local_path = gen.download_video(video_url) | |
| # 保存历史 | |
| add_to_history(prompt, [local_path], "image2video", { | |
| "duration": duration, | |
| "ratio": ratio | |
| }) | |
| return local_path, f"✅ 视频生成成功!时长: {duration}秒, 比例: {ratio}" | |
| except Exception as e: | |
| return None, f"❌ 错误: {str(e)}" | |
| # 如果没有提供公网URL,尝试使用本地图片 | |
| if not image_path: | |
| return None, "❌ 请上传参考图片或输入公网图片URL" | |
| try: | |
| gen = AgnesImageGenerator(api_key, base_url) | |
| # 处理可选参数 | |
| seed_val = int(seed) if seed and seed > 0 else None | |
| steps_val = int(steps) if steps and steps != 50 else None | |
| width_val = int(width) if width and width > 0 else None | |
| height_val = int(height) if height and height > 0 else None | |
| num_frames_val = int(num_frames) if num_frames and num_frames > 0 else None | |
| # 使用本地图片 | |
| print(f" 图生视频: 图片路径={image_path}") | |
| # 尝试使用Gradio的临时文件URL | |
| # Gradio会为上传的文件创建一个临时URL | |
| # 格式: http://localhost:7860/file=/tmp/gradio/xxx.png | |
| # 获取Gradio服务器的URL | |
| import os | |
| server_name = os.getenv("GRADIO_SERVER_NAME", "localhost") | |
| server_port = os.getenv("GRADIO_SERVER_PORT", "7860") | |
| # 构建临时文件URL | |
| # 注意:这个URL只在本地可访问,如果API服务器在远程,可能无法访问 | |
| temp_url = f"http://{server_name}:{server_port}/file={image_path}" | |
| print(f" 尝试使用临时URL: {temp_url}") | |
| print(f" 注意: 此URL仅在本地可访问,如果API服务器在远程,可能无法访问") | |
| # 直接使用临时URL调用视频生成API | |
| payload = gen._build_text_to_video_payload( | |
| prompt=prompt, | |
| duration=int(duration), | |
| aspect_ratio=ratio, | |
| frame_rate=int(frame_rate), | |
| negative_prompt=negative_prompt, | |
| seed=seed_val, | |
| num_inference_steps=steps_val, | |
| width=width_val, | |
| height=height_val, | |
| num_frames=num_frames_val, | |
| ) | |
| # 添加图片URL | |
| payload["image"] = temp_url | |
| video_url = gen._generate_video(payload) | |
| # 下载视频到本地 | |
| local_path = gen.download_video(video_url) | |
| # 保存历史 | |
| add_to_history(prompt, [local_path], "image2video", { | |
| "duration": duration, | |
| "ratio": ratio | |
| }) | |
| return local_path, f"✅ 视频生成成功!时长: {duration}秒, 比例: {ratio}" | |
| except Exception as e: | |
| error_msg = str(e) | |
| if "Invalid image" in error_msg or "padding" in error_msg or "fail_to_fetch" in error_msg: | |
| error_msg += "\n\n💡 提示: 视频API需要公网可访问的图片URL。\n本地图片的临时URL仅在本地可访问,如果API服务器在远程,无法访问。\n\n解决方案:\n1. 使用公网可访问的图片URL(推荐)\n2. 将图片上传到图床服务(如imgur、阿里云OSS等)\n3. 使用ngrok等工具将本地服务器暴露到公网" | |
| return None, f"❌ 错误: {error_msg}" | |
| def generate_multi_image_video( | |
| api_key, base_url, model, | |
| prompt, negative_prompt, image_urls_text, mode, resolution_preset, ratio, | |
| width, height, duration, frame_rate, num_frames, seed, steps | |
| ): | |
| """多图视频处理""" | |
| if not api_key: | |
| return None, "❌ 请先配置 API Key" | |
| if not prompt.strip(): | |
| return None, "❌ 请输入视频描述" | |
| if not image_urls_text.strip(): | |
| return None, "❌ 请输入图片 URL 列表" | |
| try: | |
| gen = AgnesImageGenerator(api_key, base_url) | |
| # 解析图片 URL | |
| image_urls = [url.strip() for url in image_urls_text.split("\n") if url.strip()] | |
| if not image_urls: | |
| return None, "❌ 请输入至少一个图片 URL" | |
| # 处理可选参数 | |
| seed_val = int(seed) if seed and seed > 0 else None | |
| steps_val = int(steps) if steps and steps != 50 else None | |
| width_val = int(width) if width and width > 0 else None | |
| height_val = int(height) if height and height > 0 else None | |
| num_frames_val = int(num_frames) if num_frames and num_frames > 0 else None | |
| video_url = gen.multi_image_video( | |
| prompt=prompt, | |
| image_urls=image_urls, | |
| mode=mode, | |
| duration=int(duration), | |
| aspect_ratio=ratio, | |
| frame_rate=int(frame_rate), | |
| negative_prompt=negative_prompt, | |
| seed=seed_val, | |
| num_inference_steps=steps_val, | |
| width=width_val, | |
| height=height_val, | |
| num_frames=num_frames_val, | |
| ) | |
| # 下载视频到本地 | |
| local_path = gen.download_video(video_url) | |
| # 保存历史 | |
| add_to_history(prompt, [local_path], "multi_image_video", { | |
| "mode": mode, | |
| "duration": duration, | |
| "ratio": ratio, | |
| "image_count": len(image_urls) | |
| }) | |
| return local_path, f"✅ 视频生成成功!模式: {mode}, 时长: {duration}秒, 比例: {ratio}" | |
| except Exception as e: | |
| return None, f"❌ 错误: {str(e)}" | |
| def generate_batch( | |
| api_key, base_url, model, | |
| prompts_text, size | |
| ): | |
| """批量生成处理""" | |
| if not api_key: | |
| return [], "❌ 请先配置 API Key" | |
| prompts = [p.strip() for p in prompts_text.split("\n") if p.strip()] | |
| if not prompts: | |
| return [], "❌ 请输入至少一个提示词" | |
| try: | |
| gen = AgnesImageGenerator(api_key, base_url) | |
| all_paths = [] | |
| for i, p in enumerate(prompts): | |
| urls = gen.text_to_image( | |
| prompt=p, | |
| size=parse_size(size), | |
| ) | |
| paths = [gen.download_image(url) for url in urls] | |
| all_paths.extend(paths) | |
| # 保存历史 | |
| add_to_history(f"批量生成 ({len(prompts)} 个)", all_paths, "batch") | |
| return all_paths, f"✅ 批量生成完成!共 {len(all_paths)} 张图片" | |
| except Exception as e: | |
| return [], f"❌ 错误: {str(e)}" | |
| def load_history_gallery(): | |
| """加载历史记录到画廊""" | |
| history = load_history() | |
| images = [] | |
| for record in history: | |
| for img in record.get("images", []): | |
| images.append((img, record.get("prompt", ""))) | |
| return images, history | |
| # ==================== 事件绑定 ==================== | |
| save_config_btn.click( | |
| fn=do_save_config, | |
| inputs=[api_key_input, base_url_input, model_input], | |
| outputs=[save_config_status] | |
| ) | |
| t2i_btn.click( | |
| fn=generate_text2image, | |
| inputs=[ | |
| api_key_input, base_url_input, model_input, | |
| t2i_prompt, t2i_negative_prompt, t2i_size, t2i_num | |
| ], | |
| outputs=[t2i_output, t2i_info] | |
| ) | |
| img2img_btn.click( | |
| fn=generate_image2image, | |
| inputs=[ | |
| api_key_input, base_url_input, model_input, | |
| img2img_input, img2img_prompt, img2img_negative_prompt, | |
| img2img_size, img2img_strength | |
| ], | |
| outputs=[img2img_output, img2img_info] | |
| ) | |
| video_btn.click( | |
| fn=generate_text2video, | |
| inputs=[ | |
| api_key_input, base_url_input, model_input, | |
| video_prompt, video_negative_prompt, video_resolution_preset, video_ratio, | |
| video_width, video_height, video_duration, video_frame_rate, | |
| video_num_frames, video_seed, video_steps | |
| ], | |
| outputs=[video_output, video_info] | |
| ) | |
| i2v_btn.click( | |
| fn=generate_image2video, | |
| inputs=[ | |
| api_key_input, base_url_input, model_input, | |
| i2v_input, i2v_url_input, i2v_prompt, i2v_negative_prompt, i2v_resolution_preset, i2v_ratio, | |
| i2v_width, i2v_height, i2v_duration, i2v_frame_rate, | |
| i2v_num_frames, i2v_seed, i2v_steps | |
| ], | |
| outputs=[i2v_output, i2v_info] | |
| ) | |
| mi_btn.click( | |
| fn=generate_multi_image_video, | |
| inputs=[ | |
| api_key_input, base_url_input, model_input, | |
| mi_prompt, mi_negative_prompt, mi_image_urls, mi_mode, | |
| mi_resolution_preset, mi_ratio, mi_width, mi_height, | |
| mi_duration, mi_frame_rate, mi_num_frames, mi_seed, mi_steps | |
| ], | |
| outputs=[mi_output, mi_info] | |
| ) | |
| batch_btn.click( | |
| fn=generate_batch, | |
| inputs=[ | |
| api_key_input, base_url_input, model_input, | |
| batch_prompts, batch_size | |
| ], | |
| outputs=[batch_output, batch_info] | |
| ) | |
| refresh_history_btn.click( | |
| fn=load_history_gallery, | |
| outputs=[history_gallery, history_text] | |
| ) | |
| # 页面加载时刷新历史 | |
| demo.load(fn=load_history_gallery, outputs=[history_gallery, history_text]) | |
| return demo | |
| # ==================== 启动应用 ==================== | |
| if __name__ == "__main__": | |
| import os | |
| demo = create_ui() | |
| server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0") | |
| server_port = int(os.getenv("GRADIO_SERVER_PORT", 7860)) | |
| share = os.getenv("GRADIO_SHARE", "false").lower() == "true" | |
| demo.launch( | |
| server_name=server_name, | |
| server_port=server_port, | |
| share=share, | |
| show_error=True, | |
| css=CUSTOM_CSS, | |
| max_threads=4, | |
| favicon_path=None, | |
| ) | |