merleau 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- merleau/__init__.py +1 -1
- merleau/cli.py +128 -30
- {merleau-0.2.0.dist-info → merleau-0.3.0.dist-info}/METADATA +3 -1
- merleau-0.3.0.dist-info/RECORD +6 -0
- merleau-0.2.0.dist-info/RECORD +0 -6
- {merleau-0.2.0.dist-info → merleau-0.3.0.dist-info}/WHEEL +0 -0
- {merleau-0.2.0.dist-info → merleau-0.3.0.dist-info}/entry_points.txt +0 -0
merleau/__init__.py
CHANGED
merleau/cli.py
CHANGED
|
@@ -4,21 +4,49 @@ import argparse
|
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Callable, Optional
|
|
7
9
|
|
|
8
10
|
from dotenv import load_dotenv
|
|
9
11
|
from google import genai
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
@dataclass
|
|
15
|
+
class AnalysisResult:
|
|
16
|
+
"""Result from video analysis."""
|
|
17
|
+
text: str
|
|
18
|
+
prompt_tokens: int
|
|
19
|
+
response_tokens: int
|
|
20
|
+
total_tokens: int
|
|
21
|
+
input_cost: float
|
|
22
|
+
output_cost: float
|
|
23
|
+
total_cost: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def wait_for_processing(client, file, on_progress: Optional[Callable] = None):
|
|
13
27
|
"""Wait for file to finish processing."""
|
|
14
28
|
while file.state.name == "PROCESSING":
|
|
15
|
-
|
|
29
|
+
if on_progress:
|
|
30
|
+
on_progress()
|
|
31
|
+
else:
|
|
32
|
+
print(".", end="", flush=True)
|
|
16
33
|
time.sleep(2)
|
|
17
34
|
file = client.files.get(name=file.name)
|
|
18
|
-
|
|
35
|
+
if not on_progress:
|
|
36
|
+
print()
|
|
19
37
|
return file
|
|
20
38
|
|
|
21
39
|
|
|
40
|
+
def calculate_cost(usage):
|
|
41
|
+
"""Calculate cost from usage metadata."""
|
|
42
|
+
# Gemini 2.5 Flash pricing (as of 2025):
|
|
43
|
+
# Input: $0.15 per 1M tokens (text/image), $0.075 per 1M tokens for video
|
|
44
|
+
# Output: $0.60 per 1M tokens, $3.50 for thinking tokens
|
|
45
|
+
input_cost = (usage.prompt_token_count / 1_000_000) * 0.15
|
|
46
|
+
output_cost = (usage.candidates_token_count / 1_000_000) * 0.60
|
|
47
|
+
return input_cost, output_cost, input_cost + output_cost
|
|
48
|
+
|
|
49
|
+
|
|
22
50
|
def print_usage(usage):
|
|
23
51
|
"""Print token usage and cost estimation."""
|
|
24
52
|
print("\n--- Usage Information ---")
|
|
@@ -26,61 +54,131 @@ def print_usage(usage):
|
|
|
26
54
|
print(f"Response tokens: {usage.candidates_token_count}")
|
|
27
55
|
print(f"Total tokens: {usage.total_token_count}")
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
# Input: $0.15 per 1M tokens (text/image), $0.075 per 1M tokens for video
|
|
31
|
-
# Output: $0.60 per 1M tokens, $3.50 for thinking tokens
|
|
32
|
-
input_cost = (usage.prompt_token_count / 1_000_000) * 0.15
|
|
33
|
-
output_cost = (usage.candidates_token_count / 1_000_000) * 0.60
|
|
34
|
-
total_cost = input_cost + output_cost
|
|
57
|
+
input_cost, output_cost, total_cost = calculate_cost(usage)
|
|
35
58
|
print(f"\nEstimated cost:")
|
|
36
59
|
print(f" Input: ${input_cost:.6f}")
|
|
37
60
|
print(f" Output: ${output_cost:.6f}")
|
|
38
61
|
print(f" Total: ${total_cost:.6f}")
|
|
39
62
|
|
|
40
63
|
|
|
41
|
-
def
|
|
42
|
-
|
|
64
|
+
def analyze_video(
|
|
65
|
+
video_path: str,
|
|
66
|
+
prompt: str = "Explain what happens in this video",
|
|
67
|
+
model: str = "gemini-2.5-flash",
|
|
68
|
+
api_key: Optional[str] = None,
|
|
69
|
+
on_upload: Optional[Callable[[str], None]] = None,
|
|
70
|
+
on_processing: Optional[Callable] = None,
|
|
71
|
+
on_analyzing: Optional[Callable] = None,
|
|
72
|
+
) -> AnalysisResult:
|
|
73
|
+
"""
|
|
74
|
+
Analyze a video file using Gemini.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
video_path: Path to the video file
|
|
78
|
+
prompt: Analysis prompt
|
|
79
|
+
model: Gemini model to use
|
|
80
|
+
api_key: Optional API key (falls back to env var)
|
|
81
|
+
on_upload: Callback when upload completes (receives file URI)
|
|
82
|
+
on_processing: Callback during processing (called repeatedly)
|
|
83
|
+
on_analyzing: Callback when analysis starts
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
AnalysisResult with text, tokens, and cost
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If API key not found or file doesn't exist
|
|
90
|
+
RuntimeError: If file processing fails
|
|
91
|
+
"""
|
|
43
92
|
load_dotenv()
|
|
44
93
|
|
|
45
|
-
api_key = os.getenv("GEMINI_API_KEY")
|
|
94
|
+
api_key = api_key or os.getenv("GEMINI_API_KEY")
|
|
46
95
|
if not api_key:
|
|
47
|
-
|
|
48
|
-
sys.exit(1)
|
|
96
|
+
raise ValueError("GEMINI_API_KEY not found in environment or .env file")
|
|
49
97
|
|
|
50
98
|
if not os.path.exists(video_path):
|
|
51
|
-
|
|
52
|
-
sys.exit(1)
|
|
99
|
+
raise ValueError(f"Video file not found: {video_path}")
|
|
53
100
|
|
|
54
101
|
client = genai.Client(api_key=api_key)
|
|
55
102
|
|
|
56
103
|
# Upload video
|
|
57
|
-
print(f"Uploading video: {video_path}")
|
|
58
104
|
myfile = client.files.upload(file=video_path)
|
|
59
|
-
|
|
105
|
+
if on_upload:
|
|
106
|
+
on_upload(myfile.uri)
|
|
60
107
|
|
|
61
108
|
# Wait for processing
|
|
62
|
-
|
|
63
|
-
myfile = wait_for_processing(client, myfile)
|
|
109
|
+
myfile = wait_for_processing(client, myfile, on_progress=on_processing)
|
|
64
110
|
|
|
65
111
|
if myfile.state.name == "FAILED":
|
|
66
|
-
|
|
67
|
-
sys.exit(1)
|
|
68
|
-
|
|
69
|
-
print(f"File state: {myfile.state.name}")
|
|
112
|
+
raise RuntimeError("File processing failed")
|
|
70
113
|
|
|
71
114
|
# Generate analysis
|
|
72
|
-
|
|
115
|
+
if on_analyzing:
|
|
116
|
+
on_analyzing()
|
|
117
|
+
|
|
73
118
|
response = client.models.generate_content(
|
|
74
119
|
model=model,
|
|
75
120
|
contents=[myfile, prompt]
|
|
76
121
|
)
|
|
77
122
|
|
|
78
|
-
|
|
79
|
-
|
|
123
|
+
# Extract usage info
|
|
124
|
+
usage = response.usage_metadata
|
|
125
|
+
input_cost, output_cost, total_cost = calculate_cost(usage)
|
|
126
|
+
|
|
127
|
+
return AnalysisResult(
|
|
128
|
+
text=response.text,
|
|
129
|
+
prompt_tokens=usage.prompt_token_count,
|
|
130
|
+
response_tokens=usage.candidates_token_count,
|
|
131
|
+
total_tokens=usage.total_token_count,
|
|
132
|
+
input_cost=input_cost,
|
|
133
|
+
output_cost=output_cost,
|
|
134
|
+
total_cost=total_cost,
|
|
135
|
+
)
|
|
136
|
+
|
|
80
137
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
def analyze(video_path, prompt, model, show_cost):
|
|
139
|
+
"""Analyze a video file using Gemini (CLI wrapper)."""
|
|
140
|
+
try:
|
|
141
|
+
print(f"Uploading video: {video_path}")
|
|
142
|
+
|
|
143
|
+
def on_upload(uri):
|
|
144
|
+
print(f"Upload complete. File URI: {uri}")
|
|
145
|
+
print("Waiting for file to be processed...", end="")
|
|
146
|
+
|
|
147
|
+
def on_processing():
|
|
148
|
+
print(".", end="", flush=True)
|
|
149
|
+
|
|
150
|
+
def on_analyzing():
|
|
151
|
+
print()
|
|
152
|
+
print(f"\nAnalyzing video with {model}...")
|
|
153
|
+
|
|
154
|
+
result = analyze_video(
|
|
155
|
+
video_path=video_path,
|
|
156
|
+
prompt=prompt,
|
|
157
|
+
model=model,
|
|
158
|
+
on_upload=on_upload,
|
|
159
|
+
on_processing=on_processing,
|
|
160
|
+
on_analyzing=on_analyzing,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
print("\n--- Video Analysis ---")
|
|
164
|
+
print(result.text)
|
|
165
|
+
|
|
166
|
+
if show_cost:
|
|
167
|
+
print("\n--- Usage Information ---")
|
|
168
|
+
print(f"Prompt tokens: {result.prompt_tokens}")
|
|
169
|
+
print(f"Response tokens: {result.response_tokens}")
|
|
170
|
+
print(f"Total tokens: {result.total_tokens}")
|
|
171
|
+
print(f"\nEstimated cost:")
|
|
172
|
+
print(f" Input: ${result.input_cost:.6f}")
|
|
173
|
+
print(f" Output: ${result.output_cost:.6f}")
|
|
174
|
+
print(f" Total: ${result.total_cost:.6f}")
|
|
175
|
+
|
|
176
|
+
except ValueError as e:
|
|
177
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
except RuntimeError as e:
|
|
180
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
181
|
+
sys.exit(1)
|
|
84
182
|
|
|
85
183
|
|
|
86
184
|
def main():
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: merleau
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Video analysis using Google's Gemini 2.5 Flash API
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: google-genai
|
|
7
7
|
Requires-Dist: python-dotenv
|
|
8
|
+
Provides-Extra: web
|
|
9
|
+
Requires-Dist: streamlit>=1.30.0; extra == 'web'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
merleau/__init__.py,sha256=zV-fMC9hX4Uq5Kb4Jegk84CouKUslY58WkdBNCMMVck,81
|
|
2
|
+
merleau/cli.py,sha256=1NofguSaSdB1RQpSb9Po6BudhQp6Q-3vYiGNky6vbmo,6396
|
|
3
|
+
merleau-0.3.0.dist-info/METADATA,sha256=XKLO1o4-g-sK2xFm38CB33NRzpQk5c7mc5sSfi6Ry8g,261
|
|
4
|
+
merleau-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
+
merleau-0.3.0.dist-info/entry_points.txt,sha256=pMCcADIqyZbK-lQ5d2iLKIWqNeeY3pAQDHI1IBmtdEQ,43
|
|
6
|
+
merleau-0.3.0.dist-info/RECORD,,
|
merleau-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
merleau/__init__.py,sha256=y5-8LWAim3ziWojAKzkwChDCV3rFkVzPmJ3m6Lkojgo,81
|
|
2
|
-
merleau/cli.py,sha256=XQz6ncqj4Zl_jLAmiRhhYXdn7kFNM76ReuG0gTvF9KY,3471
|
|
3
|
-
merleau-0.2.0.dist-info/METADATA,sha256=-71rumv5Gjq75t9fwzKSUBl_W-cPm1rLMsUrKlhRimE,192
|
|
4
|
-
merleau-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
-
merleau-0.2.0.dist-info/entry_points.txt,sha256=pMCcADIqyZbK-lQ5d2iLKIWqNeeY3pAQDHI1IBmtdEQ,43
|
|
6
|
-
merleau-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|