feat: add Chinese UI, Responses models, and mirrored Windows setup
Localize the web console and persist custom model and capability settings. Use OpenAI Responses exclusively and move runtime limits into environment configuration. Bootstrap uv and the virtual environment on Windows, support optional China mirrors, and package source distributions without environments, build output, or credentials. Update regression tests and repository quality checks for the new behavior.
This commit is contained in:
1 parent
371aa6df56
commit
2ddec53954
183 files changed
+3427
-11857
No files matched your search
@@ -80,7 +80,7 @@ def test_circuit_breaker_opens_and_half_opens():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_chunk_is_bisected_and_successful_leaves_survive():
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.GOOGLE_API_KEY", None):
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.OPENAI_API_KEY", None):
|
||||
analyzer = VideoAnalyzer(_context())
|
||||
|
||||
async def analyze(start: float, end: float, query: str) -> str:
|
||||
@@ -98,7 +98,7 @@ async def test_timeout_chunk_is_bisected_and_successful_leaves_survive():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_universal_primary_failure_uses_configured_fallback():
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.GOOGLE_API_KEY", None):
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.OPENAI_API_KEY", None):
|
||||
analyzer = VideoAnalyzer(_context())
|
||||
|
||||
primary = MagicMock()
|
||||
@@ -126,57 +126,8 @@ async def test_universal_primary_failure_uses_configured_fallback():
|
||||
assert get_model.call_args_list[1].kwargs["use_fallback"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_native_chunk_exhaustion_commits_universal_fallback(tmp_path):
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.GOOGLE_API_KEY", None):
|
||||
analyzer = VideoAnalyzer(_context())
|
||||
analyzer.use_native_gemini = True
|
||||
raw_video = tmp_path / "segment.mp4"
|
||||
raw_video.write_bytes(b"video")
|
||||
extracted = SimpleNamespace(
|
||||
success=True,
|
||||
video_path=raw_video,
|
||||
actual_start_relative_time=0.0,
|
||||
duration_seconds=5.0,
|
||||
warning=None,
|
||||
)
|
||||
controller = SimpleNamespace(extract_segment_metadata=AsyncMock(return_value=extracted))
|
||||
fallback_text = "[from 0.0s to 5.0s] Summary: fallback recovered Analysis: grounded"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"artemis.agents.video_analyzer.video_analyzer.get_controller",
|
||||
return_value=controller,
|
||||
),
|
||||
patch(
|
||||
"artemis.agents.video_analyzer.video_analyzer.compress_video_for_api",
|
||||
new=AsyncMock(return_value=raw_video),
|
||||
),
|
||||
patch.object(
|
||||
analyzer,
|
||||
"upload_and_poll_file",
|
||||
new=AsyncMock(side_effect=RuntimeError("503 provider unavailable")),
|
||||
) as upload,
|
||||
patch.object(
|
||||
analyzer,
|
||||
"_exec_single_chunk_universal",
|
||||
new=AsyncMock(return_value=fallback_text),
|
||||
) as fallback,
|
||||
patch(
|
||||
"artemis.agents.video_analyzer.video_analyzer.asyncio.sleep",
|
||||
new=AsyncMock(),
|
||||
),
|
||||
):
|
||||
result = await analyzer._exec_single_chunk(0.0, 5.0, "verify transition")
|
||||
|
||||
assert result == fallback_text
|
||||
assert upload.await_count == 2
|
||||
fallback.assert_awaited_once()
|
||||
assert analyzer.blackboard.missing_intervals(0.0, 5.0, "verify transition") == []
|
||||
|
||||
|
||||
def test_action_timestamps_drive_dense_sampling():
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.GOOGLE_API_KEY", None):
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.OPENAI_API_KEY", None):
|
||||
analyzer = VideoAnalyzer(_context())
|
||||
step = SimpleNamespace(timestamp=104.0, action_taken={"name": "tap"})
|
||||
storage = SimpleNamespace(get_steps=lambda _session: [step])
|
||||
@@ -304,7 +255,7 @@ class _AgenticRejected(Exception):
|
||||
@pytest.mark.asyncio
|
||||
async def test_pending_chunk_sized_for_agentic_ceiling_is_not_run_statically(tmp_path):
|
||||
"""A chunk planned at 600s whose run degraded before it started is handed back."""
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.GOOGLE_API_KEY", None):
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.OPENAI_API_KEY", None):
|
||||
analyzer = VideoAnalyzer(_context())
|
||||
analyzer.use_native_gemini = True
|
||||
# A sibling chunk already flipped the run to static (60s ceiling).
|
||||
@@ -318,7 +269,7 @@ async def test_pending_chunk_sized_for_agentic_ceiling_is_not_run_statically(tmp
|
||||
return_value=controller,
|
||||
),
|
||||
patch(
|
||||
"artemis.agents.video_analyzer.chunk_native._run_native_chunk_conversation",
|
||||
"artemis.agents.video_analyzer.chunk_runner._prepare_chunk_media",
|
||||
new=AsyncMock(),
|
||||
) as static_path,
|
||||
patch.object(analyzer, "_exec_single_chunk_universal", new=AsyncMock()) as universal,
|
||||
@@ -335,83 +286,10 @@ async def test_pending_chunk_sized_for_agentic_ceiling_is_not_run_statically(tmp
|
||||
assert analyzer.blackboard.missing_intervals(0.0, 150.0, "verify toggle") == [(0.0, 150.0)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sibling_chunk_degraded_mid_run_is_replanned_into_static_pieces(tmp_path):
|
||||
"""Two 600s chunks: the first is rejected while the second is still in media prep."""
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.GOOGLE_API_KEY", None):
|
||||
analyzer = VideoAnalyzer(_context())
|
||||
analyzer.use_native_gemini = True
|
||||
analyzer.video_processing = "agentic"
|
||||
analyzer.chunk_size_seconds = analyzer.agentic_chunk_size_seconds
|
||||
assert analyzer.chunk_size_seconds == 600.0
|
||||
|
||||
raw_video = tmp_path / "segment.mp4"
|
||||
raw_video.write_bytes(b"video")
|
||||
first_rejected = asyncio.Event()
|
||||
|
||||
async def extract(start: float, end: float):
|
||||
if start >= 600.0 and analyzer.video_processing == "agentic":
|
||||
# The second 600s chunk is mid-prep when the first one degrades.
|
||||
await first_rejected.wait()
|
||||
return SimpleNamespace(
|
||||
success=True,
|
||||
video_path=raw_video,
|
||||
actual_start_relative_time=start,
|
||||
duration_seconds=end - start,
|
||||
warning=None,
|
||||
)
|
||||
|
||||
controller = SimpleNamespace(extract_segment_metadata=AsyncMock(side_effect=extract))
|
||||
agentic_calls: list[tuple[float, float]] = []
|
||||
|
||||
async def agentic(analyzer_, media, cs, ce, *rest):
|
||||
agentic_calls.append((cs, ce))
|
||||
first_rejected.set()
|
||||
raise _AgenticRejected("400 INVALID_ARGUMENT: processing 'agentic' is not supported")
|
||||
|
||||
async def static_answer(analyzer_, media, cs, ce, *rest):
|
||||
return f"[from {cs:.1f}s to {ce:.1f}s] Summary: static Analysis: ok"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"artemis.agents.video_analyzer.video_analyzer.get_controller",
|
||||
return_value=controller,
|
||||
),
|
||||
patch(
|
||||
"artemis.agents.video_analyzer.video_analyzer.compress_video_for_api",
|
||||
new=AsyncMock(return_value=raw_video),
|
||||
),
|
||||
patch(
|
||||
"artemis.agents.video_analyzer.chunk_native.run_agentic_chunk_conversation",
|
||||
new=AsyncMock(side_effect=agentic),
|
||||
),
|
||||
patch(
|
||||
"artemis.agents.video_analyzer.chunk_native._run_native_chunk_conversation",
|
||||
new=AsyncMock(side_effect=static_answer),
|
||||
) as static_path,
|
||||
patch.object(analyzer, "_exec_single_chunk_universal", new=AsyncMock()) as universal,
|
||||
patch("artemis.agents.video_analyzer.video_analyzer._record_llm_event"),
|
||||
):
|
||||
result = await analyzer.exec_spawn_sub_agent(0.0, 1200.0, "verify toggle")
|
||||
|
||||
# Only the first chunk ever reached the agentic API; the second never ran
|
||||
# its 600s clip statically and neither went to the universal fallback.
|
||||
assert agentic_calls == [(0.0, 600.0)]
|
||||
universal.assert_not_awaited()
|
||||
segments = sorted((call.args[2], call.args[3]) for call in static_path.await_args_list)
|
||||
assert len(segments) == 20
|
||||
assert segments[0] == (0.0, 60.0)
|
||||
assert segments[-1] == (1140.0, 1200.0)
|
||||
assert all(ce - cs <= 60.0 for cs, ce in segments)
|
||||
assert "PARTIAL" not in result
|
||||
assert result.count("Summary: static") == 20
|
||||
assert analyzer.video_processing == "static"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_short_degraded_chunk_is_retried_statically_once():
|
||||
"""A 5s chunk (below min_chunk*2) is re-run as-is instead of failing."""
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.GOOGLE_API_KEY", None):
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.OPENAI_API_KEY", None):
|
||||
analyzer = VideoAnalyzer(_context())
|
||||
calls: list[tuple[float, float]] = []
|
||||
|
||||
@@ -436,7 +314,7 @@ async def test_short_degraded_chunk_is_retried_statically_once():
|
||||
@pytest.mark.asyncio
|
||||
async def test_static_retry_of_degraded_chunk_is_bounded():
|
||||
"""A second degrade on the same interval is terminal: no loop, no bisect."""
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.GOOGLE_API_KEY", None):
|
||||
with patch("artemis.agents.video_analyzer.video_analyzer.settings.OPENAI_API_KEY", None):
|
||||
analyzer = VideoAnalyzer(_context())
|
||||
calls: list[tuple[float, float]] = []
|
||||
|
||||
|
||||
Reference in new issue
Block a user