geomind-ai 1.1.0__tar.gz → 1.2.0__tar.gz

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.
Files changed (23) hide show
  1. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/PKG-INFO +1 -1
  2. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/__init__.py +1 -1
  3. geomind_ai-1.2.0/geomind/agent.py +218 -0
  4. geomind_ai-1.2.0/geomind/cli.py +392 -0
  5. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/config.py +3 -3
  6. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/tools/processing.py +90 -38
  7. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/tools/stac_search.py +61 -11
  8. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/PKG-INFO +1 -1
  9. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/pyproject.toml +1 -1
  10. geomind_ai-1.1.0/geomind/agent.py +0 -457
  11. geomind_ai-1.1.0/geomind/cli.py +0 -199
  12. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/LICENSE +0 -0
  13. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/MANIFEST.in +0 -0
  14. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/README.md +0 -0
  15. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/tools/__init__.py +0 -0
  16. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind/tools/geocoding.py +0 -0
  17. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/SOURCES.txt +0 -0
  18. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/dependency_links.txt +0 -0
  19. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/entry_points.txt +0 -0
  20. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/requires.txt +0 -0
  21. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/geomind_ai.egg-info/top_level.txt +0 -0
  22. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/requirements.txt +0 -0
  23. {geomind_ai-1.1.0 → geomind_ai-1.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geomind-ai
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: AI agent for geospatial analysis with Sentinel-2 satellite imagery
5
5
  Author: Harsh Shinde, Rajat Shinde
6
6
  License-Expression: MIT
@@ -3,7 +3,7 @@ GeoMind - Geospatial AI Agent
3
3
 
4
4
  """
5
5
 
6
- __version__ = "1.1.0"
6
+ __version__ = "1.2.0"
7
7
  __author__ = "Harsh Shinde, Rajat Shinde"
8
8
 
9
9
  from .agent import GeoMindAgent
@@ -0,0 +1,218 @@
1
+ """
2
+ GeoMind Agent - Simplified AI agent for satellite imagery analysis.
3
+ """
4
+
5
+ import json
6
+ from typing import Optional
7
+ from datetime import datetime
8
+ from openai import OpenAI
9
+
10
+ from .config import OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL
11
+ from .tools import (
12
+ list_recent_imagery,
13
+ create_rgb_composite,
14
+ calculate_ndvi,
15
+ search_imagery,
16
+ get_bbox_from_location,
17
+ geocode_location,
18
+ get_item_details,
19
+ get_band_statistics,
20
+ )
21
+
22
+ # Tool function mapping
23
+ TOOL_FUNCTIONS = {
24
+ "list_recent_imagery": list_recent_imagery,
25
+ "create_rgb_composite": create_rgb_composite,
26
+ "calculate_ndvi": calculate_ndvi,
27
+ "search_imagery": search_imagery,
28
+ "get_bbox_from_location": get_bbox_from_location,
29
+ "geocode_location": geocode_location,
30
+ "get_item_details": get_item_details,
31
+ "get_band_statistics": get_band_statistics,
32
+ }
33
+
34
+ # Simplified tool definitions
35
+ TOOLS = [
36
+ {
37
+ "type": "function",
38
+ "function": {
39
+ "name": "list_recent_imagery",
40
+ "description": "Find recent Sentinel-2 imagery for a location",
41
+ "parameters": {
42
+ "type": "object",
43
+ "properties": {
44
+ "location_name": {"type": "string", "description": "Location name"},
45
+ "days": {"type": "integer", "description": "Days to look back (default: 14)"},
46
+ "max_cloud_cover": {"type": "number", "description": "Max cloud cover %"},
47
+ "max_items": {"type": "integer", "description": "Max results"},
48
+ },
49
+ "required": [],
50
+ },
51
+ },
52
+ },
53
+ {
54
+ "type": "function",
55
+ "function": {
56
+ "name": "create_rgb_composite",
57
+ "description": "Create RGB composite from Sentinel-2 data",
58
+ "parameters": {
59
+ "type": "object",
60
+ "properties": {
61
+ "zarr_url": {"type": "string", "description": "SR_10m Zarr URL"},
62
+ "location_name": {"type": "string", "description": "Location for title"},
63
+ "subset_size": {"type": "integer", "description": "Image size (default: 1000)"},
64
+ },
65
+ "required": ["zarr_url"],
66
+ },
67
+ },
68
+ },
69
+ {
70
+ "type": "function",
71
+ "function": {
72
+ "name": "calculate_ndvi",
73
+ "description": "Calculate NDVI vegetation index",
74
+ "parameters": {
75
+ "type": "object",
76
+ "properties": {
77
+ "zarr_url": {"type": "string", "description": "SR_10m Zarr URL"},
78
+ "location_name": {"type": "string", "description": "Location for title"},
79
+ "subset_size": {"type": "integer", "description": "Image size (default: 1000)"},
80
+ },
81
+ "required": ["zarr_url"],
82
+ },
83
+ },
84
+ },
85
+ {
86
+ "type": "function",
87
+ "function": {
88
+ "name": "search_imagery",
89
+ "description": "Search Sentinel-2 imagery by parameters",
90
+ "parameters": {
91
+ "type": "object",
92
+ "properties": {
93
+ "bbox": {"type": "array", "items": {"type": "number"}, "description": "Bounding box"},
94
+ "start_date": {"type": "string", "description": "Start date YYYY-MM-DD"},
95
+ "end_date": {"type": "string", "description": "End date YYYY-MM-DD"},
96
+ "max_cloud_cover": {"type": "number", "description": "Max cloud cover %"},
97
+ "max_items": {"type": "integer", "description": "Max results"},
98
+ },
99
+ "required": [],
100
+ },
101
+ },
102
+ },
103
+ {
104
+ "type": "function",
105
+ "function": {
106
+ "name": "get_bbox_from_location",
107
+ "description": "Get bounding box for a location",
108
+ "parameters": {
109
+ "type": "object",
110
+ "properties": {
111
+ "place_name": {"type": "string", "description": "Location name"},
112
+ "buffer_km": {"type": "number", "description": "Buffer distance in km"},
113
+ },
114
+ "required": ["place_name"],
115
+ },
116
+ },
117
+ },
118
+ ]
119
+
120
+
121
+ class GeoMindAgent:
122
+ """Simplified GeoMind agent for satellite imagery analysis."""
123
+
124
+ def __init__(self, model: Optional[str] = None, api_key: Optional[str] = None):
125
+ self.api_key = api_key or OPENROUTER_API_KEY
126
+ self.model_name = model or OPENROUTER_MODEL
127
+
128
+ if not self.api_key:
129
+ raise ValueError("OpenRouter API key required")
130
+
131
+ print(f"GeoMind Agent initialized with {self.model_name}")
132
+ self.client = OpenAI(base_url=OPENROUTER_API_URL, api_key=self.api_key)
133
+ self.history = []
134
+
135
+ def _get_system_prompt(self) -> str:
136
+ """Short, focused system prompt."""
137
+ return f"""You are GeoMind, an AI assistant for satellite imagery analysis using Sentinel-2 data.
138
+
139
+ Current date: {datetime.now().strftime('%Y-%m-%d')}
140
+
141
+ KEY RULES:
142
+ 1. Use list_recent_imagery to find satellite data for locations
143
+ 2. Extract Zarr URLs from results: item['zarr_assets']['SR_10m']['href']
144
+ 3. Always include location_name parameter when creating images
145
+ 4. Use actual function response data in your answers (don't make up paths/stats)
146
+ 5. Let functions use default output paths (saves to outputs/ folder as .png)
147
+
148
+ Be helpful and accurate - always use real data from function responses."""
149
+
150
+ def chat(self, message: str, verbose: bool = True) -> str:
151
+ """Send message to agent and get response."""
152
+ if verbose:
153
+ print(f"\nUser: {message}")
154
+ print("Processing...")
155
+
156
+ self.history.append({"role": "user", "content": message})
157
+ messages = [{"role": "system", "content": self._get_system_prompt()}] + self.history
158
+
159
+ for _ in range(10): # Max 10 iterations
160
+ response = self.client.chat.completions.create(
161
+ model=self.model_name,
162
+ messages=messages,
163
+ tools=TOOLS,
164
+ tool_choice="auto",
165
+ max_tokens=4096,
166
+ )
167
+
168
+ choice = response.choices[0]
169
+ assistant_message = choice.message
170
+
171
+ tool_calls = assistant_message.tool_calls
172
+ if tool_calls:
173
+ # Add assistant message with tool calls
174
+ messages.append({
175
+ "role": "assistant",
176
+ "content": assistant_message.content or "",
177
+ "tool_calls": [
178
+ {
179
+ "id": tc.id,
180
+ "type": "function",
181
+ "function": {"name": tc.function.name, "arguments": tc.function.arguments},
182
+ }
183
+ for tc in tool_calls
184
+ ],
185
+ })
186
+
187
+ # Execute tool calls
188
+ for tool_call in tool_calls:
189
+ func_name = tool_call.function.name
190
+ func_args = json.loads(tool_call.function.arguments)
191
+
192
+ print(f" Executing: {func_name}({func_args})")
193
+
194
+ try:
195
+ result = TOOL_FUNCTIONS[func_name](**func_args)
196
+ except Exception as e:
197
+ result = {"error": str(e)}
198
+
199
+ messages.append({
200
+ "role": "tool",
201
+ "tool_call_id": tool_call.id,
202
+ "content": json.dumps(result, default=str),
203
+ })
204
+ else:
205
+ # Final response
206
+ final_text = assistant_message.content or ""
207
+ self.history.append({"role": "assistant", "content": final_text})
208
+
209
+ if verbose:
210
+ print(f"\nGeoMind: {final_text}")
211
+
212
+ return final_text
213
+
214
+ return "Max iterations reached"
215
+
216
+ def reset(self):
217
+ """Reset chat history."""
218
+ self.history = []
@@ -0,0 +1,392 @@
1
+ """
2
+ Command-line interface for GeoMind.
3
+ """
4
+
5
+ import sys
6
+ import os
7
+ import argparse
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ import subprocess
11
+ import platform
12
+ import threading
13
+ import time
14
+
15
+ from .agent import GeoMindAgent
16
+
17
+
18
+ # Config file path for storing API key
19
+ CONFIG_DIR = Path.home() / ".geomind"
20
+ CONFIG_FILE = CONFIG_DIR / "config"
21
+
22
+
23
+ def get_saved_api_key() -> Optional[str]:
24
+ """Get API key saved on user's PC."""
25
+ if CONFIG_FILE.exists():
26
+ try:
27
+ return CONFIG_FILE.read_text().strip()
28
+ except Exception:
29
+ return None
30
+ return None
31
+
32
+
33
+ def save_api_key(api_key: str) -> bool:
34
+ """Save API key to user's PC."""
35
+ try:
36
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
37
+ CONFIG_FILE.write_text(api_key)
38
+ return True
39
+ except Exception:
40
+ return False
41
+
42
+
43
+ def display_recent_images():
44
+ """Display recently created images if any exist."""
45
+ outputs_dir = Path("outputs")
46
+ if not outputs_dir.exists():
47
+ return
48
+
49
+ # Get recent image files (created in last few seconds)
50
+ import time
51
+ recent_threshold = time.time() - 30 # 30 seconds ago
52
+
53
+ recent_images = []
54
+ for ext in ['*.png', '*.jpg', '*.jpeg', '*.tiff']:
55
+ for img_file in outputs_dir.glob(ext):
56
+ if img_file.stat().st_mtime > recent_threshold:
57
+ recent_images.append(img_file)
58
+
59
+ if recent_images:
60
+ print("\n" + "="*60)
61
+ print("Generated Images:")
62
+ for img in recent_images:
63
+ print(f" • {img.name} ({img.stat().st_size // 1024}KB)")
64
+
65
+ # Try to open the most recent image
66
+ if recent_images:
67
+ latest_image = max(recent_images, key=lambda x: x.stat().st_mtime)
68
+ open_image_viewer(latest_image)
69
+ print("="*60)
70
+
71
+
72
+ def open_image_viewer(image_path: Path):
73
+ """Open image in default viewer."""
74
+ try:
75
+ system = platform.system()
76
+ if system == "Windows":
77
+ os.startfile(str(image_path))
78
+ elif system == "Darwin": # macOS
79
+ subprocess.run(["open", str(image_path)], check=False)
80
+ else: # Linux
81
+ subprocess.run(["xdg-open", str(image_path)], check=False)
82
+ print(f" -> Opened {image_path.name} in default viewer")
83
+ except Exception:
84
+ print(f" -> Saved to: {image_path}")
85
+
86
+
87
+ def format_response_box(title: str, content: str, color_code: str = "\033[94m") -> str:
88
+ """Format response in an attractive box."""
89
+ RESET = "\033[0m"
90
+ lines = content.split('\n')
91
+ max_width = max(len(line) for line in lines) if lines else 0
92
+ max_width = max(max_width, len(title) + 4)
93
+ width = min(max_width + 4, 80)
94
+
95
+ box = f"{color_code}"
96
+ box += "┌" + "─" * (width - 2) + "┐\n"
97
+ box += f"│ {title:<{width-4}} │\n"
98
+ box += "├" + "─" * (width - 2) + "┤\n"
99
+
100
+ for line in lines:
101
+ if line.strip():
102
+ box += f"│ {line:<{width-4}} │\n"
103
+ else:
104
+ box += f"│{' ' * (width-2)}│\n"
105
+
106
+ box += "└" + "─" * (width - 2) + "┘"
107
+ box += RESET
108
+ return box
109
+
110
+
111
+ class ThinkingIndicator:
112
+ """Claude Code style thinking animation."""
113
+
114
+ def __init__(self):
115
+ self.is_thinking = False
116
+ self.thread = None
117
+ self.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
118
+ self.thinking_messages = [
119
+ "Thinking",
120
+ "Analyzing satellite data",
121
+ "Processing request",
122
+ "Searching imagery"
123
+ ]
124
+
125
+ def start(self):
126
+ """Start the thinking animation."""
127
+ self.is_thinking = True
128
+ self.thread = threading.Thread(target=self._animate)
129
+ self.thread.daemon = True
130
+ self.thread.start()
131
+
132
+ def stop(self):
133
+ """Stop the thinking animation."""
134
+ self.is_thinking = False
135
+ if self.thread:
136
+ self.thread.join(timeout=0.1)
137
+ # Clear the line
138
+ print("\r" + " " * 60 + "\r", end="", flush=True)
139
+
140
+ def _animate(self):
141
+ """Run the thinking animation."""
142
+ frame_idx = 0
143
+ message_idx = 0
144
+ message_counter = 0
145
+
146
+ # Colors like Claude Code
147
+ DIM = '\033[2m'
148
+ RESET = '\033[0m'
149
+
150
+ while self.is_thinking:
151
+ spinner = self.frames[frame_idx % len(self.frames)]
152
+
153
+ # Cycle through thinking messages every 30 frames (3 seconds)
154
+ if message_counter % 30 == 0:
155
+ message_idx = (message_idx + 1) % len(self.thinking_messages)
156
+
157
+ message = self.thinking_messages[message_idx]
158
+
159
+ # Show thinking with shimmer effect like Claude Code
160
+ print(f"\r{DIM}{spinner} {message}...{RESET}", end="", flush=True)
161
+
162
+ time.sleep(0.1)
163
+ frame_idx += 1
164
+ message_counter += 1
165
+
166
+
167
+ def main():
168
+ """Main CLI entry point for the geomind package."""
169
+ parser = argparse.ArgumentParser(
170
+ description="GeoMind - AI agent for geospatial analysis with Sentinel-2 imagery",
171
+ formatter_class=argparse.RawDescriptionHelpFormatter,
172
+ epilog="""
173
+ Examples:
174
+ # Interactive mode
175
+ geomind
176
+
177
+ # Single query
178
+ geomind --query "Find recent imagery of Paris"
179
+
180
+ # With custom model
181
+ geomind --model "anthropic/claude-3.5-sonnet"
182
+
183
+ # With API key
184
+ geomind --api-key "your-openrouter-api-key"
185
+
186
+ # Clear saved API key
187
+ geomind --clear-key
188
+
189
+ Environment Variables:
190
+ OPENROUTER_API_KEY Your OpenRouter API key
191
+ OPENROUTER_MODEL Model to use (default: nvidia/nemotron-3-nano-30b-a3b:free)
192
+ OPENROUTER_API_URL API endpoint (default: https://openrouter.ai/api/v1)
193
+ """,
194
+ )
195
+
196
+ parser.add_argument(
197
+ "--query",
198
+ "-q",
199
+ type=str,
200
+ help="Single query to run (if not provided, starts interactive mode)",
201
+ )
202
+ parser.add_argument(
203
+ "--model",
204
+ "-m",
205
+ type=str,
206
+ help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')",
207
+ )
208
+ parser.add_argument(
209
+ "--api-key",
210
+ "-k",
211
+ type=str,
212
+ help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)",
213
+ )
214
+ parser.add_argument(
215
+ "--version", "-v", action="store_true", help="Show version and exit"
216
+ )
217
+ parser.add_argument("--clear-key", action="store_true", help="Clear saved API key")
218
+
219
+ args = parser.parse_args()
220
+
221
+ if args.clear_key:
222
+ if CONFIG_FILE.exists():
223
+ CONFIG_FILE.unlink()
224
+ print("Saved API key cleared.")
225
+ else:
226
+ print("No saved API key found.")
227
+ sys.exit(0)
228
+
229
+ if args.version:
230
+ from . import __version__
231
+
232
+ print(f"GeoMind version {__version__}")
233
+ sys.exit(0)
234
+
235
+ # Start interactive or single-query mode
236
+ try:
237
+ if args.query:
238
+ # Single query mode - check API key in order: argument > env > saved file
239
+ from .config import OPENROUTER_API_KEY
240
+
241
+ api_key = args.api_key or OPENROUTER_API_KEY or get_saved_api_key()
242
+ if not api_key:
243
+ print("Error: No API key found. Run 'geomind' first to set up.")
244
+ sys.exit(1)
245
+ agent = GeoMindAgent(model=args.model, api_key=api_key)
246
+ agent.chat(args.query)
247
+ else:
248
+ # Interactive mode
249
+ run_interactive(model=args.model, api_key=args.api_key)
250
+ except ValueError as e:
251
+ print(f"\nError: {e}")
252
+ sys.exit(1)
253
+ except KeyboardInterrupt:
254
+ print("\n\nGoodbye!")
255
+ sys.exit(0)
256
+ except Exception as e:
257
+ print(f"\nUnexpected error: {e}")
258
+ sys.exit(1)
259
+
260
+
261
+ def print_banner():
262
+ from . import __version__
263
+
264
+ # ANSI color codes
265
+ BOLD = '\033[1m'
266
+ DIM = '\033[2m'
267
+ RESET = '\033[0m'
268
+
269
+ banner = f"""
270
+ ┌──────────────────────────────────────────────────────────────────────┐
271
+ │ {BOLD}>_ GeoMind{RESET} (v{__version__}) │
272
+ │ │
273
+ │ model: nvidia/nemotron-3-nano-30b-a3b:free │
274
+ │ docs: https://harshshinde0.github.io/GeoMind │
275
+ │ authors: Harsh Shinde, Rajat Shinde │
276
+ │ official: https://harshshinde0.github.io/GeoMind │
277
+ │ │
278
+ │ Type "?" for help, "quit" to exit. │
279
+ └──────────────────────────────────────────────────────────────────────┘
280
+ """
281
+ print(banner)
282
+ print()
283
+
284
+
285
+ def print_help():
286
+ """Print interactive session help."""
287
+ help_text = """
288
+ Interactive Commands:
289
+ help, ? Show this help
290
+ reset Reset conversation
291
+ exit, quit, q Exit GeoMind
292
+
293
+ Query Examples:
294
+ > Find recent Sentinel-2 imagery of Paris
295
+ > Show me NDVI data for the Amazon rainforest
296
+ > Search for images with less than 10% cloud cover in London
297
+ > Get satellite data for coordinates 40.7128, -74.0060
298
+
299
+ For CLI options, run: geomind --help
300
+ """
301
+ print(help_text)
302
+
303
+
304
+ def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
305
+ """Run interactive CLI mode."""
306
+ from . import __version__
307
+
308
+ print_banner()
309
+
310
+ # Check for API key in order: argument > env > saved file
311
+ from .config import OPENROUTER_API_KEY
312
+
313
+ # Priority: command line arg > env variable > saved file
314
+ if api_key:
315
+ # Use provided argument
316
+ pass
317
+ elif OPENROUTER_API_KEY:
318
+ api_key = OPENROUTER_API_KEY
319
+ else:
320
+ api_key = get_saved_api_key()
321
+
322
+ if not api_key:
323
+ print("\nOpenRouter API key required (FREE)")
324
+ print(" Get yours at: https://openrouter.ai/settings/keys\n")
325
+ api_key = input(" Enter your API key: ").strip()
326
+
327
+ if not api_key:
328
+ print("\nNo API key provided. Exiting.")
329
+ return
330
+
331
+ # Save the key for future use
332
+ if save_api_key(api_key):
333
+ print(" API key saved! You won't need to enter it again.\n")
334
+ else:
335
+ print(" Warning: Could not save API key. You'll need to enter it next time.\n")
336
+
337
+ agent = GeoMindAgent(model=model, api_key=api_key)
338
+
339
+ # Claude Code style color scheme
340
+ CYAN = '\033[96m'
341
+ DIM = '\033[2m'
342
+ BOLD = '\033[1m'
343
+ RESET = '\033[0m'
344
+
345
+ while True:
346
+ try:
347
+ # Simple prompt like Claude Code
348
+ user_input = input(f"\n{CYAN}>{RESET} ").strip()
349
+
350
+ if not user_input:
351
+ continue
352
+
353
+ if user_input.lower() in ["quit", "exit", "q"]:
354
+ print(f"\n{DIM}Goodbye!{RESET}")
355
+ break
356
+
357
+ if user_input.lower() == "reset":
358
+ agent.reset()
359
+ print(f"{DIM}Started new conversation{RESET}")
360
+ continue
361
+
362
+ if user_input.lower() in ["help", "?"]:
363
+ print_help()
364
+ continue
365
+
366
+ # Start thinking animation
367
+ thinking = ThinkingIndicator()
368
+ thinking.start()
369
+
370
+ try:
371
+ # Get response from agent
372
+ response = agent.chat(user_input, verbose=False)
373
+
374
+ # Stop thinking animation
375
+ thinking.stop()
376
+
377
+ # Display response cleanly like Claude Code
378
+ print(f"\n{response}")
379
+
380
+ except Exception as chat_error:
381
+ thinking.stop()
382
+ raise chat_error
383
+
384
+ except KeyboardInterrupt:
385
+ print(f"\n\n{DIM}Goodbye!{RESET}")
386
+ break
387
+ except Exception as e:
388
+ print(f"\n{DIM}Error: {e}{RESET}")
389
+
390
+
391
+ if __name__ == "__main__":
392
+ main()
@@ -37,9 +37,9 @@ REFLECTANCE_OFFSET = -0.1
37
37
  RGB_BANDS = {"red": "b04", "green": "b03", "blue": "b02"}
38
38
 
39
39
  # Default search parameters
40
- DEFAULT_MAX_CLOUD_COVER = 20 # percent
41
- DEFAULT_BUFFER_KM = 10 # km buffer around point for bbox
42
- DEFAULT_MAX_ITEMS = 10
40
+ DEFAULT_MAX_CLOUD_COVER = 50 # percent (increased for better results)
41
+ DEFAULT_BUFFER_KM = 15 # km buffer around point for bbox (increased coverage)
42
+ DEFAULT_MAX_ITEMS = 20 # increased to find more options
43
43
 
44
44
  # Output directory for saved images
45
45
  OUTPUT_DIR = Path("outputs")