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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """Merleau - Video analysis using Google's Gemini API."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
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
- def wait_for_processing(client, file):
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
- print(".", end="", flush=True)
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
- print()
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
- # Gemini 2.5 Flash pricing (as of 2025):
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 analyze(video_path, prompt, model, show_cost):
42
- """Analyze a video file using Gemini."""
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
- print("Error: GEMINI_API_KEY not found in environment or .env file", file=sys.stderr)
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
- print(f"Error: Video file not found: {video_path}", file=sys.stderr)
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
- print(f"Upload complete. File URI: {myfile.uri}")
105
+ if on_upload:
106
+ on_upload(myfile.uri)
60
107
 
61
108
  # Wait for processing
62
- print("Waiting for file to be processed...", end="")
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
- print(f"Error: File processing failed", file=sys.stderr)
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
- print(f"\nAnalyzing video with {model}...")
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
- print("\n--- Video Analysis ---")
79
- print(response.text)
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
- # Show usage if requested
82
- if show_cost and hasattr(response, 'usage_metadata'):
83
- print_usage(response.usage_metadata)
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.2.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,,
@@ -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,,