danielrosehill commited on
Commit
932db80
Β·
1 Parent(s): 630fb49
Files changed (2) hide show
  1. app.py +158 -20
  2. test-local.sh +37 -0
app.py CHANGED
@@ -1,9 +1,11 @@
1
  import gradio as gr
2
  import os
3
- from PIL import Image
4
  import io
5
  import base64
6
  from openai import OpenAI
 
 
7
 
8
  def encode_image(image):
9
  """Convert PIL Image to base64 string for API"""
@@ -11,6 +13,69 @@ def encode_image(image):
11
  image.save(buffered, format="JPEG", quality=95)
12
  return base64.b64encode(buffered.getvalue()).decode('utf-8')
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  def analyze_satellite_image(image, geolocation, brief, analysis_mode, api_key):
15
  """
16
  Analyze satellite imagery using Meta Llama Vision via OpenRouter
@@ -74,14 +139,32 @@ Provide your analysis in a structured format covering:
74
  else: # annotated mode
75
  instruction = f"""Analyze this satellite image and provide a professional intelligence assessment with annotations.{location_context}{brief_context}
76
 
77
- First, identify key features that should be annotated. Then provide:
78
- 1. A detailed description of what annotations you would place (describe location, label, and significance of each)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  2. Overview and general observations
80
- 3. Key features and infrastructure identified
81
- 4. Notable patterns or anomalies
82
- 5. Assessment and implications (if relevant to the brief)
83
 
84
- Note: Describe the annotations you would make in text form, as you cannot directly draw on images. Be specific about locations (e.g., "top-left quadrant", "center-right", using approximate coordinates or descriptive positions)."""
85
 
86
  # Encode image
87
  image_data = encode_image(image)
@@ -116,10 +199,47 @@ Note: Describe the annotations you would make in text form, as you cannot direct
116
 
117
  analysis_text = response.choices[0].message.content
118
 
119
- # For annotated mode, return both text and original image
120
- # (Future enhancement: could add actual annotation drawing)
121
  if analysis_mode == "annotated":
122
- return analysis_text, image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  else:
124
  return analysis_text, None
125
 
@@ -179,15 +299,19 @@ with gr.Blocks(title="SATINT Analyst - Satellite Imagery Analysis", theme=gr.the
179
  analyze_btn = gr.Button("Analyze Imagery", variant="primary", size="lg")
180
 
181
  with gr.Column(scale=1):
182
- analysis_output = gr.Textbox(
183
- label="Intelligence Analysis",
184
- lines=20,
185
- max_lines=30,
186
- show_copy_button=True
 
 
187
  )
 
 
188
 
189
  annotated_output = gr.Image(
190
- label="Reference Image (for annotated mode)",
191
  visible=True
192
  )
193
 
@@ -196,8 +320,9 @@ with gr.Blocks(title="SATINT Analyst - Satellite Imagery Analysis", theme=gr.the
196
  ### Usage Tips
197
  - **Geolocation**: Use decimal notation (e.g., 38.8977, -77.0365) for latitude and longitude
198
  - **Brief**: Provide specific questions or focus areas for more targeted analysis
199
- - **Text Only Mode**: Receive a detailed written analysis
200
- - **Annotated Mode**: Receive analysis with descriptions of key features and their locations
 
201
 
202
  ### Privacy & Model
203
  - **Model**: Meta Llama 3.2 Vision 90B (via OpenRouter)
@@ -207,10 +332,23 @@ with gr.Blocks(title="SATINT Analyst - Satellite Imagery Analysis", theme=gr.the
207
  """)
208
 
209
  # Set up the analyze button
 
 
 
 
 
210
  analyze_btn.click(
211
- fn=analyze_satellite_image,
212
  inputs=[image_input, geolocation_input, brief_input, analysis_mode, api_key_input],
213
- outputs=[analysis_output, annotated_output]
 
 
 
 
 
 
 
 
214
  )
215
 
216
  if __name__ == "__main__":
 
1
  import gradio as gr
2
  import os
3
+ from PIL import Image, ImageDraw, ImageFont
4
  import io
5
  import base64
6
  from openai import OpenAI
7
+ import re
8
+ import json
9
 
10
  def encode_image(image):
11
  """Convert PIL Image to base64 string for API"""
 
13
  image.save(buffered, format="JPEG", quality=95)
14
  return base64.b64encode(buffered.getvalue()).decode('utf-8')
15
 
