Release v1.0.10: shared media_assets and screencast media_assets_root override
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 17s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-06 10:32:35 +08:00
parent aab0207aec
commit 434a405e19
14 changed files with 949 additions and 114 deletions

View File

@@ -4,8 +4,6 @@ from __future__ import annotations
import asyncio
import os
import random
import shutil
import subprocess
import sys
import time
@@ -15,6 +13,12 @@ from pathlib import Path
from typing import Any, Dict, IO, List, Optional
from .. import config
from ..media_assets import (
background_music_issue,
ensure_media_assets,
pick_background_music,
resolve_ffmpeg,
)
_JIANGCHANG_SUBTITLE_STYLE = (
"FontName=Arial,FontSize=14,"
@@ -22,11 +26,6 @@ _JIANGCHANG_SUBTITLE_STYLE = (
"Outline=2,Shadow=1,Alignment=2"
)
_DEFAULT_MUSIC_ROOT = os.environ.get(
"MEDIA_ASSETS_ROOT",
r"D:\OpenClaw\client-commons\media-assets",
)
_CAPTURE_FRAMERATE = 15
@@ -34,14 +33,23 @@ def _record_video_enabled() -> bool:
return config.get_bool("OPENCLAW_RECORD_VIDEO", default=False)
def _resolve_music_file() -> Optional[Path]:
music_dir = Path(_DEFAULT_MUSIC_ROOT) / "music"
if not music_dir.is_dir():
def _resolve_ffmpeg_exe() -> Optional[Path]:
ffmpeg = resolve_ffmpeg()
if ffmpeg is None:
return None
files = list(music_dir.rglob("*.mp3"))
if not files:
return None
return random.choice(files)
path = Path(ffmpeg)
return path if path.is_file() else None
def _resolve_music_file(warnings: List[str]) -> Optional[Path]:
music = pick_background_music()
if music is not None:
return music
issue = background_music_issue()
if issue:
warnings.append(issue)
return None
@dataclass
@@ -87,7 +95,12 @@ def _escape_srt_path(path: str) -> str:
return path.replace("\\", "/").replace(":", "\\:")
def _run_ffmpeg(cmd: List[str]) -> tuple[bool, str]:
def _ffmpeg_cmd(ffmpeg_exe: Path, *args: str) -> List[str]:
return [str(ffmpeg_exe), *args]
def _run_ffmpeg(ffmpeg_exe: Path, cmd_tail: List[str]) -> tuple[bool, str]:
cmd = _ffmpeg_cmd(ffmpeg_exe, *cmd_tail)
try:
result = subprocess.run(
cmd,
@@ -104,12 +117,12 @@ def _run_ffmpeg(cmd: List[str]) -> tuple[bool, str]:
return True, ""
def _build_desktop_record_cmd(capture_path: str) -> Optional[List[str]]:
def _build_desktop_record_cmd(ffmpeg_exe: Path, capture_path: str) -> Optional[List[str]]:
"""Windows gdigrab 全桌面录制;非 Windows 返回 None。"""
if sys.platform != "win32":
return None
return [
"ffmpeg",
return _ffmpeg_cmd(
ffmpeg_exe,
"-y",
"-f",
"gdigrab",
@@ -124,10 +137,11 @@ def _build_desktop_record_cmd(capture_path: str) -> Optional[List[str]]:
"-pix_fmt",
"yuv420p",
capture_path,
]
)
def _compose_capture_mp4(
ffmpeg_exe: Path,
capture_path: Path,
srt_path: Path,
output_path: Path,
@@ -147,8 +161,7 @@ def _compose_capture_mp4(
f"[0:v]{subtitle_filter}[vout];"
f"[1:a]volume={music_volume},apad[aout]"
)
cmd = [
"ffmpeg",
cmd_tail = [
"-y",
"-i",
str(capture_path),
@@ -176,8 +189,7 @@ def _compose_capture_mp4(
str(output_path),
]
else:
cmd = [
"ffmpeg",
cmd_tail = [
"-y",
"-i",
str(capture_path),
@@ -197,7 +209,7 @@ def _compose_capture_mp4(
"yuv420p",
str(output_path),
]
return _run_ffmpeg(cmd)
return _run_ffmpeg(ffmpeg_exe, cmd_tail)
class RpaVideoSession:
@@ -287,11 +299,13 @@ class RpaVideoSession:
def _start_ffmpeg_capture(self) -> None:
if not self.enabled:
return
if not shutil.which("ffmpeg"):
ffmpeg_exe = _resolve_ffmpeg_exe()
if ffmpeg_exe is None:
self.warnings.append("ffmpeg_not_found")
return
cmd = _build_desktop_record_cmd(self._capture_path)
cmd = _build_desktop_record_cmd(ffmpeg_exe, self._capture_path)
if cmd is None:
self._append_warning(
"ffmpeg_record_start_failed",
@@ -391,14 +405,21 @@ class RpaVideoSession:
except Exception as exc:
self.warnings.append(f"subtitle_write_failed: {exc}")
music = _resolve_music_file()
if music is None:
music = _resolve_music_file(self.warnings)
if music is None and "background_music_lfs_pointer_only" not in self.warnings and "background_music_invalid" not in self.warnings:
self.warnings.append("background_music_missing")
ffmpeg_exe = _resolve_ffmpeg_exe()
if ffmpeg_exe is None:
self.warnings.append("ffmpeg_not_found")
self.artifacts = self.summary()
return
srt_file = Path(self._srt_path)
use_srt = srt_file.is_file() and srt_file.stat().st_size > 0
ok, err = _compose_capture_mp4(
ffmpeg_exe,
capture,
srt_file if use_srt else Path(self._srt_path),
Path(self._planned_output),
@@ -407,6 +428,7 @@ class RpaVideoSession:
if not ok and music is not None:
self.warnings.append(f"ffmpeg_with_music_failed: {err}")
ok, err = _compose_capture_mp4(
ffmpeg_exe,
capture,
srt_file if use_srt else Path(self._srt_path),
Path(self._planned_output),
@@ -414,8 +436,8 @@ class RpaVideoSession:
)
if not ok and not use_srt:
ok, err = _run_ffmpeg(
ffmpeg_exe,
[
"ffmpeg",
"-y",
"-i",
str(capture),
@@ -428,7 +450,7 @@ class RpaVideoSession:
"-pix_fmt",
"yuv420p",
str(self._planned_output),
]
],
)
if not ok:
self._append_warning("ffmpeg_compose_failed", err)