返回 Pixelle-Video
api_workflows.py
根目录 / web / pipelines / api_workflows.py
1 """Helpers for exposing direct API media models in Streamlit pipeline UIs."""
2
3 import os
4 from typing import Any
5
6 import streamlit as st
7 from loguru import logger
8
9 from web.i18n import get_language
10
11
12 def is_api_workflow(workflow_key: str | None) -> bool:
13 """Return True for direct provider workflow keys such as api/dashscope/xxx."""
14 return bool(workflow_key and workflow_key.startswith("api/"))
15
16
17 def is_source_workflow(workflow: dict, source: str) -> bool:
18 """Return True when a workflow belongs to a concrete source namespace."""
19 key = workflow.get("key") or workflow.get("path") or ""
20 return key.startswith(f"{source}/") or workflow.get("source") == source
21
22
23 def workflow_source_label(source: str) -> str:
24 """Human-facing label for workflow source selectors."""
25 zh = get_language() == "zh_CN"
26 labels = {
27 "selfhost": "本地 ComfyUI" if zh else "Local ComfyUI",
28 "runninghub": "RunningHub",
29 "api": "API 模型" if zh else "API models",
30 }
31 return labels.get(source, source)
32
33
34 def workflow_source_help(subject: str | None = None) -> str:
35 """Common help text for workflow/model source selectors."""
36 zh = get_language() == "zh_CN"
37 subject_text = subject or ("当前步骤" if zh else "this step")
38 if zh:
39 return (
40 f"选择{subject_text}使用的模型服务来源:"
41 "RunningHub 使用云端工作流;本地 ComfyUI 使用 selfhost 工作流;"
42 "API 调用直接请求模型供应商。选择后,下方列表只显示该来源下可用的工作流或模型。"
43 )
44 return (
45 f"Choose the model service source for {subject_text}: "
46 "RunningHub uses cloud workflows; Local ComfyUI uses selfhost workflows; "
47 "API call directly requests model providers. The list below only shows workflows or models from the selected source."
48 )
49
50
51 def workflow_select_help() -> str:
52 """Common help text for workflow/model select boxes."""
53 if get_language() == "zh_CN":
54 return "这里只显示上方所选模型服务来源下可用的工作流或模型。"
55 return "Only workflows or models from the selected model service source are shown here."
56
57
58 def list_local_media_workflows(
59 pixelle_video: Any,
60 media_type: str,
61 source: str,
62 key_contains: str | None = None,
63 key_prefix: str | None = None,
64 ) -> list[dict]:
65 """List non-API media workflows by source without mixing provider models."""
66 try:
67 workflows = []
68 seen_keys = set()
69 for workflow in pixelle_video.media.list_workflows():
70 key = workflow.get("key") or workflow.get("path") or ""
71 if not key or key.startswith("api/"):
72 continue
73 if not is_source_workflow(workflow, source):
74 continue
75 if key_contains and key_contains.lower() not in key.lower():
76 continue
77 if key_prefix:
78 fname = os.path.basename(key)
79 if not fname.startswith(key_prefix):
80 continue
81 workflow_media_type = workflow.get("media_type")
82 if media_type == "video":
83 if workflow_media_type and workflow_media_type != "video":
84 continue
85 if not workflow_media_type and "video_" not in key.lower() and not (key_prefix and os.path.basename(key).startswith(key_prefix)):
86 continue
87 elif workflow_media_type and workflow_media_type != media_type:
88 continue
89 elif media_type == "image" and "video_" in key.lower():
90 continue
91 seen_keys.add(key)
92 workflows.append({
93 "key": key,
94 "display_name": workflow.get("display_name") or key,
95 **workflow,
96 })
97
98 if key_prefix:
99 try:
100 from pixelle_video.utils.os_util import get_resource_path, list_resource_files
101
102 for filename in list_resource_files("workflows", source):
103 if not filename.startswith(key_prefix) or not filename.endswith(".json"):
104 continue
105 key = f"{source}/{filename}"
106 if key in seen_keys:
107 continue
108 seen_keys.add(key)
109 workflows.append({
110 "key": key,
111 "name": filename,
112 "display_name": f"{filename} - {source.title()}",
113 "source": source,
114 "path": get_resource_path("workflows", source, filename),
115 "media_type": media_type,
116 })
117 except Exception as exc:
118 logger.warning(f"Failed to list {source}/{key_prefix} workflows from files: {exc}")
119 return workflows
120 except Exception as exc:
121 logger.warning(f"Failed to list {source} {media_type} workflows: {exc}")
122 return []
123
124
125 def list_api_media_workflows(
126 pixelle_video: Any,
127 media_type: str,
128 required_adapter_abilities: list[str] | tuple[str, ...] | set[str] | None = None,
129 verified_only: bool = False,
130 ) -> list[dict]:
131 """List API-backed media workflows in the same option shape used by UIs."""
132 api_media = getattr(pixelle_video, "api_media", None)
133 if api_media is None:
134 return []
135
136 required = set(required_adapter_abilities or [])
137
138 try:
139 workflows = []
140 for workflow in api_media.list_workflows():
141 if workflow.get("media_type") != media_type:
142 continue
143
144 if verified_only and not workflow.get("api_contract_verified", True):
145 continue
146
147 adapter_abilities = set(workflow.get("adapter_ability_types") or [])
148 if required and not required.intersection(adapter_abilities):
149 continue
150
151 workflows.append({
152 "key": workflow["key"],
153 "display_name": workflow.get("display_name") or workflow["key"],
154 **workflow,
155 })
156
157 return workflows
158 except Exception as exc:
159 logger.warning(f"Failed to list API {media_type} workflows: {exc}")
160 return []
161
162
163 def render_api_video_controls(
164 workflow: dict | None,
165 key_prefix: str,
166 default_duration: int = 5,
167 allow_audio_driven: bool = False,
168 show_duration: bool = True,
169 default_ratio: str | None = None,
170 ) -> dict:
171 """Render common API video controls based on verified adapter capability metadata."""
172 if not workflow or not is_api_workflow(workflow.get("key")):
173 return {}
174
175 zh = get_language() == "zh_CN"
176 capabilities = workflow.get("capabilities") or {}
177 adapter_abilities = set(workflow.get("adapter_ability_types") or [])
178 params: dict[str, Any] = {}
179
180 title = "API 视频模型参数" if zh else "API video model options"
181 with st.expander(title, expanded=False):
182 ability_text = ", ".join(sorted(adapter_abilities)) or ("未标注" if zh else "unknown")
183 st.caption(("已接入能力:" if zh else "Adapter abilities: ") + ability_text)
184
185 if not workflow.get("api_contract_verified", False):
186 st.warning(
187 "这个模型的公开 API 数据契约尚未完全确认,只会传递最基础的图生视频参数。"
188 if zh
189 else "This model's public API contract is not fully verified; only basic image-to-video parameters will be passed."
190 )
191
192 duration_contract = capabilities.get("duration") or {}
193 min_duration = int(duration_contract.get("min", 3))
194 max_duration = int(duration_contract.get("max", 15))
195 if show_duration:
196 default_value = min(max(int(default_duration or min_duration), min_duration), max_duration)
197 params["duration"] = st.slider(
198 "视频时长(秒)" if zh else "Duration (seconds)",
199 min_value=min_duration,
200 max_value=max_duration,
201 value=default_value,
202 step=1,
203 key=f"{key_prefix}_api_duration",
204 )
205 else:
206 st.caption(
207 f"视频时长将自动跟随每段旁白音频长度,并限制在模型支持范围 {min_duration}-{max_duration}s。"
208 if zh
209 else f"Duration follows each scene's narration audio and is clamped to the model range {min_duration}-{max_duration}s."
210 )
211
212 resolutions = capabilities.get("resolutions") or []
213 if resolutions:
214 params["resolution"] = st.selectbox(
215 "分辨率" if zh else "Resolution",
216 resolutions,
217 index=0,
218 key=f"{key_prefix}_api_resolution",
219 )
220
221 ratios = capabilities.get("ratios") or []
222 if ratios:
223 preferred_ratio = default_ratio or "9:16"
224 default_ratio_index = ratios.index(preferred_ratio) if preferred_ratio in ratios else 0
225 params["video_ratio"] = st.selectbox(
226 "画幅比例" if zh else "Aspect ratio",
227 ratios,
228 index=default_ratio_index,
229 key=f"{key_prefix}_api_ratio",
230 )
231
232 negative_prompt = st.text_area(
233 "负向提示词(可选)" if zh else "Negative prompt (optional)",
234 value="",
235 height=70,
236 key=f"{key_prefix}_api_negative_prompt",
237 )
238 if negative_prompt.strip():
239 params["negative_prompt"] = negative_prompt.strip()
240
241 if workflow.get("api_contract_verified", False):
242 params["watermark"] = st.checkbox(
243 "添加水印" if zh else "Add watermark",
244 value=False,
245 key=f"{key_prefix}_api_watermark",
246 )
247
248 if workflow.get("provider") == "seedance" and workflow.get("api_contract_verified", False):
249 params["generate_audio"] = st.checkbox(
250 "让模型生成原生音频" if zh else "Generate native audio",
251 value=False,
252 key=f"{key_prefix}_api_generate_audio",
253 )
254
255 if workflow.get("provider") == "kling" and workflow.get("api_contract_verified", False):
256 params["sound"] = "on" if st.checkbox(
257 "让模型生成原生音频" if zh else "Generate native audio",
258 value=False,
259 key=f"{key_prefix}_api_kling_sound",
260 ) else "off"
261
262 if allow_audio_driven and "audio_driven_i2v" in adapter_abilities:
263 params["use_narration_audio_as_driving_audio"] = st.checkbox(
264 "使用本场景旁白音频驱动画面" if zh else "Use narration audio as driving audio",
265 value=False,
266 help=(
267 "仅对已验证支持 driving_audio 的 API 模型生效。"
268 if zh
269 else "Only applies to verified API models that support driving_audio."
270 ),
271 key=f"{key_prefix}_api_audio_driven",
272 )
273
274 return {key: value for key, value in params.items() if value not in (None, "")}
275
275 lines PYTHON