Skip to content

Commit 53565ec

Browse files
authored
Beta (#116)
* fix(captcha): 复用 playwright chromium 并移除 DISPLAY 依赖 * fix(flow): 修复 project 绑定图片上传回退 * docs(readme): 补充 veo 3.1 lite 模型说明
1 parent bcef0fb commit 53565ec

6 files changed

Lines changed: 286 additions & 32 deletions

File tree

Dockerfile.headed

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,14 @@ WORKDIR /app
44

55
ENV PYTHONDONTWRITEBYTECODE=1 \
66
PYTHONUNBUFFERED=1 \
7-
ALLOW_DOCKER_HEADED_CAPTCHA=true
7+
ALLOW_DOCKER_HEADED_CAPTCHA=true \
8+
PLAYWRIGHT_BROWSERS_PATH=0
89

9-
# 安装 Chrome 运行依赖(无头模式仍需要这些系统库)
10+
# 在镜像构建阶段预装 Playwright Chromium,供 personal/browser 模式复用
1011
COPY requirements.txt ./
1112

12-
RUN apt-get update \
13-
&& apt-get install -y --no-install-recommends \
14-
ca-certificates \
15-
curl \
16-
libnss3 \
17-
libnspr4 \
18-
libatk1.0-0 \
19-
libatk-bridge2.0-0 \
20-
libcups2 \
21-
libdrm2 \
22-
libxkbcommon0 \
23-
libxcomposite1 \
24-
libxdamage1 \
25-
libxfixes3 \
26-
libxrandr2 \
27-
libgbm1 \
28-
libpango-1.0-0 \
29-
libcairo2 \
30-
libasound2 \
31-
&& rm -rf /var/lib/apt/lists/*
32-
33-
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
13+
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt \
14+
&& python -m playwright install --with-deps chromium
3415

3516
COPY . .
3617
COPY docker/entrypoint.headed.sh /usr/local/bin/entrypoint.headed.sh

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,17 @@ python main.py
183183
| `veo_3_1_t2v_fast_ultra_relaxed` | 文生视频 | 横屏 |
184184
| `veo_3_1_t2v_portrait` | 文生视频 | 竖屏 |
185185
| `veo_3_1_t2v_landscape` | 文生视频 | 横屏 |
186+
| `veo_3_1_t2v_lite_portrait` | 文生视频 Lite | 竖屏 |
187+
| `veo_3_1_t2v_lite_landscape` | 文生视频 Lite | 横屏 |
186188

187189
#### 首尾帧模型 (I2V - Image to Video)
188190
📸 **支持1-2张图片:1张作为首帧,2张作为首尾帧**
189191

190192
> 💡 **自动适配**:系统会根据图片数量自动选择对应的 model_key
191193
> - **单帧模式**(1张图):使用首帧生成视频
192194
> - **双帧模式**(2张图):使用首帧+尾帧生成过渡视频
195+
> - `veo_3_1_i2v_lite_*` 仅支持 **1 张** 首帧图片
196+
> - `veo_3_1_interpolation_lite_*` 仅支持 **2 张** 首尾帧图片
193197
194198
| 模型名称 | 说明| 尺寸 |
195199
|---------|---------|--------|
@@ -205,6 +209,10 @@ python main.py
205209
| `veo_3_1_i2v_s_fast_ultra_relaxed` | 图生视频 | 横屏 |
206210
| `veo_3_1_i2v_s_portrait` | 图生视频 | 竖屏 |
207211
| `veo_3_1_i2v_s_landscape` | 图生视频 | 横屏 |
212+
| `veo_3_1_i2v_lite_portrait` | 图生视频 Lite(仅首帧) | 竖屏 |
213+
| `veo_3_1_i2v_lite_landscape` | 图生视频 Lite(仅首帧) | 横屏 |
214+
| `veo_3_1_interpolation_lite_portrait` | 图生视频 Lite(首尾帧过渡) | 竖屏 |
215+
| `veo_3_1_interpolation_lite_landscape` | 图生视频 Lite(首尾帧过渡) | 横屏 |
208216

209217
#### 多图生成 (R2V - Reference Images to Video)
210218
🖼️ **支持多张图片**

docker/entrypoint.headed.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
#!/bin/sh
22
set -eu
33

4+
resolve_browser_path() {
5+
python - <<'PY'
6+
from playwright.sync_api import sync_playwright
7+
8+
with sync_playwright() as p:
9+
print(p.chromium.executable_path or "")
10+
PY
11+
}
12+
13+
if [ -z "${BROWSER_EXECUTABLE_PATH:-}" ] || [ ! -x "${BROWSER_EXECUTABLE_PATH:-}" ]; then
14+
detected_browser_path="$(resolve_browser_path 2>/dev/null | tr -d '\r' | tail -n 1)"
15+
if [ -n "${detected_browser_path}" ] && [ -x "${detected_browser_path}" ]; then
16+
export BROWSER_EXECUTABLE_PATH="${detected_browser_path}"
17+
fi
18+
fi
19+
420
echo "[entrypoint] starting flow2api (headless browser mode)"
21+
if [ -n "${BROWSER_EXECUTABLE_PATH:-}" ] && [ -x "${BROWSER_EXECUTABLE_PATH}" ]; then
22+
echo "[entrypoint] browser executable: ${BROWSER_EXECUTABLE_PATH}"
23+
"${BROWSER_EXECUTABLE_PATH}" --version || true
24+
else
25+
echo "[entrypoint] warning: no valid browser executable found for personal/browser captcha" >&2
26+
fi
27+
528
exec python main.py

src/services/browser_captcha_personal.py

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
from ..core.logger import debug_logger
2020
from ..core.config import config
2121

22+
# 复用 browser 模式的浏览器缓存目录约定,避免容器内每次换位置。
23+
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", "0")
24+
2225

2326
# ==================== Docker 环境检测 ====================
2427
def _is_running_in_docker() -> bool:
@@ -118,6 +121,111 @@ def _ensure_nodriver_installed() -> bool:
118121
return False
119122

120123

124+
def _run_playwright_install(use_mirror: bool = False) -> bool:
125+
"""安装 playwright chromium 浏览器,复用 browser 模式的安装方式。"""
126+
cmd = [sys.executable, '-m', 'playwright', 'install', 'chromium']
127+
env = os.environ.copy()
128+
129+
if use_mirror:
130+
env['PLAYWRIGHT_DOWNLOAD_HOST'] = 'https://npmmirror.com/mirrors/playwright'
131+
132+
try:
133+
debug_logger.log_info("[BrowserCaptcha] 正在安装 chromium 浏览器...")
134+
print("[BrowserCaptcha] 正在安装 chromium 浏览器...")
135+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600, env=env)
136+
if result.returncode == 0:
137+
debug_logger.log_info("[BrowserCaptcha] ✅ chromium 浏览器安装成功")
138+
print("[BrowserCaptcha] ✅ chromium 浏览器安装成功")
139+
return True
140+
141+
debug_logger.log_warning(f"[BrowserCaptcha] chromium 安装失败: {result.stderr[:200]}")
142+
return False
143+
except Exception as e:
144+
debug_logger.log_warning(f"[BrowserCaptcha] chromium 安装异常: {e}")
145+
return False
146+
147+
148+
def _ensure_playwright_installed() -> bool:
149+
"""确保 playwright 可用,便于复用其 chromium 二进制。"""
150+
try:
151+
import playwright # noqa: F401
152+
debug_logger.log_info("[BrowserCaptcha] playwright 已安装")
153+
return True
154+
except ImportError:
155+
pass
156+
157+
debug_logger.log_info("[BrowserCaptcha] playwright 未安装,开始自动安装...")
158+
print("[BrowserCaptcha] playwright 未安装,开始自动安装...")
159+
160+
if _run_pip_install('playwright', use_mirror=False):
161+
return True
162+
163+
debug_logger.log_info("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
164+
print("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
165+
if _run_pip_install('playwright', use_mirror=True):
166+
return True
167+
168+
debug_logger.log_error("[BrowserCaptcha] ❌ playwright 自动安装失败,请手动安装: pip install playwright")
169+
print("[BrowserCaptcha] ❌ playwright 自动安装失败,请手动安装: pip install playwright")
170+
return False
171+
172+
173+
def _detect_playwright_browser_path() -> Optional[str]:
174+
"""读取 playwright 管理的 chromium 可执行文件路径。"""
175+
detect_script = (
176+
"from playwright.sync_api import sync_playwright\n"
177+
"with sync_playwright() as p:\n"
178+
" print(p.chromium.executable_path or '')\n"
179+
)
180+
env = os.environ.copy()
181+
env.setdefault("PLAYWRIGHT_BROWSERS_PATH", os.environ.get("PLAYWRIGHT_BROWSERS_PATH", "0") or "0")
182+
183+
try:
184+
result = subprocess.run(
185+
[sys.executable, "-c", detect_script],
186+
capture_output=True,
187+
text=True,
188+
timeout=60,
189+
env=env,
190+
)
191+
browser_path_lines = (result.stdout or "").strip().splitlines()
192+
browser_path = browser_path_lines[-1].strip() if browser_path_lines else ""
193+
if result.returncode == 0 and browser_path and os.path.exists(browser_path):
194+
debug_logger.log_info(f"[BrowserCaptcha] 检测到 playwright chromium: {browser_path}")
195+
return browser_path
196+
197+
stderr_text = (result.stderr or "").strip()
198+
if stderr_text:
199+
debug_logger.log_warning(f"[BrowserCaptcha] 检测 playwright chromium 失败: {stderr_text[:200]}")
200+
except Exception as e:
201+
debug_logger.log_info(f"[BrowserCaptcha] 检测 playwright chromium 时出错: {e}")
202+
203+
return None
204+
205+
206+
def _ensure_playwright_browser_path() -> Optional[str]:
207+
"""确保存在可复用的 chromium 二进制,并返回路径。"""
208+
browser_path = _detect_playwright_browser_path()
209+
if browser_path:
210+
return browser_path
211+
212+
if not _ensure_playwright_installed():
213+
return None
214+
215+
debug_logger.log_info("[BrowserCaptcha] playwright chromium 未安装,开始自动安装...")
216+
print("[BrowserCaptcha] playwright chromium 未安装,开始自动安装...")
217+
218+
if not _run_playwright_install(use_mirror=False):
219+
debug_logger.log_info("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
220+
print("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
221+
if not _run_playwright_install(use_mirror=True):
222+
debug_logger.log_error("[BrowserCaptcha] ❌ chromium 浏览器自动安装失败,请手动安装: python -m playwright install chromium")
223+
print("[BrowserCaptcha] ❌ chromium 浏览器自动安装失败,请手动安装: python -m playwright install chromium")
224+
return None
225+
226+
return _detect_playwright_browser_path()
227+
228+
121229
# 尝试导入 nodriver
122230
uc = None
123231
NODRIVER_AVAILABLE = False
@@ -126,14 +234,15 @@ def _ensure_nodriver_installed() -> bool:
126234
if DOCKER_HEADED_BLOCKED:
127235
debug_logger.log_warning(
128236
"[BrowserCaptcha] 检测到 Docker 环境,默认禁用内置浏览器打码。"
129-
"如需启用请设置 ALLOW_DOCKER_HEADED_CAPTCHA=true,并提供 DISPLAY/Xvfb。"
237+
"如需启用请设置 ALLOW_DOCKER_HEADED_CAPTCHA=true。"
238+
"personal 模式默认支持无头,不强制依赖 DISPLAY/Xvfb。"
130239
)
131240
print("[BrowserCaptcha] ⚠️ 检测到 Docker 环境,默认禁用内置浏览器打码")
132-
print("[BrowserCaptcha] 如需启用请设置 ALLOW_DOCKER_HEADED_CAPTCHA=true,并提供 DISPLAY/Xvfb")
241+
print("[BrowserCaptcha] 如需启用请设置 ALLOW_DOCKER_HEADED_CAPTCHA=true")
133242
else:
134243
if IS_DOCKER and ALLOW_DOCKER_HEADED:
135244
debug_logger.log_warning(
136-
"[BrowserCaptcha] Docker 内置浏览器打码白名单已启用,请确保 DISPLAY/Xvfb 可用"
245+
"[BrowserCaptcha] Docker 内置浏览器打码白名单已启用,personal 模式将按 headless 配置决定是否需要 DISPLAY/Xvfb"
137246
)
138247
print("[BrowserCaptcha] ✅ Docker 内置浏览器打码白名单已启用")
139248
if _ensure_nodriver_installed():
@@ -549,14 +658,18 @@ def _refresh_runtime_tunables(self):
549658
except Exception:
550659
self._fingerprint_cache_ttl_seconds = 3600.0
551660

661+
def _requires_virtual_display(self) -> bool:
662+
"""仅在显式有头模式下要求 Docker/Linux 提供 DISPLAY/Xvfb。"""
663+
return bool(IS_DOCKER and os.name == "posix" and not self.headless)
664+
552665
def _check_available(self):
553666
"""检查服务是否可用"""
554667
if DOCKER_HEADED_BLOCKED:
555668
raise RuntimeError(
556669
"检测到 Docker 环境,默认禁用内置浏览器打码。"
557-
"如需启用请设置环境变量 ALLOW_DOCKER_HEADED_CAPTCHA=true,并提供 DISPLAY/Xvfb。"
670+
"如需启用请设置环境变量 ALLOW_DOCKER_HEADED_CAPTCHA=true。"
558671
)
559-
if IS_DOCKER and not os.environ.get("DISPLAY"):
672+
if self._requires_virtual_display() and not os.environ.get("DISPLAY"):
560673
raise RuntimeError(
561674
"Docker 内置浏览器打码已启用,但 DISPLAY 未设置。"
562675
"请设置 DISPLAY(例如 :99)并启动 Xvfb。"
@@ -1294,6 +1407,13 @@ async def initialize(self):
12941407
f"[BrowserCaptcha] 指定浏览器不存在,改为自动发现: {browser_executable_path}"
12951408
)
12961409
browser_executable_path = None
1410+
if not browser_executable_path:
1411+
playwright_browser_path = _ensure_playwright_browser_path()
1412+
if playwright_browser_path:
1413+
browser_executable_path = playwright_browser_path
1414+
debug_logger.log_info(
1415+
f"[BrowserCaptcha] 复用 playwright chromium 作为 nodriver 浏览器: {browser_executable_path}"
1416+
)
12971417
if browser_executable_path:
12981418
debug_logger.log_info(
12991419
f"[BrowserCaptcha] 使用指定浏览器可执行文件: {browser_executable_path}"
@@ -1361,7 +1481,8 @@ async def initialize(self):
13611481
browser_args.append('--disable-extensions')
13621482

13631483
effective_launch_args = list(browser_args)
1364-
await self._wait_for_display_ready(display_value)
1484+
if self._requires_virtual_display():
1485+
await self._wait_for_display_ready(display_value)
13651486

13661487
effective_uid = "n/a"
13671488
if hasattr(os, "geteuid"):

src/services/flow_client.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -809,11 +809,12 @@ async def upload_image(
809809
ext = "png" if "png" in mime_type else "jpg"
810810
upload_file_name = f"flow2api_upload_{int(time.time() * 1000)}.{ext}"
811811
new_url = f"{self.api_base_url}/flow/uploadImage"
812+
normalized_project_id = str(project_id or "").strip()
812813
new_client_context = {
813814
"tool": "PINHOLE"
814815
}
815-
if project_id:
816-
new_client_context["projectId"] = project_id
816+
if normalized_project_id:
817+
new_client_context["projectId"] = normalized_project_id
817818

818819
new_json_data = {
819820
"clientContext": new_client_context,
@@ -860,6 +861,23 @@ async def upload_image(
860861
raise Exception(f"Invalid upload response: missing media id, keys={list(new_result.keys())}")
861862
except Exception as new_upload_error:
862863
last_error = new_upload_error
864+
retry_reason = "网络超时" if self._is_timeout_error(new_upload_error) else self._get_retry_reason(str(new_upload_error))
865+
866+
# 旧接口不携带 projectId,带项目上下文的上传一旦回退就可能把图片挂到错误项目。
867+
if normalized_project_id:
868+
if retry_reason and retry_attempt < max_retries - 1:
869+
debug_logger.log_warning(
870+
f"[UPLOAD] Project-scoped upload 遇到{retry_reason},准备重试新版接口 "
871+
f"({retry_attempt + 2}/{max_retries}, project_id={normalized_project_id})..."
872+
)
873+
await asyncio.sleep(1)
874+
continue
875+
raise RuntimeError(
876+
"Project-scoped image upload failed via /flow/uploadImage; "
877+
"legacy :uploadUserImage fallback is disabled because it may attach media "
878+
f"to a different project (project_id={normalized_project_id})."
879+
) from new_upload_error
880+
863881
debug_logger.log_warning(
864882
f"[UPLOAD] New upload API failed, fallback to legacy endpoint: {new_upload_error}"
865883
)

0 commit comments

Comments
 (0)