Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

### Fixed
- **v2 cleanup**: Consolidate small provider/runtime fixes for Gemini JSON prompts, Cohere templating, JSON array extraction, iterable streaming, missing `jsonref` dependency guidance, retry semantics and hook metadata, and multimodal autodetection.
- **Multimodal (Audio)**: `Audio.from_url` and `Audio.from_path` now raise `ValueError` (unsupported/empty) and `FileNotFoundError` (missing) instead of bare `assert` statements, which are silently stripped under `python -O`. Brings `Audio` validation in line with `Image`/`PDF`. ([#2361](https://github.com/567-labs/instructor/pull/2361))

### Tests / CI
- **Type checking**: Upgrade to `ty` 0.0.44, enforce warning-free checks with GitHub annotations, cover V2 tests, validate supported Python versions and platforms, and strengthen installed-package public API typing tests.
Expand Down
22 changes: 15 additions & 7 deletions instructor/v2/core/multimodal.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,11 @@ def from_url(cls, url: str) -> Audio:
return cls.from_gs_url(url)
response = requests.get(url)
content_type = response.headers.get("content-type")
assert content_type in VALID_AUDIO_MIME_TYPES, (
f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
)
if content_type not in VALID_AUDIO_MIME_TYPES:
raise ValueError(
f"Unsupported audio format: {content_type}. "
f"Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
)

data = base64.b64encode(response.content).decode("utf-8")
return cls(source=url, data=data, media_type=content_type)
Expand All @@ -336,7 +338,11 @@ def from_url(cls, url: str) -> Audio:
def from_path(cls, path: Union[str, Path]) -> Audio: # noqa: UP007
"""Create an Audio instance from a file path."""
path = Path(path)
assert path.is_file(), f"Audio file not found: {path}"
if not path.is_file():
raise FileNotFoundError(f"Audio file not found: {path}")

if path.stat().st_size == 0:
raise ValueError("Audio file is empty")

mime_type = mimetypes.guess_type(str(path))[0]

Expand All @@ -348,9 +354,11 @@ def from_path(cls, path: Union[str, Path]) -> Audio: # noqa: UP007
): # <--- this is the case for aac audio files in Windows
mime_type = "audio/aac"

assert mime_type in VALID_AUDIO_MIME_TYPES, (
f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
)
if mime_type not in VALID_AUDIO_MIME_TYPES:
raise ValueError(
f"Unsupported audio format: {mime_type}. "
f"Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
)

data = base64.b64encode(path.read_bytes()).decode("utf-8")
return cls(source=str(path), data=data, media_type=mime_type)
Expand Down
47 changes: 47 additions & 0 deletions tests/v2/test_core_multimodal_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,50 @@ def test_audio_from_path_normalizes_windows_wav_and_aac_mime_types(

assert wav_audio.media_type == "audio/wav"
assert aac_audio.media_type == "audio/aac"


def test_audio_from_url_raises_value_error_for_unsupported_content_type(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``Audio.from_url`` rejects an unsupported content-type with ``ValueError``
(not ``AssertionError``), mirroring ``Image.from_url``. ``assert`` is stripped
under ``python -O``, so validation must not rely on it."""

class _Response:
headers = {"content-type": "video/mp4"}
content = b"\x00\x01\x02"

monkeypatch.setattr("requests.get", lambda *_args, **_kwargs: _Response())

with pytest.raises(ValueError, match="Unsupported audio format"):
Audio.from_url("https://example.com/clip.mp4")


def test_audio_from_path_raises_value_error_for_unsupported_format(
tmp_path: Path,
) -> None:
"""``Audio.from_path`` rejects an unsupported format with ``ValueError``
(not ``AssertionError``), mirroring ``Image.from_path``."""
txt_path = tmp_path / "clip.txt"
txt_path.write_bytes(b"not audio") # ``.txt`` -> ``text/plain`` (not an audio type)

with pytest.raises(ValueError, match="Unsupported audio format"):
Audio.from_path(txt_path)


def test_audio_from_path_raises_value_error_for_empty_file(tmp_path: Path) -> None:
"""``Audio.from_path`` rejects an empty file, mirroring ``Image.from_path``."""
empty_path = tmp_path / "empty.wav"
empty_path.write_bytes(b"")

with pytest.raises(ValueError, match="Audio file is empty"):
Audio.from_path(empty_path)


def test_audio_from_path_raises_file_not_found_for_missing_file(tmp_path: Path) -> None:
"""``Audio.from_path`` raises ``FileNotFoundError`` (not ``AssertionError``)
for a missing file, mirroring ``Image.from_path``."""
missing_path = tmp_path / "missing.wav"

with pytest.raises(FileNotFoundError):
Audio.from_path(missing_path)
Loading