Propelis/QC_Rules
0
1import streamlit as st2import tempfile3import os4import pandas as pd5from src.extract_text.google_document_api import GoogleDocumentAPI6from pdf2image import convert_from_path7from PIL import Image, ImageDraw, ImageFont8from src.utils.image_utils import ImageUtils9import base6410from io import BytesIO11from src.utils.barcode import Barcode12import anthropic13import json14 15def load_client_artwork_files():16 """Load all artwork PDF files from client directory"""17 base_path = "requirements_library/client-requirements"18 artwork_files = []19 20 if not os.path.exists(base_path):21 return artwork_files22 23 # Walk through all subdirectories24 for root, dirs, files in os.walk(base_path):25 for file in files:26 file_path = os.path.join(root, file)27 relative_path = os.path.relpath(file_path, base_path)28 29 if file.lower().endswith('.pdf'):30 artwork_files.append({31 'name': f"{relative_path}",32 'path': file_path,33 'type': 'artwork'34 })35 36 return artwork_files37 38def load_artwork_content(file_info):39 """Load artwork content as bytes"""40 try:41 with open(file_info['path'], 'rb') as f:42 return f.read()43 except Exception as e:44 st.error(f"Error loading artwork file {file_info['name']}: {str(e)}")45 return None46 47def extract_pdf_data(pdf_file, file_name):48 """Extract text, bounding boxes, images, and barcodes from PDF"""49 try:50 # Create a temporary file to process the PDF51 with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:52 pdf_file.seek(0)53 tmp_file.write(pdf_file.read())54 tmp_pdf_path = tmp_file.name55 56 # Extract text and bounding boxes using Google Document API57 google_document_api = GoogleDocumentAPI(credentials_path="src/extract_text/photon-services-f0d3ec1417d0.json")58 document = google_document_api.process_document(tmp_pdf_path)59 text_content = google_document_api.extract_text_with_markdown_table(document)60 bounding_boxes = google_document_api.extract_text_with_bounding_boxes(document)61 62 # Convert PDF to image63 try:64 images = convert_from_path(tmp_pdf_path)65 if not images:66 raise ValueError("No pages found in PDF")67 page_image = images[0] # Assuming single page for now68 except Exception as e:69 st.error(f"Error converting PDF to image: {str(e)}")70 # Create a placeholder image71 page_image = Image.new('RGB', (800, 600), color='white')72 draw = ImageDraw.Draw(page_image)73 draw.text((400, 300), "PDF conversion failed", fill='black', anchor='mm')74 75 # Process image for comparison: standardize size and optimize quality76 processed_image, quality, file_size = ImageUtils.process_image_for_comparison(77 page_image, 78 target_size=(1200, 1600), # Standard size for comparison79 max_size_bytes=1024 * 1024 # 1MB limit80 )81 82 # Convert processed image to base64 for API83 image_base64 = ImageUtils.image_to_base64_optimized(84 page_image,85 target_size=(1200, 1600),86 max_size_bytes=1024 * 102487 )88 89 # Scan for barcodes90 barcode = Barcode()91 barcode_results = barcode.scan_and_validate(page_image)92 93 # Clean up temporary file94 if os.path.exists(tmp_pdf_path):95 os.unlink(tmp_pdf_path)96 97 return {98 'text_content': text_content,99 'bounding_boxes': bounding_boxes,100 'image': processed_image, # Use the processed image101 'original_image': page_image, # Keep original for reference102 'image_base64': image_base64,103 'barcode_results': barcode_results,104 'file_name': file_name,105 'image_quality': quality,106 'image_size_bytes': file_size107 }108 109 except Exception as e:110 st.error(f"Error processing PDF {file_name}: {str(e)}")111 return None112 113def compare_artworks_with_claude(artwork1_data, artwork2_data, model="claude-sonnet-4-20250514"):114 """Compare two artworks using Claude API"""115 116 # Prepare the comparison prompt117 prompt = f"""118You are an expert packaging compliance analyzer. Compare these two artwork PDFs and provide a detailed analysis of their differences and similarities.119 120## Artwork 1: {artwork1_data['file_name']}121**Text Content:**122{artwork1_data['text_content']}123 124**Bounding Box Data:**125{json.dumps(artwork1_data['bounding_boxes'][:10], indent=2) if artwork1_data['bounding_boxes'] else "No text elements detected"}126 127**Barcode Data:**128{json.dumps(artwork1_data['barcode_results'], indent=2) if artwork1_data['barcode_results'] else "No barcodes detected"}129 130## Artwork 2: {artwork2_data['file_name']}131**Text Content:**132{artwork2_data['text_content']}133 134**Bounding Box Data:**135{json.dumps(artwork2_data['bounding_boxes'][:10], indent=2) if artwork2_data['bounding_boxes'] else "No text elements detected"}136 137**Barcode Data:**138{json.dumps(artwork2_data['barcode_results'], indent=2) if artwork2_data['barcode_results'] else "No barcodes detected"}139 140Please provide a comprehensive comparison analysis in the following JSON format:141 142{{143 "overall_similarity": 0.85,144 "comparison_summary": "Brief overview of the comparison results",145 "text_differences": [146 {{147 "category": "Missing Text",148 "artwork1_content": "Text found only in artwork 1",149 "artwork2_content": "Text found only in artwork 2",150 "significance": "HIGH/MEDIUM/LOW",151 "description": "Detailed explanation of the difference"152 }}153 ],154 "layout_differences": [155 {{156 "category": "Position Changes",157 "element": "Element that moved",158 "artwork1_position": "Description of position in artwork 1",159 "artwork2_position": "Description of position in artwork 2",160 "significance": "HIGH/MEDIUM/LOW",161 "description": "Impact of this change"162 }}163 ],164 "barcode_differences": [165 {{166 "category": "Barcode Changes",167 "artwork1_barcodes": "Description of barcodes in artwork 1",168 "artwork2_barcodes": "Description of barcodes in artwork 2",169 "significance": "HIGH/MEDIUM/LOW",170 "description": "Analysis of barcode differences"171 }}172 ],173 "visual_differences": [174 {{175 "category": "Visual Elements",176 "description": "Description of visual differences observed in the images",177 "significance": "HIGH/MEDIUM/LOW",178 "recommendation": "Suggested action or consideration"179 }}180 ],181 "compliance_impact": [182 {{183 "area": "Regulatory compliance area affected",184 "impact": "Description of potential compliance impact",185 "risk_level": "HIGH/MEDIUM/LOW",186 "recommendation": "Recommended action"187 }}188 ],189 "recommendations": [190 "List of actionable recommendations based on the comparison"191 ]192}}193 194Analyze both the textual content and visual elements. Pay special attention to:1951. Missing or changed text elements1962. Repositioned elements that might affect readability1973. Barcode differences that could impact functionality1984. Visual changes that might affect brand consistency or compliance1995. Any changes that could impact regulatory compliance200 201Provide specific, actionable insights that would be valuable for quality control and compliance verification.202"""203 204 try:205 # Initialize Anthropic client206 client = anthropic.Anthropic(api_key=os.getenv('CLAUDE_API_KEY'))207 208 # Create message with both images209 message = client.messages.create(210 model=model,211 max_tokens=4000,212 messages=[213 {214 "role": "user",215 "content": [216 {217 "type": "text",218 "text": prompt219 },220 {221 "type": "image",222 "source": {223 "type": "base64",224 "media_type": "image/png",225 "data": artwork1_data['image_base64']226 }227 },228 {229 "type": "image",230 "source": {231 "type": "base64",232 "media_type": "image/png",233 "data": artwork2_data['image_base64']234 }235 }236 ]237 }238 ]239 )240 241 # Parse the response242 response_text = ""243 for content_block in message.content:244 if hasattr(content_block, 'type') and content_block.type == 'text':245 response_text += content_block.text246 247 # Try to extract JSON from the response248 try:249 # Find JSON in the response250 start_idx = response_text.find('{')251 end_idx = response_text.rfind('}') + 1252 253 if start_idx != -1 and end_idx != -1:254 json_str = response_text[start_idx:end_idx]255 comparison_results = json.loads(json_str)256 else:257 # Fallback: create a basic structure with the raw response258 comparison_results = {259 "overall_similarity": 0.5,260 "comparison_summary": "Analysis completed but JSON parsing failed",261 "raw_response": response_text,262 "text_differences": [],263 "layout_differences": [],264 "barcode_differences": [],265 "visual_differences": [],266 "compliance_impact": [],267 "recommendations": ["Review the raw analysis output for detailed insights"]268 }269 except json.JSONDecodeError:270 # Fallback for JSON parsing errors271 comparison_results = {272 "overall_similarity": 0.5,273 "comparison_summary": "Analysis completed but structured parsing failed",274 "raw_response": response_text,275 "text_differences": [],276 "layout_differences": [],277 "barcode_differences": [],278 "visual_differences": [],279 "compliance_impact": [],280 "recommendations": ["Review the raw analysis output for detailed insights"]281 }282 283 return comparison_results284 285 except Exception as e:286 st.error(f"Error calling Claude API: {str(e)}")287 return None288 289def display_comparison_results(results, artwork1_data, artwork2_data):290 """Display the comparison results in a structured format"""291 292 if not results:293 st.error("No comparison results to display")294 return295 296 # Overall Summary297 st.markdown("## ๐ Comparison Summary")298 299 col1, col2, col3 = st.columns(3)300 with col1:301 similarity = results.get('overall_similarity', 0.5)302 st.metric("Overall Similarity", f"{similarity:.1%}")303 304 with col2:305 total_differences = (306 len(results.get('text_differences', [])) +307 len(results.get('layout_differences', [])) +308 len(results.get('barcode_differences', [])) +309 len(results.get('visual_differences', []))310 )311 st.metric("Total Differences", total_differences)312 313 with col3:314 compliance_impacts = len(results.get('compliance_impact', []))315 st.metric("Compliance Impacts", compliance_impacts)316 317 # Summary description318 if 'comparison_summary' in results:319 st.markdown(f"**Summary:** {results['comparison_summary']}")320 321 # Create tabs for different types of differences322 tabs = st.tabs(["๐ Text Differences", "๐ Layout Changes", "๐ฑ Barcode Changes", "๐จ Visual Differences", "โ๏ธ Compliance Impact", "๐ก Recommendations"])323 324 with tabs[0]: # Text Differences325 st.markdown("### Text Content Differences")326 text_diffs = results.get('text_differences', [])327 if text_diffs:328 for i, diff in enumerate(text_diffs):329 significance_color = {"HIGH": "๐ด", "MEDIUM": "๐ก", "LOW": "๐ข"}.get(diff.get('significance', 'MEDIUM'), "๐ก")330 331 with st.expander(f"{significance_color} {diff.get('category', 'Text Difference')} - {diff.get('significance', 'MEDIUM')} Impact"):332 col1, col2 = st.columns(2)333 with col1:334 st.markdown(f"**{artwork1_data['file_name']}:**")335 st.text(diff.get('artwork1_content', 'N/A'))336 with col2:337 st.markdown(f"**{artwork2_data['file_name']}:**")338 st.text(diff.get('artwork2_content', 'N/A'))339 340 st.markdown(f"**Description:** {diff.get('description', 'No description available')}")341 else:342 st.info("No significant text differences found")343 344 with tabs[1]: # Layout Changes345 st.markdown("### Layout and Positioning Changes")346 layout_diffs = results.get('layout_differences', [])347 if layout_diffs:348 for diff in layout_diffs:349 significance_color = {"HIGH": "๐ด", "MEDIUM": "๐ก", "LOW": "๐ข"}.get(diff.get('significance', 'MEDIUM'), "๐ก")350 351 with st.expander(f"{significance_color} {diff.get('category', 'Layout Change')} - {diff.get('significance', 'MEDIUM')} Impact"):352 st.markdown(f"**Element:** {diff.get('element', 'Unknown element')}")353 354 col1, col2 = st.columns(2)355 with col1:356 st.markdown(f"**Position in {artwork1_data['file_name']}:**")357 st.text(diff.get('artwork1_position', 'N/A'))358 with col2:359 st.markdown(f"**Position in {artwork2_data['file_name']}:**")360 st.text(diff.get('artwork2_position', 'N/A'))361 362 st.markdown(f"**Impact:** {diff.get('description', 'No description available')}")363 else:364 st.info("No significant layout differences found")365 366 with tabs[2]: # Barcode Changes367 st.markdown("### Barcode Differences")368 barcode_diffs = results.get('barcode_differences', [])369 if barcode_diffs:370 for diff in barcode_diffs:371 significance_color = {"HIGH": "๐ด", "MEDIUM": "๐ก", "LOW": "๐ข"}.get(diff.get('significance', 'MEDIUM'), "๐ก")372 373 with st.expander(f"{significance_color} {diff.get('category', 'Barcode Change')} - {diff.get('significance', 'MEDIUM')} Impact"):374 col1, col2 = st.columns(2)375 with col1:376 st.markdown(f"**{artwork1_data['file_name']} Barcodes:**")377 st.text(diff.get('artwork1_barcodes', 'N/A'))378 with col2:379 st.markdown(f"**{artwork2_data['file_name']} Barcodes:**")380 st.text(diff.get('artwork2_barcodes', 'N/A'))381 382 st.markdown(f"**Analysis:** {diff.get('description', 'No description available')}")383 else:384 st.info("No significant barcode differences found")385 386 with tabs[3]: # Visual Differences387 st.markdown("### Visual and Design Differences")388 visual_diffs = results.get('visual_differences', [])389 if visual_diffs:390 for diff in visual_diffs:391 significance_color = {"HIGH": "๐ด", "MEDIUM": "๐ก", "LOW": "๐ข"}.get(diff.get('significance', 'MEDIUM'), "๐ก")392 393 with st.expander(f"{significance_color} {diff.get('category', 'Visual Change')} - {diff.get('significance', 'MEDIUM')} Impact"):394 st.markdown(f"**Description:** {diff.get('description', 'No description available')}")395 if 'recommendation' in diff:396 st.markdown(f"**Recommendation:** {diff['recommendation']}")397 else:398 st.info("No significant visual differences found")399 400 with tabs[4]: # Compliance Impact401 st.markdown("### Compliance and Regulatory Impact")402 compliance_impacts = results.get('compliance_impact', [])403 if compliance_impacts:404 for impact in compliance_impacts:405 risk_color = {"HIGH": "๐ด", "MEDIUM": "๐ก", "LOW": "๐ข"}.get(impact.get('risk_level', 'MEDIUM'), "๐ก")406 407 with st.expander(f"{risk_color} {impact.get('area', 'Compliance Area')} - {impact.get('risk_level', 'MEDIUM')} Risk"):408 st.markdown(f"**Impact:** {impact.get('impact', 'No description available')}")409 st.markdown(f"**Recommendation:** {impact.get('recommendation', 'No recommendation provided')}")410 else:411 st.success("No compliance impacts identified")412 413 with tabs[5]: # Recommendations414 st.markdown("### Action Items and Recommendations")415 recommendations = results.get('recommendations', [])416 if recommendations:417 for i, rec in enumerate(recommendations, 1):418 st.markdown(f"{i}. {rec}")419 else:420 st.info("No specific recommendations provided")421 422 # Raw response section (collapsible)423 if 'raw_response' in results:424 with st.expander("๐ Raw Analysis Output"):425 st.text(results['raw_response'])426 427def display_side_by_side_images(artwork1_data, artwork2_data):428 """Display the two artwork images side by side"""429 st.markdown("## ๐ผ๏ธ Side-by-Side Comparison")430 431 col1, col2 = st.columns(2)432 433 with col1:434 st.markdown(f"### {artwork1_data['file_name']}")435 st.image(ImageUtils.crop_image(artwork1_data['image']), caption=artwork1_data['file_name'], use_container_width=True)436 437 # Display image processing info438 if 'image_quality' in artwork1_data and 'image_size_bytes' in artwork1_data:439 quality = artwork1_data['image_quality']440 size_mb = artwork1_data['image_size_bytes'] / (1024 * 1024)441 st.info(f"๐ Image Quality: {quality}% | Size: {size_mb:.2f}MB")442 443 # Display extracted data summary444 with st.expander("๐ Extracted Data Summary"):445 text_elements = len(artwork1_data['bounding_boxes']) if artwork1_data['bounding_boxes'] else 0446 barcodes = len(artwork1_data['barcode_results']) if artwork1_data['barcode_results'] else 0447 st.metric("Text Elements", text_elements)448 st.metric("Barcodes", barcodes)449 450 with col2:451 st.markdown(f"### {artwork2_data['file_name']}")452 st.image(ImageUtils.crop_image(artwork2_data['image']), caption=artwork2_data['file_name'], use_container_width=True)453 454 # Display image processing info455 if 'image_quality' in artwork2_data and 'image_size_bytes' in artwork2_data:456 quality = artwork2_data['image_quality']457 size_mb = artwork2_data['image_size_bytes'] / (1024 * 1024)458 st.info(f"๐ Image Quality: {quality}% | Size: {size_mb:.2f}MB")459 460 # Display extracted data summary461 with st.expander("๐ Extracted Data Summary"):462 text_elements = len(artwork2_data['bounding_boxes']) if artwork2_data['bounding_boxes'] else 0463 barcodes = len(artwork2_data['barcode_results']) if artwork2_data['barcode_results'] else 0464 st.metric("Text Elements", text_elements)465 st.metric("Barcodes", barcodes)466 467def main():468 st.set_page_config(layout="wide", page_title="Artwork Comparison Tool")469 470 # Load client artwork files471 client_artwork_files = load_client_artwork_files()472 473 # Initialize session state474 if "artwork1_data" not in st.session_state:475 st.session_state.artwork1_data = None476 if "artwork2_data" not in st.session_state:477 st.session_state.artwork2_data = None478 if "comparison_results" not in st.session_state:479 st.session_state.comparison_results = None480 481 st.title("๐จ Artwork Comparison Tool")482 st.write("Compare two packaging artwork PDFs to identify differences in text, layout, barcodes, and visual elements.")483 484 # File selection section485 st.markdown("## ๐ Select Artworks to Compare")486 487 col1, col2 = st.columns(2)488 489 with col1:490 st.markdown("### ๐จ Artwork 1")491 492 # Create tabs for client files vs upload493 art1_tab1, art1_tab2 = st.tabs(["๐ Client Files", "๐ค Upload New"])494 495 with art1_tab1:496 if client_artwork_files:497 art1_options = ["Select artwork 1..."] + [f["name"] for f in client_artwork_files]498 selected_art1_file = st.selectbox("Choose artwork 1:", art1_options, key="art1_select")499 500 if selected_art1_file != "Select artwork 1...":501 # Find and load the selected file502 for file_info in client_artwork_files:503 if file_info["name"] == selected_art1_file:504 file_content = load_artwork_content(file_info)505 if file_content:506 import io507 temp_file = io.BytesIO(file_content)508 temp_file.name = file_info["name"]509 510 # Extract data from the artwork511 with st.spinner("Processing artwork 1..."):512 st.session_state.artwork1_data = extract_pdf_data(temp_file, file_info["name"])513 514 if st.session_state.artwork1_data:515 st.success(f"โ
Loaded artwork 1: {selected_art1_file}")516 break517 else:518 st.info("No client artwork files found")519 520 with art1_tab2:521 artwork1_file = st.file_uploader("Upload Artwork 1 (PDF)", type=["pdf"], key="art1_upload")522 523 if artwork1_file:524 with st.spinner("Processing artwork 1..."):525 st.session_state.artwork1_data = extract_pdf_data(artwork1_file, artwork1_file.name)526 527 if st.session_state.artwork1_data:528 st.success(f"โ
Uploaded artwork 1: {artwork1_file.name}")529 530 with col2:531 st.markdown("### ๐จ Artwork 2")532 533 # Create tabs for client files vs upload534 art2_tab1, art2_tab2 = st.tabs(["๐ Client Files", "๐ค Upload New"])535 536 with art2_tab1:537 if client_artwork_files:538 art2_options = ["Select artwork 2..."] + [f["name"] for f in client_artwork_files]539 selected_art2_file = st.selectbox("Choose artwork 2:", art2_options, key="art2_select")540 541 if selected_art2_file != "Select artwork 2...":542 # Find and load the selected file543 for file_info in client_artwork_files:544 if file_info["name"] == selected_art2_file:545 file_content = load_artwork_content(file_info)546 if file_content:547 import io548 temp_file = io.BytesIO(file_content)549 temp_file.name = file_info["name"]550 551 # Extract data from the artwork552 with st.spinner("Processing artwork 2..."):553 st.session_state.artwork2_data = extract_pdf_data(temp_file, file_info["name"])554 555 if st.session_state.artwork2_data:556 st.success(f"โ
Loaded artwork 2: {selected_art2_file}")557 break558 else:559 st.info("No client artwork files found")560 561 with art2_tab2:562 artwork2_file = st.file_uploader("Upload Artwork 2 (PDF)", type=["pdf"], key="art2_upload")563 564 if artwork2_file:565 with st.spinner("Processing artwork 2..."):566 st.session_state.artwork2_data = extract_pdf_data(artwork2_file, artwork2_file.name)567 568 if st.session_state.artwork2_data:569 st.success(f"โ
Uploaded artwork 2: {artwork2_file.name}")570 571 # Display images side by side if both are loaded572 if st.session_state.artwork1_data and st.session_state.artwork2_data:573 display_side_by_side_images(st.session_state.artwork1_data, st.session_state.artwork2_data)574 575 # Model selection576 model_option = "claude-sonnet-4-20250514"577 578 # Comparison button579 if st.button("๐ Compare Artworks", type="primary"):580 if st.session_state.artwork1_data and st.session_state.artwork2_data:581 with st.spinner("Analyzing artworks with Claude..."):582 st.session_state.comparison_results = compare_artworks_with_claude(583 st.session_state.artwork1_data,584 st.session_state.artwork2_data,585 model=model_option586 )587 588 if st.session_state.comparison_results:589 st.success("โ
Comparison analysis complete!")590 else:591 st.error("โ Comparison analysis failed")592 else:593 st.warning("โ ๏ธ Please select or upload both artworks before comparing")594 595 # Display comparison results596 if st.session_state.comparison_results:597 display_comparison_results(598 st.session_state.comparison_results,599 st.session_state.artwork1_data,600 st.session_state.artwork2_data601 )602 603 # Add helpful information604 st.markdown("---")605 st.markdown("""606 ### ๐ ๏ธ How It Works607 1. **Extract Content**: The tool extracts text, bounding boxes, images, and barcodes from both PDFs608 2. **AI Analysis**: Claude analyzes the extracted data and visual elements to identify differences609 3. **Structured Results**: Differences are categorized by type (text, layout, barcode, visual) and significance610 4. **Compliance Assessment**: Potential compliance impacts are identified with risk levels and recommendations611 612 ### ๐ฏ Use Cases613 - **Quality Control**: Verify artwork changes between versions614 - **Brand Consistency**: Ensure visual elements remain consistent615 - **Compliance Review**: Identify changes that might affect regulatory compliance616 - **Change Documentation**: Track and document artwork modifications617 """)618 619if __name__ == "__main__":620 main()