16
+ def draw_annotations(image, annotations):
17
+ """
18
+ Draw numbered annotations on the image
19
+
20
+ Args:
21
+ image: PIL Image object
22
+ annotations: List of dicts with 'x', 'y', 'label' keys (coordinates are 0-1 normalized)
23
+
24
+ Returns:
25
+ PIL Image with annotations drawn
26
+ """
27
+ # Create a copy to avoid modifying original
28
+ img_copy = image.copy()
29
+ draw = ImageDraw.Draw(img_copy)
30
+
31
+ # Get image dimensions
32
+ width, height = img_copy.size
33
+
34
+ # Try to load a better font, fall back to default if not available
35
+ try:
36
+ font_size = max(20, min(width, height) // 40)
37
+ font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size)
38
+ except:
39
+ font = ImageFont.load_default()
40
+
41
+ # Draw each annotation
42
+ for i, ann in enumerate(annotations, 1):
43
+ # Convert normalized coordinates to pixel coordinates
44
+ x = int(ann['x'] * width)
45
+ y = int(ann['y'] * height)
46
+
47
+ # Circle radius based on image size
48
+ radius = max(15, min(width, height) // 80)
49
+
50
+ # Draw outer circle (white border)
51
+ draw.ellipse(
52
+ [(x - radius - 2, y - radius - 2), (x + radius + 2, y + radius + 2)],
53
+ fill='white',
54
+ outline='white'
55
+ )
56
+
57
+ # Draw inner circle (red)
58
+ draw.ellipse(
59
+ [(x - radius, y - radius), (x + radius, y + radius)],
60
+ fill='red',
61
+ outline='white',
62
+ width=2
63
+ )
64
+
65
+ # Draw number
66
+ number_text = str(i)
67
+ # Get text bounding box for centering
68
+ bbox = draw.textbbox((0, 0), number_text, font=font)
69
+ text_width = bbox[2] - bbox[0]
70
+ text_height = bbox[3] - bbox[1]
71
+
72
+ # Draw text centered in circle
73
+ text_x = x - text_width // 2
74
+ text_y = y - text_height // 2
75
+ draw.text((text_x, text_y), number_text, fill='white', font=font)
76
+
77
+ return img_copy
78
+
79
  def analyze_satellite_image(image, geolocation, brief, analysis_mode, api_key):
80
  """
81
  Analyze satellite imagery using Meta Llama Vision via OpenRouter
 
139
  else: # annotated mode
140
  instruction = f"""Analyze this satellite image and provide a professional intelligence assessment with annotations.{location_context}{brief_context}
141
 
142
+ You MUST format your response in TWO sections:
143
+
144
+ SECTION 1 - ANNOTATIONS (JSON):
145
+ Provide a JSON array of annotation points. Each point should have:
146
+ - "x": horizontal position (0.0 to 1.0, where 0.0 is left edge, 1.0 is right edge)
147
+ - "y": vertical position (0.0 to 1.0, where 0.0 is top edge, 1.0 is bottom edge)
148
+ - "label": brief description of the feature
149
+
150
+ Start this section with exactly: ANNOTATIONS:
151
+ Then provide valid JSON on the next line.
152
+
153
+ Example format:
154
+ ANNOTATIONS:
155
+ [
156
+ {{"x": 0.25, "y": 0.35, "label": "Military installation"}},
157
+ {{"x": 0.75, "y": 0.60, "label": "Vehicle staging area"}}
158
+ ]
159
+
160
+ SECTION 2 - ANALYSIS:
161
+ Provide your detailed analysis referencing the numbered annotations (1, 2, 3, etc.) that will be drawn on the image:
162
+ 1. Key features identified (reference annotation numbers)
163
  2. Overview and general observations
164
+ 3. Notable patterns or anomalies
165
+ 4. Assessment and implications (if relevant to the brief)
 
166
 
167
+ Remember: The annotations will be numbered automatically in the order you list them."""
168
 
169
  # Encode image
170
  image_data = encode_image(image)
 
199
 
200
  analysis_text = response.choices[0].message.content
201
 
202
+ # For annotated mode, parse annotations and draw on image
 
203
  if analysis_mode == "annotated":
204
+ try:
205
+ # Extract annotations JSON from response
206
+ annotations = []
207
+ annotated_image = image
208
+
209
+ # Look for ANNOTATIONS: section
210
+ if "ANNOTATIONS:" in analysis_text:
211
+ # Extract the JSON part
212
+ parts = analysis_text.split("ANNOTATIONS:")
213
+ if len(parts) > 1:
214
+ json_part = parts[1].split("SECTION 2")[0].strip()
215
+ # Also try splitting by "ANALYSIS:" if SECTION 2 not found
216
+ if "ANALYSIS:" in json_part:
217
+ json_part = json_part.split("ANALYSIS:")[0].strip()
218
+
219
+ # Try to extract JSON array
220
+ json_match = re.search(r'\[.*?\]', json_part, re.DOTALL)
221
+ if json_match:
222
+ json_str = json_match.group(0)
223
+ annotations = json.loads(json_str)
224
+
225
+ # Draw annotations on image
226
+ if annotations:
227
+ annotated_image = draw_annotations(image, annotations)
228
+
229
+ # Clean up the analysis text to remove JSON section
230
+ # Keep only the analysis part
231
+ if "ANALYSIS:" in analysis_text:
232
+ analysis_text = "ANALYSIS:\n" + analysis_text.split("ANALYSIS:")[1].strip()
233
+ elif "SECTION 2" in analysis_text:
234
+ analysis_text = analysis_text.split("SECTION 2")[1].strip()
235
+ if analysis_text.startswith("- ANALYSIS:"):
236
+ analysis_text = analysis_text[12:].strip()
237
+
238
+ return analysis_text, annotated_image
239
+
240
+ except Exception as e:
241
+ # If annotation parsing fails, return original image with a note
242
+ return f"[Note: Annotation parsing failed: {str(e)}]\n\n{analysis_text}", image
243
  else:
244
  return analysis_text, None
245
 
 
299
  analyze_btn = gr.Button("Analyze Imagery", variant="primary", size="lg")
300
 
301
  with gr.Column(scale=1):
302
+ gr.Markdown("### Intelligence Analysis")
303
+ with gr.Row():
304
+ copy_btn = gr.Button("πŸ“‹ Copy to Clipboard", size="sm", scale=0)
305
+ analysis_output = gr.Markdown(
306
+ value="*Analysis will appear here...*",
307
+ height=600,
308
+ elem_classes="analysis-box"
309
  )
310
+ # Hidden textbox to hold raw text for copying
311
+ analysis_text_raw = gr.Textbox(visible=False)
312
 
313
  annotated_output = gr.Image(
314
+ label="Annotated Image",
315
  visible=True
316
  )
317
 
 
320
  ### Usage Tips
321
  - **Geolocation**: Use decimal notation (e.g., 38.8977, -77.0365) for latitude and longitude
322
  - **Brief**: Provide specific questions or focus areas for more targeted analysis
323
+ - **Text Only Mode**: Receive a detailed written analysis with markdown formatting
324
+ - **Annotated Mode**: Receive analysis with numbered annotations drawn on the image referencing key features
325
+ - **Copy Button**: Click the clipboard button to copy the analysis text
326
 
327
  ### Privacy & Model
328
  - **Model**: Meta Llama 3.2 Vision 90B (via OpenRouter)
 
332
  """)
