Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Users interact with `opai` directly from notebook cells. The package root is the
- Resolves the active global context and forwards calls into the application layer.

## Public API Design
The public API should be short and stable. Prefer this style:
The public API should be short and stable. Prefer this style (examples):

```python
ctx = opai.init(name: str) -> Context
Expand Down Expand Up @@ -144,6 +144,9 @@ Outcome:
- Make artifact creation deterministic where possible.
- Make errors notebook-friendly and actionable.
- Fail fast on required dependencies. If a feature depends on a package that is required by this repo, import it normally and let missing dependencies fail at import time rather than adding deferred runtime guards. Reserve lazy imports or fallback guards for truly optional integrations only.
- Do not import types from the `typing` module. Python 3.10+ native annotations are the repo standard.
- Prefer built-in generics such as `list[str]`, `dict[str, object]`, `tuple[int, ...]`, and unions like `Path | None`.
- When a protocol-style annotation is needed, import it from `collections.abc` instead of `typing`.

## Open Design Points To Resolve In Implementation
These decisions must be made consistently across the package:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ requires-python = ">=3.10"
dependencies = [
"jupyterlab>=4.5.6",
"opencv-contrib-python>=4.13.0.92",
"rich>=14.1.0",
]

[project.optional-dependencies]
Expand Down
15 changes: 14 additions & 1 deletion src/opai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
from opai.presentation.facade import calibrate, get_context, init, main
from opai.presentation.facade import (
add_demos,
add_mapping,
browse_session,
calibrate,
get_context,
init,
list_sessions,
main,
)

__all__ = [
"add_demos",
"add_mapping",
"browse_session",
"calibrate",
"get_context",
"init",
"list_sessions",
"main",
]
8 changes: 7 additions & 1 deletion src/opai/application/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
from opai.application.calibration import calibrate
from opai.application.session import (
add_demos,
add_mapping,
browse_session,
list_sessions,
)

__all__ = ["calibrate"]
__all__ = ["add_demos", "add_mapping", "browse_session", "calibrate", "list_sessions"]
14 changes: 8 additions & 6 deletions src/opai/application/calibration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Any, List, Sequence, Tuple
from __future__ import annotations

from collections.abc import Sequence

import cv2
import numpy as np
Expand Down Expand Up @@ -37,8 +39,8 @@ def calibrate(
image_height, image_width = _get_frame_size(frames)
image_size = (image_width, image_height)

all_charuco_corners = [] # type: List[np.ndarray]
all_charuco_ids = [] # type: List[np.ndarray]
all_charuco_corners: list[np.ndarray] = []
all_charuco_ids: list[np.ndarray] = []

for frame in frames:
grayscale = _to_grayscale(frame)
Expand Down Expand Up @@ -124,14 +126,14 @@ def _validate_inputs(
)


def _resolve_dictionary(name: str) -> Any:
def _resolve_dictionary(name: str) -> cv2.aruco.Dictionary:
dictionary_id = getattr(cv2.aruco, name, None)
if dictionary_id is None:
raise OPAIValidationError(f"Unsupported ArUco dictionary: {name}")
return cv2.aruco.getPredefinedDictionary(dictionary_id)


def _get_frame_size(frames: Sequence[np.ndarray]) -> Tuple[int, int]:
def _get_frame_size(frames: Sequence[np.ndarray]) -> tuple[int, int]:
height, width = frames[0].shape[:2]
return int(height), int(width)

Expand All @@ -143,7 +145,7 @@ def _to_grayscale(frame: np.ndarray) -> np.ndarray:


def _compute_mse_reprojection_error(
board: Any,
board: cv2.aruco.CharucoBoard,
charuco_corners: Sequence[np.ndarray],
charuco_ids: Sequence[np.ndarray],
rvecs: Sequence[np.ndarray],
Expand Down
106 changes: 106 additions & 0 deletions src/opai/application/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from __future__ import annotations

from collections.abc import Iterable
from pathlib import Path

from opai.core.exceptions import OPAIValidationError
from opai.domain.context import Context
from opai.domain.session import DemoAsset, SessionManifest
from opai.infrastructure.context_store import (
get_session_directory,
list_session_names,
load_manifest_for_context,
persist_manifest_for_context,
)
from opai.infrastructure.persistence import (
build_file_tree,
copy_demo_assets,
copy_mapping_asset,
list_relative_paths,
)


def add_demos(ctx: Context, video_paths: Iterable[str | Path]) -> tuple[DemoAsset, ...]:
source_paths = _normalize_video_paths(video_paths, label="demo")
manifest = load_manifest_for_context(ctx)
copied_assets = copy_demo_assets(
ctx.session_directory,
current_demos=manifest.demos,
source_paths=source_paths,
)
updated_manifest = SessionManifest(
session_name=manifest.session_name,
demos=manifest.demos + copied_assets,
mapping=manifest.mapping,
)
persist_manifest_for_context(ctx, updated_manifest)
return copied_assets


def add_mapping(ctx: Context, video_path: str | Path) -> MappingAsset:
source_path = _normalize_single_video_path(video_path, label="mapping")
manifest = load_manifest_for_context(ctx)
mapping_asset = copy_mapping_asset(ctx.session_directory, source_path)
updated_manifest = SessionManifest(
session_name=manifest.session_name,
demos=manifest.demos,
mapping=mapping_asset,
)
persist_manifest_for_context(ctx, updated_manifest)
return mapping_asset


def list_sessions() -> list[str]:
return list_session_names()


def browse_session(name: str) -> tuple[list[str], dict[str, dict]]:
session_name = _normalize_session_name(name)
session_directory = get_session_directory(session_name)
if not session_directory.exists():
raise OPAIValidationError(
f"Session '{session_name}' does not exist.",
details={"session_name": session_name},
)
return list_relative_paths(session_directory), build_file_tree(session_directory)


def _normalize_video_paths(
video_paths: Iterable[str | Path],
*,
label: str,
) -> tuple[Path, ...]:
normalized = tuple(
_coerce_existing_file_path(path, label=label) for path in video_paths
)
if not normalized:
raise OPAIValidationError(
f"At least one {label} video path is required.",
details={"label": label},
)
return normalized


def _normalize_single_video_path(video_path: str | Path, *, label: str) -> Path:
return _coerce_existing_file_path(video_path, label=label)


def _coerce_existing_file_path(value: str | Path, *, label: str) -> Path:
path = Path(value).expanduser()
if not path.exists():
raise OPAIValidationError(
f"{label.capitalize()} video does not exist: {path}",
details={"path": str(path)},
)
if not path.is_file():
raise OPAIValidationError(
f"{label.capitalize()} path must point to a file: {path}",
details={"path": str(path)},
)
return path


def _normalize_session_name(name: str) -> str:
if not isinstance(name, str) or not name.strip():
raise OPAIValidationError("Session name must be a non-empty string.")
return name.strip()
10 changes: 6 additions & 4 deletions src/opai/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, Optional
from __future__ import annotations


class OPAIError(Exception):
Expand All @@ -9,15 +9,17 @@ class OPAIError(Exception):
def __init__(
self,
message: str,
error_code: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
error_code: str | None = None,
details: dict[str, str | int | float | bool | None] | None = None,
) -> None:
self.message = message
self.error_code = error_code or self.default_error_code
self.details = details or {}
super().__init__(message)

def to_dict(self) -> Dict[str, Any]:
def to_dict(
self,
) -> dict[str, str | dict[str, str | int | float | bool | None]]:
return {
"error_code": self.error_code,
"message": self.message,
Expand Down
9 changes: 8 additions & 1 deletion src/opai/domain/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from opai.domain.calibration import CalibrationResult
from opai.domain.context import Context
from opai.domain.session import DemoAsset, MappingAsset, SessionManifest

__all__ = ["CalibrationResult", "Context"]
__all__ = [
"CalibrationResult",
"Context",
"DemoAsset",
"MappingAsset",
"SessionManifest",
]
9 changes: 6 additions & 3 deletions src/opai/domain/calibration.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import numpy as np


@dataclass
Expand All @@ -22,5 +25,5 @@ class CalibrationResult:
image_width: int
intrinsic_type: str
intrinsics: CalibrationIntrinsics
camera_matrix: Any
dist_coeffs: Any
camera_matrix: np.ndarray
dist_coeffs: np.ndarray
3 changes: 3 additions & 0 deletions src/opai/domain/context.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

Expand All @@ -6,3 +8,4 @@
class Context:
name: str
session_directory: Path
manifest_path: Path | None = None
25 changes: 25 additions & 0 deletions src/opai/domain/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class DemoAsset:
demo_id: str
source_path: str
stored_path: str
original_filename: str


@dataclass(frozen=True)
class MappingAsset:
source_path: str
stored_path: str
original_filename: str


@dataclass(frozen=True)
class SessionManifest:
session_name: str
demos: tuple[DemoAsset, ...]
mapping: MappingAsset | None = None
23 changes: 20 additions & 3 deletions src/opai/infrastructure/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
from opai.infrastructure.context_store import get_active_context, init_context
from opai.infrastructure.persistence import write_calibration_result
from opai.infrastructure.context_store import (
get_active_context,
get_session_directory,
init_context,
list_session_names,
)
from opai.infrastructure.persistence import (
load_session_manifest,
write_calibration_result,
write_session_manifest,
)

__all__ = ["get_active_context", "init_context", "write_calibration_result"]
__all__ = [
"get_active_context",
"get_session_directory",
"init_context",
"list_session_names",
"load_session_manifest",
"write_calibration_result",
"write_session_manifest",
]
Loading
Loading