feat: add Chinese UI, Responses models, and mirrored Windows setup
CI / Python 3.12 / ubuntu-latest (push) Canceled after 0s
CI / Python 3.12 / windows-latest (push) Canceled after 0s
CI / Frontend tests and production build (push) Canceled after 0s
CI / Build and smoke-test Python packages (push) Canceled after 0s

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:
chenmingxuan committed 2026-09-22 19:16:17 +08:00
1 parent 371aa6df56
commit 2ddec53954
183 files changed
+3427 -11857

No files matched your search

+29 -121
View File
@@ -14,9 +14,8 @@
"""VideoAnalyzer facade: entry point and shared runtime state.
The execution engines live in sibling modules (``chunk_native``,
``audio_native``, ``universal_engine``, ``native_coordinator``,
``gemini_files``, ``universal_tools``) and resolve their patchable
The execution engines live in sibling modules (``chunk_runner``,
``audio_runner``, ``universal_engine``, ``universal_tools``) and resolve their patchable
collaborators through THIS module's namespace, so existing
``mock.patch("artemis.agents.video_analyzer.video_analyzer.<name>")``
targets keep working. Keep the imports below in place even when the facade
@@ -36,8 +35,6 @@ import tempfile
import time
from typing import Any
from google import genai
from google.genai import types
from langchain_core.messages import (
BaseMessage,
HumanMessage,
@@ -49,9 +46,6 @@ from artemis.agents.video_analyzer.conflict_resolution import (
ConflictResolutionService,
)
from artemis.agents.video_analyzer.blackboard import get_video_blackboard
from artemis.agents.video_analyzer.gemini_files import (
cleanup_abandoned_gemini_files,
)
from artemis.agents.video_analyzer.reliability import (
AgenticVideoDegraded,
VideoCircuitBreaker,
@@ -60,19 +54,11 @@ from artemis.agents.video_analyzer.reliability import (
from artemis.agents.video_analyzer.universal_tools import (
UNIVERSAL_MAIN_TOOLS,
UNIVERSAL_SUBMIT_ANSWER_TOOL,
build_native_tools_declaration,
build_submit_answer_declaration,
)
from artemis.config import get_temp_dir, settings
from artemis.constants import SAFETY_SETTINGS_BLOCK_NONE
from artemis.context import ArtemisContext
from artemis.controllers.controller_factory import get_controller
from artemis.data_engine.trace import CURRENT_TRACE_ID, TraceSpan, trace
from artemis.llm.google import (
is_gemini_model,
resolve_video_processing,
strip_provider_prefix,
)
from artemis.llm.reliability import retry_policy_for
from artemis.services.llm import _record_llm_event, _record_llm_retry, get_llm
from artemis.utils.logger import get_logger
@@ -159,16 +145,8 @@ class VideoAnalyzer:
getattr(agent_config, "enable_video_ledger", True),
)
self.static_chunk_size_seconds = float(
getattr(self.video_config, "chunk_size_seconds", 60.0)
getattr(self.video_config, "chunk_size_seconds", settings.ARTEMIS_VIDEO_CHUNK_SECONDS)
)
self.agentic_chunk_size_seconds = float(
getattr(self.video_config, "agentic_chunk_size_seconds", 600.0)
)
self.agentic_call_timeout_seconds = float(
getattr(self.video_config, "agentic_call_timeout_seconds", 300.0)
)
# Effective chunk ceiling; re-derived once the engine picks a
# video-processing mode (agentic sub-agents take long clips).
self.chunk_size_seconds = self.static_chunk_size_seconds
self.video_processing = "static"
self.min_chunk_seconds = float(getattr(self.video_config, "min_chunk_seconds", 4.0))
@@ -178,9 +156,15 @@ class VideoAnalyzer:
self.max_dense_action_frames = int(
getattr(self.video_config, "max_dense_action_frames", 24)
)
self.native_max_retries = int(getattr(self.video_config, "native_max_retries", 1))
self.max_retries = int(
getattr(self.video_config, "max_retries", settings.ARTEMIS_VIDEO_MAX_RETRIES)
)
self.model_call_timeout_seconds = float(
getattr(self.video_config, "model_call_timeout_seconds", 120.0)
getattr(
self.video_config,
"model_call_timeout_seconds",
settings.ARTEMIS_VIDEO_TIMEOUT_SECONDS,
)
)
breaker = getattr(ctx, "_video_circuit_breaker", None)
if breaker is None:
@@ -210,7 +194,6 @@ class VideoAnalyzer:
audio_prompt_path.read_text(encoding="utf-8") if audio_prompt_path.exists() else ""
)
self._init_tools()
self._init_engine()
def _get_universal_llm(self, *, use_fallback: bool = False):
@@ -406,65 +389,13 @@ class VideoAnalyzer:
analysis = match.group(2).strip() if match.group(2) else None
return summary, analysis
def _init_tools(self) -> None:
"""Initializes native and sub-agent tool declarations."""
self.tools_declaration = build_native_tools_declaration()
self.submit_answer_tool = build_submit_answer_declaration()
def _init_engine(self) -> None:
"""Initializes model engine and decides whether to use native Gemini or Universal path."""
ctx = self.ctx
llm_config = getattr(ctx, "llm_config", None)
llm_config = getattr(self.ctx, "llm_config", None)
utils_cfg = getattr(llm_config, "utils", None) if llm_config else None
llm_cfg = getattr(utils_cfg, "video_analyzer", None) if utils_cfg else None
self.model_name = strip_provider_prefix(
llm_cfg.model if (llm_cfg and hasattr(llm_cfg, "model")) else "gemini-3.8-flash"
)
has_google_key = bool(
settings.GOOGLE_API_KEY and settings.GOOGLE_API_KEY.get_secret_value()
)
# If client is already set on ctx or explicit Gemini configuration
self.client = getattr(ctx, "_genai_client", None)
if self.client is not None:
self.use_native_gemini = True
elif has_google_key and is_gemini_model(self.model_name):
try:
self.client = genai.Client(api_key=settings.GOOGLE_API_KEY.get_secret_value())
ctx._genai_client = self.client
self.use_native_gemini = True
asyncio.create_task(cleanup_abandoned_gemini_files(self.client))
except Exception as e:
logger.warning(
f"Failed to initialize native Gemini client: {e}. Using universal engine."
)
self.use_native_gemini = False
else:
self.use_native_gemini = False
self._resolve_video_processing()
def _resolve_video_processing(self) -> None:
"""Picks static vs agentic video understanding for the sub-agents.
Agentic processing (Gemini Interactions API) needs the native client
and a supporting model; the configured knob (``auto`` / ``agentic`` /
``static``) decides the rest. The chunk ceiling follows the mode
because agentic sub-agents are built to take long clips.
"""
configured = getattr(self.video_config, "processing", "auto")
mode = "static"
if self.use_native_gemini:
mode = resolve_video_processing(configured, self.model_name)
self.video_processing = mode
self.chunk_size_seconds = (
self.agentic_chunk_size_seconds if mode == "agentic" else self.static_chunk_size_seconds
)
if mode == "agentic":
logger.info(
f"Video sub-agents use agentic video understanding on {self.model_name}"
f" (chunk ceiling {self.chunk_size_seconds:.0f}s)"
)
self.model_name = getattr(llm_cfg, "model", None) or settings.OPENAI_MODEL
self.video_processing = "static"
self.chunk_size_seconds = self.static_chunk_size_seconds
@property
def blackboard_ledger(self) -> str:
@@ -476,13 +407,6 @@ class VideoAnalyzer:
lines.append(f"{e['start']}s - {e['end']}s: {e['summary']}")
return " | ".join(lines)
async def upload_and_poll_file(self, compressed_path: Path) -> any:
from artemis.agents.video_analyzer import gemini_files
return await gemini_files.upload_and_poll_file(
self.client, compressed_path, self.cloud_files_to_cleanup
)
def get_overlapping_warnings(self, start_time: float, end_time: float | None) -> list[dict]:
warnings = []
if not isinstance(start_time, (int, float)) or (
@@ -739,17 +663,17 @@ class VideoAnalyzer:
async def _exec_single_chunk(
self, start_time: float, end_time: float | None, specific_query: str
) -> str:
from artemis.agents.video_analyzer import chunk_native
from artemis.agents.video_analyzer import chunk_runner
return await chunk_native.exec_single_chunk(self, start_time, end_time, specific_query)
return await chunk_runner.exec_single_chunk(self, start_time, end_time, specific_query)
@trace(type="tool", name="analyze_audio_only")
async def exec_analyze_audio_only(
self, start_time: float, end_time: float | None, specific_query: str
) -> str:
from artemis.agents.video_analyzer import audio_native
from artemis.agents.video_analyzer import audio_runner
return await audio_native.exec_analyze_audio_only(
return await audio_runner.exec_analyze_audio_only(
self, start_time, end_time, specific_query
)
@@ -837,15 +761,12 @@ class VideoAnalyzer:
utils_cfg = getattr(llm_config, "utils", None) if llm_config else None
llm_cfg = getattr(utils_cfg, "video_analyzer", None) if utils_cfg else None
temperature = 0.2
thinking_level = None
if llm_cfg:
self.model_name = strip_provider_prefix(llm_cfg.model)
self.model_name = llm_cfg.model
if getattr(llm_cfg, "temperature", None) is not None:
temperature = llm_cfg.temperature
if getattr(llm_cfg, "thinking_level", None) is not None:
thinking_level = llm_cfg.thinking_level
else:
self.model_name = "gemini-3.8-flash"
self.model_name = settings.OPENAI_MODEL
# Track files for cleanup
self.local_files_to_cleanup = set()
@@ -868,24 +789,11 @@ class VideoAnalyzer:
" proceeding.\n"
)
if not self.use_native_gemini:
try:
return await self._run_universal(
time_description=time_description,
purpose=purpose,
system_prompt=system_prompt,
)
finally:
await self._cleanup_universal_resources()
from artemis.agents.video_analyzer import native_coordinator
return await native_coordinator.run_native(
self,
ctx,
time_description,
purpose,
system_prompt,
temperature,
thinking_level,
)
try:
return await self._run_universal(
time_description=time_description,
purpose=purpose,
system_prompt=system_prompt,
)
finally:
await self._cleanup_universal_resources()