333
 
334
  # Set up the analyze button
335
+ def process_analysis(image, geolocation, brief, analysis_mode, api_key):
336
+ """Wrapper to return results for both markdown and raw text"""
337
+ text, img = analyze_satellite_image(image, geolocation, brief, analysis_mode, api_key)
338
+ return text, text, img # markdown display, raw text for copying, image
339
+
340
  analyze_btn.click(
341
+ fn=process_analysis,
342
  inputs=[image_input, geolocation_input, brief_input, analysis_mode, api_key_input],
343
+ outputs=[analysis_output, analysis_text_raw, annotated_output]
344
+ )
345
+
346
+ # Set up copy button to copy from hidden textbox
347
+ copy_btn.click(
348
+ fn=lambda x: x,
349
+ inputs=[analysis_text_raw],
350
+ outputs=[],
351
+ js="(text) => {navigator.clipboard.writeText(text); return text;}"
352
  )
353
 
354
  if __name__ == "__main__":
test-local.sh ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Local testing script for SATINT-Analyst Hugging Face Space
4
+ # This uses uv for fast dependency management locally
5
+ # Hugging Face will still use requirements.txt when deployed
6
+
7
+ set -e # Exit on error
8
+
9
+ echo "πŸš€ Setting up local testing environment with uv..."
10
+
11
+ # Check if uv is installed
12
+ if ! command -v uv &> /dev/null; then
13
+ echo "❌ uv is not installed. Install it with: pip install uv"
14
+ exit 1
15
+ fi
16
+
17
+ # Create virtual environment if it doesn't exist
18
+ if [ ! -d ".venv" ]; then
19
+ echo "πŸ“¦ Creating virtual environment..."
20
+ uv venv
21
+ fi
22
+
23
+ # Activate virtual environment
24
+ echo "πŸ”Œ Activating virtual environment..."
25
+ source .venv/bin/activate
26
+
27
+ # Install dependencies from requirements.txt using uv (much faster than pip)
28
+ echo "πŸ“₯ Installing dependencies with uv..."
29
+ uv pip install -r requirements.txt
30
+
31
+ # Run the Gradio app
32
+ echo "🎯 Starting Gradio app..."
33
+ echo "πŸ“ App will be available at http://127.0.0.1:7860"
34
+ echo "Press Ctrl+C to stop"
35
+ echo ""
36
+
37
+ python app.py