|
import gradio as gr |
|
import os |
|
import re |
|
from openai import OpenAI |
|
import json |
|
import requests |
|
from PIL import Image |
|
import io |
|
import base64 |
|
|
|
|
|
DEFAULT_MODEL = "opengvlab/internvl3-14b:free" |
|
DEFAULT_SITE_URL = "https://dynamic-nature-trail-guide.app" |
|
DEFAULT_SITE_NAME = "Dynamic Nature Trail Guide" |
|
|
|
|
|
SYSTEM_PROMPT = """ |
|
You are the Dynamic Nature Trail Guide, an expert in identifying and explaining natural elements |
|
found on nature trails. For any image sent, please: |
|
1. Identify all visible plants, animals, geological features, and ecosystems |
|
2. Provide educational information about identified elements |
|
3. Mention any seasonal characteristics visible in the image |
|
4. Note any ecological significance or conservation considerations |
|
5. Offer suggestions for what to observe or learn next on the trail |
|
Keep explanations informative yet accessible to people of all ages and backgrounds. |
|
IMPORTANT: Structure your responses with clear sections and headings. |
|
""" |
|
|
|
def encode_image_to_base64(image_path): |
|
"""Convert an image file to base64 encoding""" |
|
with open(image_path, "rb") as image_file: |
|
return base64.b64encode(image_file.read()).decode('utf-8') |
|
|
|
def format_response_as_html(text): |
|
"""Convert the model's text response to formatted HTML with improved section handling""" |
|
if not text: |
|
return "" |
|
|
|
|
|
if text.startswith("Error analyzing image:"): |
|
return f'<div style="color: red; padding: 10px; border: 1px solid red; border-radius: 5px;">{text}</div>' |
|
|
|
|
|
|
|
sections = re.split(r'(?:#{1,3}\s*\d*\.?\s*|\*\*)([\w\s]+)(?:\*\*|:)', text) |
|
|
|
|
|
if len(sections) <= 1: |
|
|
|
text = text.replace('\n\n', '</p><p>') |
|
return f'<div style="padding: 15px; font-family: Arial, sans-serif; line-height: 1.6;"><p>{text}</p></div>' |
|
|
|
|
|
html_parts = ['<div style="padding: 15px; font-family: Arial, sans-serif; line-height: 1.6;">'] |
|
|
|
for i in range(1, len(sections), 2): |
|
if i < len(sections) - 1: |
|
section_title = sections[i].strip() |
|
section_content = sections[i+1].strip() |
|
|
|
|
|
section_content = format_section_content(section_content) |
|
|
|
html_parts.append(f'<div class="section">') |
|
html_parts.append(f'<h3 style="color: #336699; border-bottom: 1px solid #ccc; padding-bottom: 5px;">{section_title}</h3>') |
|
html_parts.append(f'<div class="content">{section_content}</div>') |
|
html_parts.append('</div>') |
|
|
|
|
|
if len(html_parts) == 1: |
|
text = format_section_content(text) |
|
html_parts.append(f'<p>{text}</p>') |
|
|
|
html_parts.append('</div>') |
|
return ''.join(html_parts) |
|
|
|
def format_section_content(content): |
|
"""Format the content of a section with enhanced styling""" |
|
|
|
content = re.sub(r'\n\s*\n', '</p><p>', content) |
|
content = re.sub(r'\n(?!<\/p>)', '<br>', content) |
|
|
|
|
|
content = re.sub(r'^\s*-\s*(.+?)$', r'<li>\1</li>', content, flags=re.MULTILINE) |
|
content = re.sub(r'(<li>.*?</li>)', r'<ul>\1</ul>', content, flags=re.DOTALL) |
|
|
|
content = re.sub(r'<ul>\s*</ul>', '', content) |
|
|
|
|
|
content = re.sub(r'\*\*([\w\s]+):\*\*', r'<h4 style="margin-bottom: 5px; color: #336699;">\1</h4>', content) |
|
|
|
|
|
content = re.sub(r'\b([A-Z][a-z]+\s+[a-z]+)\b(?!\<\/)', r'<strong><em>\1</em></strong>', content) |
|
|
|
|
|
color_mappings = { |
|
'endangered': '#e74c3c', |
|
'rare': '#FF6600', |
|
'native': '#27ae60', |
|
'invasive': '#CC0000', |
|
'ecosystem': '#3498db', |
|
'habitat': '#336699', |
|
'conservation': '#16a085', |
|
'protected': '#9b59b6', |
|
'biodiversity': '#2ecc71' |
|
} |
|
|
|
for keyword, color in color_mappings.items(): |
|
content = re.sub(r'\b' + keyword + r'\b', f'<span style="color: {color}; font-weight: bold;">{keyword}</span>', content, flags=re.IGNORECASE) |
|
|
|
|
|
if not content.startswith('<p>') and not content.startswith('<ul>') and not content.startswith('<h4'): |
|
content = '<p>' + content |
|
if not content.endswith('</p>') and not content.endswith('</ul>'): |
|
content = content + '</p>' |
|
|
|
return content |
|
|
|
def analyze_image(api_key, image, prompt="What can you identify in this nature trail image? Provide detailed educational information.", site_url=DEFAULT_SITE_URL, site_name=DEFAULT_SITE_NAME, model=DEFAULT_MODEL): |
|
"""Analyze the uploaded image using the InternVL3 model via OpenRouter""" |
|
if not api_key: |
|
return "<div style='color: red; padding: 10px; border: 1px solid red; border-radius: 5px;'>Please provide an OpenRouter API key.</div>" |
|
|
|
if image is None: |
|
return "<div style='color: red; padding: 10px; border: 1px solid red; border-radius: 5px;'>Please upload an image to analyze.</div>" |
|
|
|
|
|
temp_image_path = "temp_image.jpg" |
|
image.save(temp_image_path) |
|
|
|
try: |
|
|
|
base64_image = encode_image_to_base64(temp_image_path) |
|
|
|
|
|
client = OpenAI( |
|
base_url="https://openrouter.ai/api/v1", |
|
api_key=api_key, |
|
) |
|
|
|
|
|
response = client.chat.completions.create( |
|
extra_headers={ |
|
"HTTP-Referer": site_url, |
|
"X-Title": site_name, |
|
}, |
|
model=model, |
|
messages=[ |
|
{"role": "system", "content": SYSTEM_PROMPT}, |
|
{ |
|
"role": "user", |
|
"content": [ |
|
{ |
|
"type": "text", |
|
"text": prompt |
|
}, |
|
{ |
|
"type": "image_url", |
|
"image_url": { |
|
"url": f"data:image/jpeg;base64,{base64_image}" |
|
} |
|
} |
|
] |
|
} |
|
] |
|
) |
|
|
|
analysis_result = response.choices[0].message.content |
|
return format_response_as_html(analysis_result) |
|
|
|
except Exception as e: |
|
error_message = f"Error analyzing image: {str(e)}" |
|
return f'<div style="color: red; padding: 10px; border: 1px solid red; border-radius: 5px;">{error_message}</div>' |
|
|
|
finally: |
|
|
|
if os.path.exists(temp_image_path): |
|
os.remove(temp_image_path) |
|
|
|
def build_custom_prompt(identification=True, education=True, seasonal=True, conservation=True, suggestions=True, additional_prompt=""): |
|
"""Build a custom prompt based on user preferences""" |
|
prompt_parts = [] |
|
|
|
if identification: |
|
prompt_parts.append("Identify all visible plants, animals, geological features, and ecosystems") |
|
|
|
if education: |
|
prompt_parts.append("Provide educational information about identified elements") |
|
|
|
if seasonal: |
|
prompt_parts.append("Mention any seasonal characteristics visible in the image") |
|
|
|
if conservation: |
|
prompt_parts.append("Note any ecological significance or conservation considerations") |
|
|
|
if suggestions: |
|
prompt_parts.append("Offer suggestions for what to observe or learn next on the trail") |
|
|
|
if additional_prompt: |
|
prompt_parts.append(additional_prompt) |
|
|
|
if not prompt_parts: |
|
return "What can you identify in this nature trail image?" |
|
|
|
numbered_prompt = "\n".join([f"{i+1}. {part}" for i, part in enumerate(prompt_parts)]) |
|
return f"For this nature trail image, please: \n{numbered_prompt}\n\nIMPORTANT: Structure your response with clear sections and headings for better readability." |
|
|
|
def create_interface(): |
|
"""Create the Gradio interface for the Dynamic Nature Trail Guide""" |
|
with gr.Blocks(title="Dynamic Nature Trail Guide", theme=gr.themes.Soft()) as app: |
|
gr.Markdown(""" |
|
# ๐ฟ Dynamic Nature Trail Guide: Accessible Outdoor Education ๐ฟ |
|
|
|
Upload an image from your nature walk to identify plants, animals, geological features, and learn about the ecosystem. |
|
This application uses the advanced InternVL3 14B multimodal model for nature identification and education. |
|
""") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
api_key_input = gr.Textbox( |
|
label="OpenRouter API Key", |
|
placeholder="Enter your OpenRouter API key here...", |
|
type="password" |
|
) |
|
image_input = gr.Image(label="Upload Nature Image", type="pil") |
|
|
|
with gr.Accordion("Advanced Settings", open=False): |
|
site_url = gr.Textbox( |
|
label="Site URL (for OpenRouter)", |
|
value=DEFAULT_SITE_URL |
|
) |
|
site_name = gr.Textbox( |
|
label="Site Name (for OpenRouter)", |
|
value=DEFAULT_SITE_NAME |
|
) |
|
model_selection = gr.Dropdown( |
|
label="Model", |
|
choices=[DEFAULT_MODEL], |
|
value=DEFAULT_MODEL |
|
) |
|
|
|
with gr.Accordion("Customize Analysis", open=False): |
|
gr.Markdown("Select what information you want to receive about the image:") |
|
identification_checkbox = gr.Checkbox(label="Species & Feature Identification", value=True) |
|
education_checkbox = gr.Checkbox(label="Educational Information", value=True) |
|
seasonal_checkbox = gr.Checkbox(label="Seasonal Characteristics", value=True) |
|
conservation_checkbox = gr.Checkbox(label="Conservation Considerations", value=True) |
|
suggestions_checkbox = gr.Checkbox(label="Trail Suggestions", value=True) |
|
additional_prompt = gr.Textbox(label="Additional Instructions (Optional)") |
|
|
|
analyze_button = gr.Button("Analyze Nature Image", variant="primary") |
|
|
|
with gr.Column(scale=1): |
|
output_text = gr.HTML(label="Analysis Results") |
|
|
|
|
|
analyze_button.click( |
|
fn=lambda api_key, image, id_check, edu_check, season_check, conserve_check, suggest_check, add_prompt, site_url, site_name, model: |
|
analyze_image( |
|
api_key, |
|
image, |
|
build_custom_prompt(id_check, edu_check, season_check, conserve_check, suggest_check, add_prompt), |
|
site_url, |
|
site_name, |
|
model |
|
), |
|
inputs=[ |
|
api_key_input, |
|
image_input, |
|
identification_checkbox, |
|
education_checkbox, |
|
seasonal_checkbox, |
|
conservation_checkbox, |
|
suggestions_checkbox, |
|
additional_prompt, |
|
site_url, |
|
site_name, |
|
model_selection |
|
], |
|
outputs=output_text |
|
) |
|
|
|
|
|
with gr.Accordion("Example Images", open=False): |
|
gr.Markdown("Click on an example image to analyze it:") |
|
example_images = gr.Examples( |
|
examples=[ |
|
"examples/forest_trail.jpg", |
|
"examples/wetland_boardwalk.jpg", |
|
"examples/mountain_vista.jpg", |
|
"examples/coastal_trail.jpg", |
|
], |
|
inputs=image_input, |
|
label="Nature Trail Examples" |
|
) |
|
|
|
gr.Markdown(""" |
|
## How to Use This App |
|
|
|
1. Enter your OpenRouter API key (sign up at [openrouter.ai](https://openrouter.ai) if needed) |
|
2. Upload an image from your nature walk |
|
3. Customize what kind of information you want (optional) |
|
4. Click "Analyze Nature Image" |
|
5. Explore the detailed educational content about what you're seeing |
|
|
|
This application is designed to make nature more accessible and educational for everyone! |
|
""") |
|
|
|
return app |
|
|
|
|
|
if __name__ == "__main__": |
|
app = create_interface() |