Skip to content

Commit e22d5ff

Browse files
committed
fix(gemini): use official FileData part for YouTube and remove deprecated temperature
1 parent 3e8c9c7 commit e22d5ff

2 files changed

Lines changed: 163 additions & 4 deletions

File tree

src/gemini_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@ def analyze_video(
107107
types.Content(
108108
role="user",
109109
parts=[
110-
types.Part.from_uri(
111-
file_uri=video_url,
112-
mime_type="video/youtube",
110+
types.Part(
111+
file_data=types.FileData(
112+
file_uri=video_url
113+
)
113114
),
114115
types.Part.from_text(text=extraction_prompt),
115116
],
@@ -118,7 +119,6 @@ def analyze_video(
118119
config=types.GenerateContentConfig(
119120
response_mime_type="application/json",
120121
response_schema=VideoAnalysis,
121-
temperature=0.2,
122122
),
123123
)
124124

tests/test_gemini_client.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""Unit tests for Gemini client (mocked, no API quota spent)."""
2+
import json
3+
import pytest
4+
from unittest.mock import MagicMock, patch
5+
6+
from src.gemini_client import (
7+
analyze_video,
8+
load_extraction_prompt,
9+
APIStats,
10+
_is_retryable_error,
11+
_is_permanent_error,
12+
)
13+
from src.schemas import VideoAnalysis, ReviewPriority
14+
from google.genai import types
15+
16+
17+
def _sample_analysis() -> VideoAnalysis:
18+
return VideoAnalysis(
19+
title="Testing Video",
20+
summary="Summary of test video.",
21+
review_priority=ReviewPriority.high,
22+
)
23+
24+
25+
class TestGeminiClientLogic:
26+
def test_load_extraction_prompt(self):
27+
prompt = load_extraction_prompt()
28+
assert isinstance(prompt, str)
29+
assert len(prompt) > 50
30+
assert "candidate_principles" in prompt or "Candidate Principles" in prompt or "Visual Observations" in prompt
31+
32+
def test_error_classification(self):
33+
assert _is_retryable_error(Exception("429 Resource Exhausted")) is True
34+
assert _is_retryable_error(Exception("Deadline exceeded")) is True
35+
assert _is_retryable_error(Exception("Connection timeout")) is True
36+
assert _is_retryable_error(Exception("Random syntax error")) is False
37+
38+
assert _is_permanent_error(Exception("403 Forbidden")) is True
39+
assert _is_permanent_error(Exception("Private video")) is True
40+
assert _is_permanent_error(Exception("Safety block triggered")) is True
41+
assert _is_permanent_error(Exception("503 Service Unavailable")) is False
42+
43+
44+
class TestMockedAnalyzeVideo:
45+
@patch("src.gemini_client.genai.Client")
46+
def test_analyze_video_success_with_parsed(self, mock_client_cls):
47+
mock_client = MagicMock()
48+
mock_client_cls.return_value = mock_client
49+
50+
mock_response = MagicMock()
51+
mock_response.parsed = _sample_analysis()
52+
mock_response.usage_metadata = MagicMock()
53+
mock_response.usage_metadata.prompt_token_count = 1200
54+
mock_response.usage_metadata.candidates_token_count = 450
55+
mock_response.usage_metadata.total_token_count = 1650
56+
57+
mock_client.models.generate_content.return_value = mock_response
58+
59+
stats = APIStats()
60+
result = analyze_video(
61+
api_key="test_fake_api_key_12345",
62+
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
63+
model="gemini-3.6-flash",
64+
stats=stats,
65+
)
66+
67+
assert result.success is True
68+
assert result.analysis is not None
69+
assert result.analysis.title == "Testing Video"
70+
assert result.analysis.schema_version == "0.1"
71+
assert result.analysis.prompt_version == "video_extraction_v1"
72+
assert stats.requests_successful == 1
73+
assert stats.total_input_tokens == 1200
74+
assert stats.total_output_tokens == 450
75+
76+
# Verify call arguments to generate_content
77+
mock_client.models.generate_content.assert_called_once()
78+
call_kwargs = mock_client.models.generate_content.call_args.kwargs
79+
assert call_kwargs["model"] == "gemini-3.6-flash"
80+
81+
# Verify contents: should have types.Part with FileData
82+
contents = call_kwargs["contents"]
83+
content_item = contents[0]
84+
parts = content_item.parts
85+
video_part = parts[0]
86+
assert isinstance(video_part, types.Part)
87+
assert video_part.file_data is not None
88+
assert video_part.file_data.file_uri == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
89+
90+
# Verify config: no temperature, response_schema present
91+
config = call_kwargs["config"]
92+
assert config.response_mime_type == "application/json"
93+
assert config.response_schema == VideoAnalysis
94+
assert getattr(config, "temperature", None) is None
95+
96+
@patch("src.gemini_client.genai.Client")
97+
def test_analyze_video_fallback_to_json_text(self, mock_client_cls):
98+
mock_client = MagicMock()
99+
mock_client_cls.return_value = mock_client
100+
101+
mock_response = MagicMock()
102+
mock_response.parsed = None
103+
mock_response.text = json.dumps({
104+
"title": "Parsed from text",
105+
"summary": "Fallback parsing works.",
106+
"review_priority": "medium",
107+
})
108+
mock_response.usage_metadata = None
109+
110+
mock_client.models.generate_content.return_value = mock_response
111+
112+
result = analyze_video(
113+
api_key="test_fake_api_key_12345",
114+
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
115+
model="gemini-3.6-flash",
116+
)
117+
118+
assert result.success is True
119+
assert result.analysis is not None
120+
assert result.analysis.title == "Parsed from text"
121+
122+
@patch("src.gemini_client.genai.Client")
123+
def test_analyze_video_permanent_failure_no_retry(self, mock_client_cls):
124+
mock_client = MagicMock()
125+
mock_client_cls.return_value = mock_client
126+
mock_client.models.generate_content.side_effect = Exception("403 Forbidden: private video")
127+
128+
stats = APIStats()
129+
result = analyze_video(
130+
api_key="secret_key_12345678",
131+
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
132+
model="gemini-3.6-flash",
133+
stats=stats,
134+
)
135+
136+
assert result.success is False
137+
assert result.retryable is False
138+
assert result.attempts == 1
139+
assert stats.requests_failed == 1
140+
assert "secret_key_12345678" not in result.error
141+
assert "[REDACTED]" in result.error or "secret_key" not in result.error
142+
143+
@patch("src.gemini_client.time.sleep")
144+
@patch("src.gemini_client.genai.Client")
145+
def test_analyze_video_transient_failure_retried(self, mock_client_cls, mock_sleep):
146+
mock_client = MagicMock()
147+
mock_client_cls.return_value = mock_client
148+
mock_client.models.generate_content.side_effect = Exception("429 Resource Exhausted: rate limit")
149+
150+
result = analyze_video(
151+
api_key="test_fake_key_12345678",
152+
video_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
153+
model="gemini-3.6-flash",
154+
)
155+
156+
assert result.success is False
157+
assert result.retryable is True
158+
assert result.attempts == 3
159+
assert mock_sleep.call_count == 2

0 commit comments

Comments
 (0)