File size: 7,272 Bytes
106ab05 df03a2f 106ab05 df03a2f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
import streamlit as st
import requests
import json
import base64
# Configuration
API_URL = "https://openrouter.ai/api/v1/chat/completions"
def encode_image(uploaded_file):
"""Encodes the uploaded file to base64."""
bytes_data = uploaded_file.getvalue()
return base64.b64encode(bytes_data).decode('utf-8')
def analyze_receipt(base64_image, prompt_text, api_key):
"""Sends the image to OpenRouter API for analysis."""
if not api_key:
return {"error": "API Key is missing."}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
data_url = f"data:image/jpeg;base64,{base64_image}"
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": prompt_text
},
{
"type": "image_url",
"image_url": {
"url": data_url
}
}
]
}
]
payload = {
"model": "qwen/qwen3-vl-8b-instruct",
"messages": messages
}
try:
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status() # Raise an error for bad status codes
return response.json()
except requests.exceptions.RequestException as e:
return {"error": str(e)}
# Streamlit App UI
st.set_page_config(page_title="Receipt Analyzer", page_icon="🧾", layout="wide")
st.title("🧾 Receipt Cost Breakdown (Qwen 3-VL-8B)")
st.markdown("Upload a receipt image to get a JSON breakdown of costs.")
# Sidebar for configuration
with st.sidebar:
st.header("⚙️ Configuration")
# API Key Input for User Inference
st.subheader("API Access")
api_key = st.text_input("OpenRouter API Key", type="password", help="Enter your OpenRouter API Key here.")
if not api_key:
st.warning("Please enter your API Key to proceed.")
st.divider()
# User-friendly schema builder - PRIORITY 1
# st.markdown("Define what to extract from the receipt.") # Removed to save space
st.subheader("Fields to Extract")
default_fields = ["Merchant Name", "Total Amount", "Currency", "Date"]
available_fields = ["Merchant Name", "Total Amount", "Currency", "Date", "Tax/VAT", "Address", "Time", "Payment Method"]
selected_fields = st.multiselect(
"Select fields:",
options=available_fields,
default=default_fields,
help="Leave empty to extract **ALL** available information automatically."
)
if not selected_fields:
st.caption("✅ *No fields selected. The model will extract everything it finds.*")
extract_line_items = st.checkbox("Extract Line Items (Name & Price)", value=True)
st.divider()
# Custom instructions - PRIORITY 2
custom_instructions = st.text_input(
"Custom Instructions (Optional)",
placeholder="e.g., Extract the cashier name",
help="Add any specific data points or rules not covered above."
)
st.divider()
# Model Indicator - MOVED TO BOTTOM
with st.expander("ℹ️ About the Model", expanded=False):
st.info(
"**Qwen 3-VL-8B**\n\n"
"This is an open-source model efficient enough to run locally on consumer hardware."
)
# Construct the prompt dynamically
if not selected_fields:
# User selected nothing -> Extract all
prompt_text = "Analyze this receipt image. Extract **all** visible information including merchant details, dates, totals, taxes, and address in a structured JSON format."
else:
# User selected specific fields
field_str = ", ".join(selected_fields)
prompt_text = f"Analyze this receipt image. Extract the following information in JSON format: {field_str}."
if extract_line_items:
prompt_text += " Also include a detailed list of 'items' containing 'name' and 'price'."
if custom_instructions:
prompt_text += f" Additionally: {custom_instructions}."
# Enforce JSON structure
prompt_text += " Return a single valid JSON object. Do not include markdown formatting."
# Store in variable to match existing function call
custom_prompt = prompt_text
uploaded_file = st.file_uploader("Choose a receipt image...", type=["jpg", "jpeg", "png"])
if uploaded_file is not None:
col1, col2 = st.columns(2)
with col1:
# Display the uploaded image
st.image(uploaded_file, caption="Uploaded Receipt", use_column_width=True)
analyze = st.button("Analyze Receipt", type="primary", use_container_width=True)
with col2:
if analyze:
if not api_key:
st.error("Please enter an API Key in the sidebar.")
else:
with st.spinner("Analyzing receipt..."):
# Encode image
base64_image = encode_image(uploaded_file)
# Call API
api_result = analyze_receipt(base64_image, custom_prompt, api_key)
# Handle response
if "error" in api_result:
st.error(f"Error calling API: {api_result['error']}")
elif "choices" in api_result:
content = api_result["choices"][0]["message"]["content"]
with st.expander("🔍 Raw Analysis Output"):
st.code(content, language="json")
# Try to clean and parse JSON if markdown code blocks are used
try:
# Clean up code blocks if present
json_str = content.replace("```json", "").replace("```", "").strip()
parsed_json = json.loads(json_str)
st.success("Analysis Complete!")
st.subheader("Structured Data")
st.json(parsed_json)
# Optional: Display as a nice table
if "items" in parsed_json and isinstance(parsed_json["items"], list):
st.subheader("Itemized Breakdown")
st.dataframe(parsed_json["items"], use_container_width=True)
# Display other top-level keys as metrics if simple
for key, value in parsed_json.items():
if key != "items" and isinstance(value, (int, float, str)):
st.metric(key.title(), value)
except json.JSONDecodeError:
st.warning("Could not parse the response as JSON. See the raw output above.")
else:
st.error("Unexpected response format from API.")
st.json(api_result)
|