feat: improve rpa video timing and narration
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 20s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-08 11:50:54 +08:00
parent 0e2b73e1ad
commit 857e7e3d73
4 changed files with 212 additions and 10 deletions

View File

@@ -23,7 +23,7 @@ __all__ = [
"LaunchError",
"GatewayDownError",
]
__version__ = "1.0.12"
__version__ = "1.0.13"
def __getattr__(name: str):

View File

@@ -45,6 +45,10 @@ _VOICEOVER_MAX_CHARS = 60
_VOICEOVER_DEDUP_SECONDS = 10.0
_VOICEOVER_MIN_GAP_SECONDS = 2.0
_INTRO_BUFFER_SECONDS = 5.0
_INTRO_STABILIZE_SECONDS = 1.0
_OUTRO_BUFFER_SECONDS = 5.0
_VOICEOVER_SKIP_SUBSTRINGS = (
"等待 1-5s",
"跳过重复",
@@ -54,6 +58,25 @@ _VOICEOVER_SKIP_SUBSTRINGS = (
"失败",
)
_VOICEOVER_IMPORTANT_SUBSTRINGS = (
"开始",
"打开",
"启动",
"定位",
"输入",
"点击",
"搜索",
"等待搜索结果",
"检查登录状态",
"处理验证码",
"等待人工验证",
"解析",
"提取",
"采集完成",
"部分完成",
"完成",
)
# 明显技术日志:时间戳前缀、纯数字进度等
_TECH_LOG_RE = re.compile(
r"^\d{4}-\d{2}-\d{2}|^\[\w+\]|^DEBUG:|^INFO:|^WARN:|^ERROR:"
@@ -173,10 +196,17 @@ def _voiceover_text_for_step(text: str) -> Optional[str]:
return cleaned
def _is_important_voiceover(text: str) -> bool:
for important in _VOICEOVER_IMPORTANT_SUBSTRINGS:
if important in text:
return True
return False
def _select_voiceover_clips(
entries: List[_StepEntry],
) -> List[tuple[_StepEntry, str]]:
"""按时间排序,去重并保证旁白间隔。"""
"""按时间排序,去重并保证旁白间隔;关键动作可绕过最小间隔"""
result: List[tuple[_StepEntry, str]] = []
last_text_at: Dict[str, float] = {}
last_spoken_at = -_VOICEOVER_MIN_GAP_SECONDS
@@ -188,7 +218,10 @@ def _select_voiceover_clips(
prev = last_text_at.get(spoken)
if prev is not None and entry.start - prev < _VOICEOVER_DEDUP_SECONDS:
continue
if entry.start - last_spoken_at < _VOICEOVER_MIN_GAP_SECONDS:
if (
not _is_important_voiceover(spoken)
and entry.start - last_spoken_at < _VOICEOVER_MIN_GAP_SECONDS
):
continue
last_text_at[spoken] = entry.start
last_spoken_at = entry.start
@@ -534,11 +567,13 @@ class RpaVideoSession:
skill_data_dir: str,
batch_id: str,
title: str = "",
closing_title: str = "",
) -> None:
self.skill_slug = skill_slug
self.skill_data_dir = os.path.abspath(skill_data_dir)
self.batch_id = batch_id
self.title = title
self.closing_title = closing_title
self.enabled = _record_video_enabled()
self.warnings: List[str] = []
self.output_video_path: Optional[str] = None
@@ -684,16 +719,29 @@ class RpaVideoSession:
self._close_ffmpeg_record_log()
time.sleep(0.3)
async def _safe_add_step(self, text: str, *, duration: float = 4.0) -> None:
try:
self.add_step(text, duration=duration)
except Exception:
pass
async def __aenter__(self) -> "RpaVideoSession":
if self.enabled:
self._t0 = time.monotonic()
if self.title:
self.add_step(self.title, duration=3.0)
self._start_ffmpeg_capture()
await asyncio.sleep(_INTRO_STABILIZE_SECONDS)
if self.title:
await self._safe_add_step(self.title, duration=4.0)
intro_wait = _INTRO_BUFFER_SECONDS - _INTRO_STABILIZE_SECONDS
if intro_wait > 0:
await asyncio.sleep(intro_wait)
return self
async def __aexit__(self, exc_type, exc, tb) -> None:
if self.enabled:
if self.closing_title:
await self._safe_add_step(self.closing_title, duration=4.0)
await asyncio.sleep(_OUTRO_BUFFER_SECONDS)
self._stop_ffmpeg_capture()
await self.finalize()