feat: harden video analysis and runtime resilience
This commit is contained in:
1 parent
5ada543a09
commit
bb16991f2c
55 files changed
+5023
-437
No files matched your search
@@ -78,7 +78,9 @@ class AndroidDeviceController(MobileDeviceController):
|
||||
self.data_engine_start_time = data_engine_start_time
|
||||
self.data_engine = data_engine
|
||||
self._device: AdbDevice | None = None
|
||||
self._segment_cache: dict[tuple[float, float | None], VideoRecordingResult] = {}
|
||||
self._segment_cache: dict[
|
||||
tuple[str, int, float, float], VideoRecordingResult
|
||||
] = {}
|
||||
|
||||
@property
|
||||
def device(self) -> AdbDevice:
|
||||
@@ -734,19 +736,6 @@ class AndroidDeviceController(MobileDeviceController):
|
||||
end_relative_time: float | None = None,
|
||||
) -> VideoRecordingResult:
|
||||
"""Get a video segment for a specific time range (relative to video start)."""
|
||||
cache_key = (
|
||||
round(start_relative_time, 1),
|
||||
round(end_relative_time, 1) if end_relative_time is not None else None,
|
||||
)
|
||||
if cache_key in self._segment_cache:
|
||||
cached_res = self._segment_cache[cache_key]
|
||||
if cached_res.success and cached_res.video_path and cached_res.video_path.exists():
|
||||
logger.info(
|
||||
"Reusing cached trimmed video segment for range"
|
||||
f" {cache_key[0]}s to {cache_key[1]}s"
|
||||
)
|
||||
return cached_res
|
||||
|
||||
session = get_active_session(self.device_id)
|
||||
if not session:
|
||||
return VideoRecordingResult(
|
||||
@@ -754,6 +743,22 @@ class AndroidDeviceController(MobileDeviceController):
|
||||
message=f"No active recording for device {self.device_id}",
|
||||
)
|
||||
|
||||
cache_key = None
|
||||
if end_relative_time is not None:
|
||||
cache_key = (
|
||||
str(session.video_id),
|
||||
session.generation,
|
||||
round(start_relative_time, 1),
|
||||
round(end_relative_time, 1),
|
||||
)
|
||||
cached_res = self._segment_cache.get(cache_key)
|
||||
if cached_res and cached_res.success and cached_res.video_path and cached_res.video_path.exists():
|
||||
logger.info(
|
||||
"Reusing generation-scoped trimmed video segment for range"
|
||||
f" {cache_key[2]}s to {cache_key[3]}s"
|
||||
)
|
||||
return cached_res
|
||||
|
||||
try:
|
||||
mkv_path = session.local_video_path
|
||||
if not mkv_path or not mkv_path.exists():
|
||||
@@ -776,6 +781,7 @@ class AndroidDeviceController(MobileDeviceController):
|
||||
# Try to restart scrcpy
|
||||
try:
|
||||
session.android_segment_index += 1
|
||||
session.generation = session.android_segment_index
|
||||
output_dir = mkv_path.parent
|
||||
new_video_path = output_dir / f"recording_{session.android_segment_index}.mkv"
|
||||
|
||||
@@ -890,8 +896,15 @@ class AndroidDeviceController(MobileDeviceController):
|
||||
duration_seconds=round(duration, 2),
|
||||
actual_start_relative_time=actual_start,
|
||||
warning=truncation_warning,
|
||||
video_id=session.video_id,
|
||||
generation=session.generation,
|
||||
sealed_until=session.sealed_until,
|
||||
source_revision=(
|
||||
f"{session.video_id}:{session.generation}:{round(actual_end, 3)}"
|
||||
),
|
||||
)
|
||||
self._segment_cache[cache_key] = res
|
||||
if cache_key is not None:
|
||||
self._segment_cache[cache_key] = res
|
||||
return res
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -25,5 +25,17 @@ def create_device_controller(ctx: ArtemisContext) -> UnifiedMobileController:
|
||||
|
||||
|
||||
def get_controller(ctx: ArtemisContext) -> UnifiedMobileController:
|
||||
"""Backward-compatible alias for create_device_controller."""
|
||||
return create_device_controller(ctx)
|
||||
"""Return the controller shared by one execution context.
|
||||
|
||||
Sharing preserves recording-segment caches and prevents every tool call
|
||||
from constructing a new driver/controller pair.
|
||||
"""
|
||||
existing = getattr(ctx, "_mobile_controller", None)
|
||||
if isinstance(existing, UnifiedMobileController):
|
||||
return existing
|
||||
controller = create_device_controller(ctx)
|
||||
try:
|
||||
ctx._mobile_controller = controller
|
||||
except (AttributeError, ValueError):
|
||||
pass
|
||||
return controller
|
||||
@@ -61,7 +61,9 @@ class UnifiedMobileController:
|
||||
def __init__(self, ctx: ArtemisContext):
|
||||
self.ctx = ctx
|
||||
self._driver: BaseDeviceDriver = get_driver(ctx)
|
||||
self._segment_cache: dict[tuple[float, float | None], VideoRecordingResult] = {}
|
||||
self._segment_cache: dict[
|
||||
tuple[str, int, float, float], VideoRecordingResult
|
||||
] = {}
|
||||
|
||||
@property
|
||||
def driver(self) -> BaseDeviceDriver:
|
||||
@@ -290,18 +292,6 @@ class UnifiedMobileController:
|
||||
output_path: Path | None = None,
|
||||
) -> VideoRecordingResult:
|
||||
"""Get a video segment for a specific time range (relative to video start)."""
|
||||
cache_key = (
|
||||
round(start_time, 1),
|
||||
round(end_time, 1) if end_time is not None else None,
|
||||
)
|
||||
if cache_key in self._segment_cache:
|
||||
cached_res = self._segment_cache[cache_key]
|
||||
if cached_res.success and cached_res.video_path and cached_res.video_path.exists():
|
||||
logger.info(
|
||||
f"Reusing cached trimmed video segment for range {cache_key[0]}s to {cache_key[1]}s"
|
||||
)
|
||||
return cached_res
|
||||
|
||||
device_id = self._get_device_id()
|
||||
|
||||
# Handle mock driver
|
||||
@@ -324,6 +314,22 @@ class UnifiedMobileController:
|
||||
message=f"No active recording for device {device_id}",
|
||||
)
|
||||
|
||||
cache_key = None
|
||||
if end_time is not None:
|
||||
cache_key = (
|
||||
str(session.video_id),
|
||||
session.generation,
|
||||
round(start_time, 1),
|
||||
round(end_time, 1),
|
||||
)
|
||||
cached_res = self._segment_cache.get(cache_key)
|
||||
if cached_res and cached_res.success and cached_res.video_path and cached_res.video_path.exists():
|
||||
logger.info(
|
||||
"Reusing generation-scoped trimmed video segment for range "
|
||||
f"{cache_key[2]}s to {cache_key[3]}s"
|
||||
)
|
||||
return cached_res
|
||||
|
||||
try:
|
||||
mkv_path = session.local_video_path
|
||||
if not mkv_path:
|
||||
@@ -425,8 +431,15 @@ class UnifiedMobileController:
|
||||
duration_seconds=round(duration, 2),
|
||||
actual_start_relative_time=actual_start,
|
||||
warning=truncation_warning,
|
||||
video_id=session.video_id,
|
||||
generation=session.generation,
|
||||
sealed_until=session.sealed_until,
|
||||
source_revision=(
|
||||
f"{session.video_id}:{session.generation}:{round(actual_end, 3)}"
|
||||
),
|
||||
)
|
||||
self._segment_cache[cache_key] = res
|
||||
if cache_key is not None:
|
||||
self._segment_cache[cache_key] = res
|
||||
return res
|
||||
|
||||
except Exception as e:
|
||||
@@ -469,12 +482,17 @@ class UnifiedMobileController:
|
||||
"start": max(0.0, start_time - session.start_time),
|
||||
"end": max(0.0, end_time - session.start_time),
|
||||
"rotation": session.android_rotation,
|
||||
"generation": session.generation,
|
||||
"conversion_scheduled": True,
|
||||
}
|
||||
)
|
||||
session.android_conversion_tasks.append(
|
||||
asyncio.create_task(self._remux_segment_record(session.android_segment_records[-1]))
|
||||
)
|
||||
session.sealed_until = max(
|
||||
session.sealed_until,
|
||||
max(0.0, end_time - session.start_time),
|
||||
)
|
||||
|
||||
def _record_recording_failure(self, session: RecordingSession, message: str) -> None:
|
||||
if self.ctx and self.ctx.data_engine:
|
||||
@@ -490,6 +508,7 @@ class UnifiedMobileController:
|
||||
self, session: RecordingSession, display_state: tuple[int, int, int] | None
|
||||
) -> bool:
|
||||
session.android_segment_index += 1
|
||||
session.generation = session.android_segment_index
|
||||
output_dir = session.local_video_path.parent
|
||||
new_video_path = output_dir / f"recording_{session.android_segment_index:03d}.mkv"
|
||||
process = await self._spawn_scrcpy(
|
||||
@@ -635,6 +654,10 @@ class UnifiedMobileController:
|
||||
return VideoRecordingResult(
|
||||
success=True,
|
||||
message=f"Recording started on {device_id}",
|
||||
video_id=session.video_id,
|
||||
generation=session.generation,
|
||||
sealed_until=session.sealed_until,
|
||||
source_revision=f"{session.video_id}:{session.generation}:active",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -743,6 +766,10 @@ class UnifiedMobileController:
|
||||
success=True,
|
||||
message=f"Recording stopped, saved {len(mp4_paths)} orientation-safe segments",
|
||||
video_path=mp4_paths[0],
|
||||
video_id=session.video_id,
|
||||
generation=session.generation,
|
||||
sealed_until=session.sealed_until,
|
||||
source_revision=f"{session.video_id}:{session.generation}:sealed",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in new issue
Block a user