chore: sync ffmpeg RpaVideoSession, remove Playwright recording
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -159,10 +159,11 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
|
|||||||
|
|
||||||
- **路径**:`{CLAW_DATA_ROOT}/{user}/{slug}/rpa-artifacts/{batch_id}/{tag}_{timestamp}.png`
|
- **路径**:`{CLAW_DATA_ROOT}/{user}/{slug}/rpa-artifacts/{batch_id}/{tag}_{timestamp}.png`
|
||||||
- **失败必截图**:受 `OPENCLAW_ARTIFACTS_ON_FAILURE`(默认开)控制。
|
- **失败必截图**:受 `OPENCLAW_ARTIFACTS_ON_FAILURE`(默认开)控制。
|
||||||
- **运行录屏成片**:RPA 型技能默认 `OPENCLAW_RECORD_VIDEO=1`,表示录屏、中文字幕、背景音乐、最终 MP4 **一体开启**。由 service 核心中的 `RpaVideoSession` 统一触发(见 `jiangchang_skill_core.rpa.video_session`),不在 CLI/E2E 入口单独实现。
|
- **运行录屏成片**:RPA 型技能默认 `OPENCLAW_RECORD_VIDEO=1`,表示 **ffmpeg 录制运行过程** + 字幕 + 背景音乐 + 最终 MP4 **一体开启**。由 `RpaVideoSession` 统一触发,不在 CLI/E2E 单独实现。
|
||||||
- **最终视频**:`{skill_data_dir}/videos/{skill_slug}_{yyyyMMdd_HHmmss}_{batch_id}.mp4`。
|
- **Playwright 不负责录屏**,仅浏览器自动化。
|
||||||
- **中间产物**:`{skill_data_dir}/rpa-artifacts/{batch_id}/`(raw-video、字幕、失败截图等),**不放最终 MP4**。
|
- **ffmpeg 是唯一录屏器**(Windows:`gdigrab` + `desktop`)。
|
||||||
- Playwright 使用 `record_video_dir` 指向 `rpa-artifacts/{batch_id}/raw-video/`。
|
- **最终视频**:`{skill_data_dir}/videos/{skill_slug}_{yyyyMMdd_HHmmss}_{batch_id}.mp4`
|
||||||
|
- **中间产物**:`rpa-artifacts/{batch_id}/capture.mp4`、`subtitles/`、`logs/` 等,**不放最终 MP4**。
|
||||||
- **常见 tag**:`before_submit` / `after_submit` / `captcha` / `login_fail` / `error`。
|
- **常见 tag**:`before_submit` / `after_submit` / `captcha` / `login_fail` / `error`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# vendored from jiangchang-platform-kit; do not edit here, edit the kit.
|
"""统一 persistent context 启动封装(仅浏览器自动化,不负责录屏)。"""
|
||||||
"""统一 persistent context 启动封装。"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -25,12 +24,11 @@ async def launch_persistent_browser(
|
|||||||
executable_path: str | None = None,
|
executable_path: str | None = None,
|
||||||
headless: bool | None = None,
|
headless: bool | None = None,
|
||||||
extra_args: list[str] | None = None,
|
extra_args: list[str] | None = None,
|
||||||
record_video_dir: str | None = None,
|
|
||||||
) -> "BrowserContext":
|
) -> "BrowserContext":
|
||||||
"""用 stealth 参数启动 persistent context + 注入 STEALTH_INIT_SCRIPT。
|
"""用 stealth 参数启动 persistent context + 注入 STEALTH_INIT_SCRIPT。
|
||||||
|
|
||||||
headless=None 时读 OPENCLAW_BROWSER_HEADLESS。
|
headless=None 时读 OPENCLAW_BROWSER_HEADLESS。
|
||||||
record_video_dir 非空则开录屏。
|
录屏由 RpaVideoSession(ffmpeg)负责,本函数不向 Playwright 传递任何录屏参数。
|
||||||
"""
|
"""
|
||||||
if headless is None:
|
if headless is None:
|
||||||
v = (os.environ.get("OPENCLAW_BROWSER_HEADLESS") or "0").strip().lower()
|
v = (os.environ.get("OPENCLAW_BROWSER_HEADLESS") or "0").strip().lower()
|
||||||
@@ -53,8 +51,6 @@ async def launch_persistent_browser(
|
|||||||
)
|
)
|
||||||
if ignore is not None:
|
if ignore is not None:
|
||||||
launch_kwargs["ignore_default_args"] = ignore
|
launch_kwargs["ignore_default_args"] = ignore
|
||||||
if record_video_dir:
|
|
||||||
launch_kwargs["record_video_dir"] = record_video_dir
|
|
||||||
|
|
||||||
context = await playwright.chromium.launch_persistent_context(**launch_kwargs)
|
context = await playwright.chromium.launch_persistent_context(**launch_kwargs)
|
||||||
if stealth_enabled():
|
if stealth_enabled():
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
"""RPA 运行录屏会话:Playwright raw video → 字幕 + 背景音乐 → 最终 MP4。"""
|
"""RPA 运行录屏会话:ffmpeg 桌面录制 → 字幕 + 背景音乐 → 最终 MP4。
|
||||||
|
|
||||||
|
Playwright 仅用于浏览器自动化,不负责录屏。
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -15,7 +19,6 @@ from typing import Any, Dict, List, Optional
|
|||||||
|
|
||||||
from .. import config
|
from .. import config
|
||||||
|
|
||||||
# 与 screencast/composer.py 保持一致的字幕样式
|
|
||||||
_JIANGCHANG_SUBTITLE_STYLE = (
|
_JIANGCHANG_SUBTITLE_STYLE = (
|
||||||
"FontName=Arial,FontSize=14,"
|
"FontName=Arial,FontSize=14,"
|
||||||
"PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,"
|
"PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,"
|
||||||
@@ -27,6 +30,8 @@ _DEFAULT_MUSIC_ROOT = os.environ.get(
|
|||||||
r"D:\OpenClaw\client-commons\media-assets",
|
r"D:\OpenClaw\client-commons\media-assets",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_CAPTURE_FRAMERATE = 15
|
||||||
|
|
||||||
|
|
||||||
def _record_video_enabled() -> bool:
|
def _record_video_enabled() -> bool:
|
||||||
return config.get_bool("OPENCLAW_RECORD_VIDEO", default=False)
|
return config.get_bool("OPENCLAW_RECORD_VIDEO", default=False)
|
||||||
@@ -50,7 +55,6 @@ class _StepEntry:
|
|||||||
|
|
||||||
|
|
||||||
def _generate_srt(entries: List[_StepEntry], output_path: str) -> None:
|
def _generate_srt(entries: List[_StepEntry], output_path: str) -> None:
|
||||||
"""根据步骤时间线生成 SRT(防重叠)。"""
|
|
||||||
MIN_DURATION = 1.0
|
MIN_DURATION = 1.0
|
||||||
GAP = 0.1
|
GAP = 0.1
|
||||||
steps = sorted(entries, key=lambda e: e.start)
|
steps = sorted(entries, key=lambda e: e.start)
|
||||||
@@ -86,16 +90,6 @@ def _escape_srt_path(path: str) -> str:
|
|||||||
return path.replace("\\", "/").replace(":", "\\:")
|
return path.replace("\\", "/").replace(":", "\\:")
|
||||||
|
|
||||||
|
|
||||||
def _find_raw_webm(raw_dir: str) -> Optional[Path]:
|
|
||||||
root = Path(raw_dir)
|
|
||||||
if not root.is_dir():
|
|
||||||
return None
|
|
||||||
webms = list(root.rglob("*.webm"))
|
|
||||||
if not webms:
|
|
||||||
return None
|
|
||||||
return max(webms, key=lambda p: p.stat().st_size)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_ffmpeg(cmd: List[str]) -> tuple[bool, str]:
|
def _run_ffmpeg(cmd: List[str]) -> tuple[bool, str]:
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -113,8 +107,31 @@ def _run_ffmpeg(cmd: List[str]) -> tuple[bool, str]:
|
|||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
def _compose_webm_mp4(
|
def _build_desktop_record_cmd(capture_path: str) -> Optional[List[str]]:
|
||||||
webm_path: Path,
|
"""Windows gdigrab 全桌面录制;非 Windows 返回 None。"""
|
||||||
|
if sys.platform != "win32":
|
||||||
|
return None
|
||||||
|
return [
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-f",
|
||||||
|
"gdigrab",
|
||||||
|
"-framerate",
|
||||||
|
str(_CAPTURE_FRAMERATE),
|
||||||
|
"-i",
|
||||||
|
"desktop",
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
"ultrafast",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
capture_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_capture_mp4(
|
||||||
|
capture_path: Path,
|
||||||
srt_path: Path,
|
srt_path: Path,
|
||||||
output_path: Path,
|
output_path: Path,
|
||||||
*,
|
*,
|
||||||
@@ -134,39 +151,60 @@ def _compose_webm_mp4(
|
|||||||
f"[1:a]volume={music_volume},apad[aout]"
|
f"[1:a]volume={music_volume},apad[aout]"
|
||||||
)
|
)
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg", "-y",
|
"ffmpeg",
|
||||||
"-i", str(webm_path),
|
"-y",
|
||||||
"-i", str(music_file),
|
"-i",
|
||||||
"-filter_complex", filter_complex,
|
str(capture_path),
|
||||||
"-map", "[vout]",
|
"-i",
|
||||||
"-map", "[aout]",
|
str(music_file),
|
||||||
"-c:v", "libx264",
|
"-filter_complex",
|
||||||
"-preset", "fast",
|
filter_complex,
|
||||||
"-crf", "23",
|
"-map",
|
||||||
"-c:a", "aac",
|
"[vout]",
|
||||||
"-b:a", "128k",
|
"-map",
|
||||||
|
"[aout]",
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
"fast",
|
||||||
|
"-crf",
|
||||||
|
"23",
|
||||||
|
"-c:a",
|
||||||
|
"aac",
|
||||||
|
"-b:a",
|
||||||
|
"128k",
|
||||||
"-shortest",
|
"-shortest",
|
||||||
"-pix_fmt", "yuv420p",
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
str(output_path),
|
str(output_path),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg", "-y",
|
"ffmpeg",
|
||||||
"-i", str(webm_path),
|
"-y",
|
||||||
"-vf", subtitle_filter,
|
"-i",
|
||||||
"-c:v", "libx264",
|
str(capture_path),
|
||||||
"-preset", "fast",
|
"-vf",
|
||||||
"-crf", "23",
|
subtitle_filter,
|
||||||
"-c:a", "aac",
|
"-c:v",
|
||||||
"-b:a", "128k",
|
"libx264",
|
||||||
"-pix_fmt", "yuv420p",
|
"-preset",
|
||||||
|
"fast",
|
||||||
|
"-crf",
|
||||||
|
"23",
|
||||||
|
"-c:a",
|
||||||
|
"aac",
|
||||||
|
"-b:a",
|
||||||
|
"128k",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
str(output_path),
|
str(output_path),
|
||||||
]
|
]
|
||||||
return _run_ffmpeg(cmd)
|
return _run_ffmpeg(cmd)
|
||||||
|
|
||||||
|
|
||||||
class RpaVideoSession:
|
class RpaVideoSession:
|
||||||
"""Service/RPA 层统一录屏会话;OPENCLAW_RECORD_VIDEO=1 时启用完整成片流程。"""
|
"""Service/RPA 层统一 ffmpeg 桌面录屏;OPENCLAW_RECORD_VIDEO=1 时启用成片流程。"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -183,6 +221,8 @@ class RpaVideoSession:
|
|||||||
self.enabled = _record_video_enabled()
|
self.enabled = _record_video_enabled()
|
||||||
self.warnings: List[str] = []
|
self.warnings: List[str] = []
|
||||||
self.output_video_path: Optional[str] = None
|
self.output_video_path: Optional[str] = None
|
||||||
|
self.capture_path: Optional[str] = None
|
||||||
|
# 已废弃:勿用于 Playwright;始终为 None。
|
||||||
self.record_video_dir: Optional[str] = None
|
self.record_video_dir: Optional[str] = None
|
||||||
self.artifacts: Dict[str, Any] = {}
|
self.artifacts: Dict[str, Any] = {}
|
||||||
|
|
||||||
@@ -194,24 +234,27 @@ class RpaVideoSession:
|
|||||||
self._artifact_root = os.path.join(
|
self._artifact_root = os.path.join(
|
||||||
self.skill_data_dir, "rpa-artifacts", batch_id
|
self.skill_data_dir, "rpa-artifacts", batch_id
|
||||||
)
|
)
|
||||||
self._raw_video_dir = os.path.join(self._artifact_root, "raw-video")
|
|
||||||
self._subtitles_dir = os.path.join(self._artifact_root, "subtitles")
|
self._subtitles_dir = os.path.join(self._artifact_root, "subtitles")
|
||||||
self._logs_dir = os.path.join(self._artifact_root, "logs")
|
self._logs_dir = os.path.join(self._artifact_root, "logs")
|
||||||
|
self._capture_path = os.path.join(self._artifact_root, "capture.mp4")
|
||||||
self._srt_path = os.path.join(self._subtitles_dir, f"{self._video_stem}.srt")
|
self._srt_path = os.path.join(self._subtitles_dir, f"{self._video_stem}.srt")
|
||||||
self._planned_output = os.path.join(self._videos_dir, f"{self._video_stem}.mp4")
|
self._planned_output = os.path.join(self._videos_dir, f"{self._video_stem}.mp4")
|
||||||
|
|
||||||
self._steps: List[_StepEntry] = []
|
self._steps: List[_StepEntry] = []
|
||||||
self._t0: Optional[float] = None
|
self._t0: Optional[float] = None
|
||||||
|
self._ffmpeg_proc: Optional[subprocess.Popen] = None
|
||||||
|
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
os.makedirs(self._videos_dir, exist_ok=True)
|
os.makedirs(self._videos_dir, exist_ok=True)
|
||||||
os.makedirs(self._raw_video_dir, exist_ok=True)
|
|
||||||
os.makedirs(self._subtitles_dir, exist_ok=True)
|
os.makedirs(self._subtitles_dir, exist_ok=True)
|
||||||
os.makedirs(self._logs_dir, exist_ok=True)
|
os.makedirs(self._logs_dir, exist_ok=True)
|
||||||
self.record_video_dir = self._raw_video_dir
|
os.makedirs(self._artifact_root, exist_ok=True)
|
||||||
|
|
||||||
def step(self, text: str, *, duration: float = 4.0) -> None:
|
def step(self, text: str, *, duration: float = 4.0) -> None:
|
||||||
"""记录一步骤字幕(中文说明)。"""
|
self.add_step(text, duration=duration)
|
||||||
|
|
||||||
|
def add_step(self, text: str, *, duration: float = 4.0) -> None:
|
||||||
|
"""记录一步骤字幕(相对录屏开始时间)。"""
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return
|
return
|
||||||
if self._t0 is None:
|
if self._t0 is None:
|
||||||
@@ -219,71 +262,159 @@ class RpaVideoSession:
|
|||||||
elapsed = time.monotonic() - self._t0
|
elapsed = time.monotonic() - self._t0
|
||||||
self._steps.append(_StepEntry(start=elapsed, text=text.strip(), duration=duration))
|
self._steps.append(_StepEntry(start=elapsed, text=text.strip(), duration=duration))
|
||||||
|
|
||||||
|
def _start_ffmpeg_capture(self) -> None:
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
if not shutil.which("ffmpeg"):
|
||||||
|
self.warnings.append("ffmpeg_not_found")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = _build_desktop_record_cmd(self._capture_path)
|
||||||
|
if cmd is None:
|
||||||
|
self.warnings.append(
|
||||||
|
"ffmpeg_record_start_failed: desktop capture only supported on Windows"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._ffmpeg_proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
self.capture_path = self._capture_path
|
||||||
|
except Exception as exc:
|
||||||
|
self.warnings.append(f"ffmpeg_record_start_failed: {exc}")
|
||||||
|
self._ffmpeg_proc = None
|
||||||
|
|
||||||
|
def _stop_ffmpeg_capture(self) -> None:
|
||||||
|
proc = self._ffmpeg_proc
|
||||||
|
if proc is None:
|
||||||
|
return
|
||||||
|
if proc.poll() is None:
|
||||||
|
try:
|
||||||
|
if proc.stdin:
|
||||||
|
proc.stdin.write(b"q")
|
||||||
|
proc.stdin.flush()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=20)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
try:
|
||||||
|
proc.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._ffmpeg_proc = None
|
||||||
|
await_sleep = 0.3
|
||||||
|
time.sleep(await_sleep)
|
||||||
|
|
||||||
async def __aenter__(self) -> "RpaVideoSession":
|
async def __aenter__(self) -> "RpaVideoSession":
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
self._t0 = time.monotonic()
|
self._t0 = time.monotonic()
|
||||||
if self.title:
|
if self.title:
|
||||||
self.step(self.title, duration=3.0)
|
self.add_step(self.title, duration=3.0)
|
||||||
|
self._start_ffmpeg_capture()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
|
self._stop_ffmpeg_capture()
|
||||||
await self.finalize()
|
await self.finalize()
|
||||||
|
|
||||||
async def finalize(self) -> None:
|
async def finalize(self) -> None:
|
||||||
"""在 Playwright context 已 close 后合成最终 MP4;失败只记 warning。"""
|
"""停止录制后合成最终 MP4;失败只记 warning,不抛异常。"""
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
|
self.artifacts = self.summary()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self._steps and self.title:
|
if not self._steps and self.title:
|
||||||
self.step(self.title)
|
self.add_step(self.title)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(0.5)
|
capture = Path(self._capture_path)
|
||||||
webm = _find_raw_webm(self._raw_video_dir)
|
if not capture.is_file() or capture.stat().st_size <= 0:
|
||||||
if webm is None:
|
self.warnings.append("ffmpeg_capture_missing")
|
||||||
self.warnings.append("no_playwright_raw_video")
|
self.artifacts = self.summary()
|
||||||
self.artifacts = self.to_artifact_dict()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.capture_path = str(capture.resolve())
|
||||||
|
|
||||||
entries = list(self._steps)
|
entries = list(self._steps)
|
||||||
if entries:
|
if entries:
|
||||||
entries[-1].duration = max(entries[-1].duration, 5.0)
|
entries[-1].duration = max(entries[-1].duration, 5.0)
|
||||||
_generate_srt(entries, self._srt_path)
|
try:
|
||||||
|
_generate_srt(entries, self._srt_path)
|
||||||
|
except Exception as exc:
|
||||||
|
self.warnings.append(f"subtitle_write_failed: {exc}")
|
||||||
|
|
||||||
music = _resolve_music_file()
|
music = _resolve_music_file()
|
||||||
if music is None:
|
if music is None:
|
||||||
self.warnings.append("background_music_missing")
|
self.warnings.append("background_music_missing")
|
||||||
|
|
||||||
ok, err = _compose_webm_mp4(
|
srt_file = Path(self._srt_path)
|
||||||
webm,
|
use_srt = srt_file.is_file() and srt_file.stat().st_size > 0
|
||||||
Path(self._srt_path),
|
|
||||||
|
ok, err = _compose_capture_mp4(
|
||||||
|
capture,
|
||||||
|
srt_file if use_srt else Path(self._srt_path),
|
||||||
Path(self._planned_output),
|
Path(self._planned_output),
|
||||||
music_file=music,
|
music_file=music,
|
||||||
)
|
)
|
||||||
if not ok and music is not None:
|
if not ok and music is not None:
|
||||||
self.warnings.append(f"ffmpeg_with_music_failed: {err}")
|
self.warnings.append(f"ffmpeg_with_music_failed: {err}")
|
||||||
ok, err = _compose_webm_mp4(
|
ok, err = _compose_capture_mp4(
|
||||||
webm,
|
capture,
|
||||||
Path(self._srt_path),
|
srt_file if use_srt else Path(self._srt_path),
|
||||||
Path(self._planned_output),
|
Path(self._planned_output),
|
||||||
music_file=None,
|
music_file=None,
|
||||||
)
|
)
|
||||||
|
if not ok and not use_srt:
|
||||||
|
ok, err = _run_ffmpeg(
|
||||||
|
[
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-i",
|
||||||
|
str(capture),
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
"fast",
|
||||||
|
"-crf",
|
||||||
|
"23",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
str(self._planned_output),
|
||||||
|
]
|
||||||
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
self.warnings.append(f"ffmpeg_compose_failed: {err}")
|
self.warnings.append(f"ffmpeg_compose_failed: {err}")
|
||||||
self.artifacts = self.to_artifact_dict()
|
self.artifacts = self.summary()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.output_video_path = str(Path(self._planned_output).resolve())
|
self.output_video_path = str(Path(self._planned_output).resolve())
|
||||||
self.artifacts = self.to_artifact_dict()
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.warnings.append(f"video_finalize_error: {exc}")
|
self.warnings.append(f"video_finalize_error: {exc}")
|
||||||
self.artifacts = self.to_artifact_dict()
|
|
||||||
|
|
||||||
def to_artifact_dict(self) -> Dict[str, Any]:
|
self.artifacts = self.summary()
|
||||||
|
|
||||||
|
def summary(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"enabled": self.enabled,
|
"enabled": self.enabled,
|
||||||
"path": self.output_video_path,
|
"path": self.output_video_path,
|
||||||
|
"capture_path": self.capture_path,
|
||||||
"warnings": list(self.warnings),
|
"warnings": list(self.warnings),
|
||||||
"batch_id": self.batch_id,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_artifact_dict(self) -> Dict[str, Any]:
|
||||||
|
data = self.summary()
|
||||||
|
data["batch_id"] = self.batch_id
|
||||||
|
return data
|
||||||
|
|||||||
@@ -50,14 +50,13 @@ class SimRpaAdapter(AdapterBase):
|
|||||||
batch_id=batch_id,
|
batch_id=batch_id,
|
||||||
title="技能仿真演示",
|
title="技能仿真演示",
|
||||||
) as video:
|
) as video:
|
||||||
video.step("正在打开目标页面")
|
video.add_step("正在打开目标页面")
|
||||||
try:
|
try:
|
||||||
async with async_playwright() as pw:
|
async with async_playwright() as pw:
|
||||||
context = await launch_persistent_browser(
|
context = await launch_persistent_browser(
|
||||||
pw,
|
pw,
|
||||||
profile_dir=profile_dir,
|
profile_dir=profile_dir,
|
||||||
headless=config.get_bool("OPENCLAW_BROWSER_HEADLESS"),
|
headless=config.get_bool("OPENCLAW_BROWSER_HEADLESS"),
|
||||||
record_video_dir=video.record_video_dir,
|
|
||||||
)
|
)
|
||||||
page = context.pages[0] if context.pages else await context.new_page()
|
page = context.pages[0] if context.pages else await context.new_page()
|
||||||
await page.goto(url, wait_until="domcontentloaded", timeout=60_000)
|
await page.goto(url, wait_until="domcontentloaded", timeout=60_000)
|
||||||
|
|||||||
Reference in New Issue
Block a user