Skip to content
Open
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
54 changes: 31 additions & 23 deletions obs-studio/agent-harness/cli_anything/obs_studio/core/sources.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
"""OBS Studio CLI - Source management."""

import copy
import math
from typing import Dict, Any, List, Optional
from cli_anything.obs_studio.utils.obs_utils import generate_id, unique_name, get_item, validate_range
from cli_anything.obs_studio.utils.obs_utils import (
generate_id,
unique_name,
get_item,
validate_range,
validate_position,
validate_size,
validate_crop,
)


SOURCE_TYPES = {
Expand Down Expand Up @@ -122,13 +131,9 @@ def add_source(
src["visible"] = visible

if position:
src["position"] = {"x": float(position.get("x", 0)), "y": float(position.get("y", 0))}
src["position"] = validate_position(position)
if size:
w = int(size.get("width", 1920))
h = int(size.get("height", 1080))
if w < 1 or h < 1:
raise ValueError(f"Size must be positive: {w}x{h}")
src["size"] = {"width": w, "height": h}
src["size"] = validate_size(size)
if settings:
src["settings"].update(settings)

Expand Down Expand Up @@ -201,25 +206,28 @@ def transform_source(
source = get_item(sources, source_index, "source")

if position:
source["position"] = {
"x": float(position.get("x", source["position"]["x"])),
"y": float(position.get("y", source["position"]["y"])),
}
source["position"] = validate_position(
{
"x": position.get("x", source["position"]["x"]),
"y": position.get("y", source["position"]["y"]),
}
)
if size:
w = int(size.get("width", source["size"]["width"]))
h = int(size.get("height", source["size"]["height"]))
if w < 1 or h < 1:
raise ValueError(f"Size must be positive: {w}x{h}")
source["size"] = {"width": w, "height": h}
source["size"] = validate_size(
{
"width": size.get("width", source["size"]["width"]),
"height": size.get("height", source["size"]["height"]),
}
)
if crop:
for key in ("top", "bottom", "left", "right"):
if key in crop:
val = int(crop[key])
if val < 0:
raise ValueError(f"Crop {key} must be non-negative, got {val}")
source["crop"][key] = val
merged_crop = dict(source.get("crop") or {})
merged_crop.update(crop)
source["crop"] = validate_crop(merged_crop)
if rotation is not None:
source["rotation"] = float(rotation)
rot = float(rotation)
if not math.isfinite(rot):
raise ValueError(f"Rotation must be a finite number, got {rot}")
source["rotation"] = rot

return source

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Reject non-finite values in OBS geometry validators."""

from __future__ import annotations

import pytest

from cli_anything.obs_studio.core.sources import add_source, transform_source
from cli_anything.obs_studio.utils.obs_utils import (
validate_crop,
validate_position,
validate_size,
)


def _project() -> dict:
return {
"scenes": [
{
"name": "Scene",
"sources": [
{
"id": 0,
"name": "Cam",
"type": "browser",
"visible": True,
"locked": False,
"opacity": 1.0,
"rotation": 0.0,
"position": {"x": 0.0, "y": 0.0},
"size": {"width": 1920, "height": 1080},
"crop": {"top": 0, "bottom": 0, "left": 0, "right": 0},
"settings": {},
}
],
}
]
}


def test_validate_position_rejects_nan():
with pytest.raises(ValueError, match="finite"):
validate_position({"x": float("nan"), "y": 1.0})


def test_validate_size_rejects_nan():
with pytest.raises(ValueError, match="finite"):
validate_size({"width": float("nan"), "height": 1080})


def test_validate_crop_rejects_inf():
with pytest.raises(ValueError, match="finite"):
validate_crop({"left": float("inf"), "right": 0, "top": 0, "bottom": 0})


def test_validate_position_accepts_finite():
assert validate_position({"x": 1.5, "y": -2.0}) == {"x": 1.5, "y": -2.0}


def test_add_source_rejects_nan_position():
with pytest.raises(ValueError, match="finite"):
add_source(
_project(),
"browser",
position={"x": float("nan"), "y": 0},
)


def test_transform_source_rejects_nan_position():
with pytest.raises(ValueError, match="finite"):
transform_source(_project(), 0, position={"x": float("nan")})
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
"""OBS Studio CLI - JSON helpers and utilities."""

import json
import math
import os
import copy
from typing import Dict, Any, List, Optional



def _finite_number(value: Any, name: str):
"""Reject non-finite values without float-rounding large ints."""
if isinstance(value, bool):
raise ValueError(f"{name} must be a finite number, got {value!r}")
if isinstance(value, int):
return value
try:
num = float(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"{name} must be a finite number, got {value!r}") from exc
if not math.isfinite(num):
raise ValueError(f"{name} must be a finite number, got {value!r}")
return num


def generate_id(items: List[Dict[str, Any]]) -> int:
"""Generate the next unique ID for a list of items."""
if not items:
Expand Down Expand Up @@ -34,16 +51,17 @@ def validate_range(value: float, min_val: float, max_val: float, name: str) -> f

def validate_position(pos: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and normalize a position dict."""
return {
"x": float(pos.get("x", 0)),
"y": float(pos.get("y", 0)),
}
x = _finite_number(pos.get("x", 0), "Position x")
y = _finite_number(pos.get("y", 0), "Position y")
return {"x": x, "y": y}


def validate_size(size: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and normalize a size dict."""
w = int(size.get("width", 1920))
h = int(size.get("height", 1080))
width_raw = _finite_number(size.get("width", 1920), "Size width")
height_raw = _finite_number(size.get("height", 1080), "Size height")
w = int(width_raw)
h = int(height_raw)
if w < 1:
raise ValueError(f"Width must be positive, got {w}")
if h < 1:
Expand All @@ -55,7 +73,8 @@ def validate_crop(crop: Dict[str, Any]) -> Dict[str, Any]:
"""Validate and normalize a crop dict."""
result = {}
for key in ("top", "bottom", "left", "right"):
val = int(crop.get(key, 0))
raw = _finite_number(crop.get(key, 0), f"Crop {key}")
val = int(raw)
if val < 0:
raise ValueError(f"Crop {key} must be non-negative, got {val}")
result[key] = val
Expand Down
Loading