import gradio as gr import os import json import numpy as np import requests from PIL import Image import uuid import glob import time # Create directories for examples and temporary files os.makedirs("examples", exist_ok=True) os.makedirs("temp", exist_ok=True) os.makedirs("static", exist_ok=True) # 'static' directory is not used by Ketcher from CDN os.environ["GRADIO_TEMP_DIR"] = os.path.abspath("temp") # Sample example images EXAMPLE_IMAGES = [ "examples/9213587.png", "examples/4056723.png", "examples/7632980.png", "examples/1890451.png", "examples/3401765.png", "examples/8572109.png", ] url = os.getenv("URL") headers = { "Authorization": os.getenv("KEY") } # Ensure example images exist, or create placeholders for img_path_rel in EXAMPLE_IMAGES: # Make paths absolute for robust checking and placeholder creation img_path_abs = os.path.abspath(img_path_rel) if not os.path.exists(img_path_abs): print(f"Warning: Example image {img_path_abs} not found. Creating a placeholder.") try: # Ensure the 'examples' directory exists before saving placeholder os.makedirs(os.path.dirname(img_path_abs), exist_ok=True) placeholder_img = Image.new('RGB', (200, 200), color='lightgrey') # Increased size # Add text to placeholder from PIL import ImageDraw draw = ImageDraw.Draw(placeholder_img) draw.text((10, 10), os.path.basename(img_path_rel) + "\n(Placeholder)", fill=(0, 0, 0)) placeholder_img.save(img_path_abs) except Exception as e: print(f"Could not create placeholder image {img_path_abs}: {e}") # HTML for Ketcher container - Simplified, static Ketcher viewer_html = """

loading SMILES editor

""" # HTML for sidebar with icons - without JavaScript functions sidebar_html = """ """ # JavaScript to initialize sidebar functionality init_sidebar_js = """ function() { // Global variables for sidebar window.currentContent = ""; window.currentContentType = ""; // Global variables for multiple molecules window.currentMolecules = []; window.currentActiveTab = 0; // Define sidebar functions in global scope window.showSidebar = function(content, contentType) { const sidebar = document.getElementById('sidebar-container'); const contentArea = document.getElementById('content-area'); const sidebarTitle = document.getElementById('sidebar-title'); // Update content and type window.currentContent = content; window.currentContentType = contentType; // Set title based on content type sidebarTitle.textContent = contentType === 'smiles' ? 'SMILES' : 'Molfile'; // Format and display content let formattedContent = content; if (contentType === 'molfile') { // For Molfile, preserve line breaks and formatting formattedContent = '
' + content.replace(//g, '>') + '
'; } else { // For SMILES, word-wrap but don't add pre tags formattedContent = '
' + content.replace(//g, '>') + '
'; } contentArea.innerHTML = formattedContent; // Show sidebar with animation sidebar.style.display = 'block'; setTimeout(() => { sidebar.style.right = '0'; }, 10); }; window.hideSidebar = function() { const sidebar = document.getElementById('sidebar-container'); sidebar.style.right = '-350px'; setTimeout(() => { sidebar.style.display = 'none'; }, 300); }; // Function to switch molecules in Ketcher window.switchToMolecule = function(moleculeIndex) { if (!window.currentMolecules || moleculeIndex >= window.currentMolecules.length) { console.error('Invalid molecule index or no molecules available'); return; } const molecule = window.currentMolecules[moleculeIndex]; if (window.ketcher && typeof window.ketcher.setMolecule === 'function') { try { window.ketcher.setMolecule(molecule.molfile); window.currentActiveTab = moleculeIndex; // console.log(`Switched to molecule ${moleculeIndex + 1}: ${molecule.smiles}`); // Update active tab styling const tabButtons = document.querySelectorAll('.mol-tab-btn'); tabButtons.forEach((btn, index) => { if (index === moleculeIndex) { btn.classList.add('active'); btn.style.backgroundColor = '#003366'; btn.style.color = 'white'; } else { btn.classList.remove('active'); btn.style.backgroundColor = '#f5f5f5'; btn.style.color = '#333'; } }); } catch (e) { console.error('Error setting molecule in Ketcher:', e); } } else { console.error('Ketcher not ready or setMolecule function not available'); } }; // Improved copy to clipboard function with fallback window.copyToClipboard = function() { if (!window.currentContent) return; const copyBtn = document.getElementById('copy-btn-s'); const showSuccess = function() { copyBtn.innerHTML = ''; setTimeout(() => { copyBtn.innerHTML = ''; }, 2000); }; // Try using the Clipboard API first if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(window.currentContent) .then(showSuccess) .catch(function(err) { console.warn('Clipboard API failed:', err); // Fall back to the textarea method fallbackCopyTextToClipboard(); }); } else { // Fallback for browsers that don't support the Clipboard API fallbackCopyTextToClipboard(); } // Fallback copy method using textarea and document.execCommand function fallbackCopyTextToClipboard() { try { const textarea = document.getElementById('copy-textarea'); textarea.value = window.currentContent; textarea.focus(); textarea.select(); const successful = document.execCommand('copy'); if (successful) { console.log('Fallback copy successful'); showSuccess(); } else { console.warn('Fallback copy command was unsuccessful'); alert('Copy failed. Please try selecting and copying the text manually.'); } } catch (err) { console.error('Fallback copy failed:', err); alert('Copy failed. Please try selecting and copying the text manually.'); } } }; window.downloadContent = function() { if (window.currentContent) { const extension = window.currentContentType === 'smiles' ? '.smi' : '.mol'; const filename = 'molecule' + extension; const blob = new Blob([window.currentContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } }; // Set up event listeners document.addEventListener('DOMContentLoaded', function() { // Close sidebar button const closeBtn = document.getElementById('close-sidebar'); if (closeBtn) { closeBtn.addEventListener('click', window.hideSidebar); } // Copy button const copyBtn = document.getElementById('copy-btn-s'); if (copyBtn) { copyBtn.addEventListener('click', window.copyToClipboard); } // Download button const downloadBtn = document.getElementById('download-btn'); if (downloadBtn) { downloadBtn.addEventListener('click', window.downloadContent); } }); // Initialize event listeners immediately if DOM is already loaded if (document.readyState === 'complete' || document.readyState === 'interactive') { const closeBtn = document.getElementById('close-sidebar'); if (closeBtn) { closeBtn.addEventListener('click', window.hideSidebar); } const copyBtn = document.getElementById('copy-btn-s'); if (copyBtn) { copyBtn.addEventListener('click', window.copyToClipboard); } const downloadBtn = document.getElementById('download-btn'); if (downloadBtn) { downloadBtn.addEventListener('click', window.downloadContent); } } console.log("Sidebar functionality initialized"); } """ # JavaScript to load Ketcher (simplified, static version) load_ketcher_js = """ async () => { var loadingDiv = document.getElementById('loading-ketcher'); var ketcherRootDiv = document.getElementById('root'); loadingDiv.style.display = 'flex'; ketcherRootDiv.style.display = 'none'; let cssUrl = "https://huggingface.co/datasets/Sunin/ketcher-3.2.0/raw/main/ketcher_standalone_3.2.0/static/css/main.aafe9c71.css"; fetch(cssUrl) .then(res => res.text()) .then(text => { const style = document.createElement('style'); style.textContent = text; document.head.appendChild(style); }).catch(err => console.error("Failed to load Ketcher CSS:", err)); let jsUrl = "https://huggingface.co/datasets/Sunin/ketcher-3.2.0/resolve/main/ketcher_standalone_3.2.0/static/js/main.7edc2752.js"; fetch(jsUrl) .then(res => res.text()) .then(text => { const script = document.createElement('script'); script.src = URL.createObjectURL(new Blob([text], { type: 'application/javascript' })); script.onload = () => { function checkKetcherReady() { if (window.ketcher && typeof window.ketcher.setMolecule === 'function') { loadingDiv.style.display = 'none'; ketcherRootDiv.style.display = 'block'; console.log("Ketcher loaded and initialized."); } else { setTimeout(checkKetcherReady, 100); } } checkKetcherReady(); }; script.onerror = () => { console.error("Failed to load Ketcher JS."); loadingDiv.innerHTML = "

Failed to load SMILES editor.

"; }; document.head.appendChild(script); }).catch(err => { console.error("Failed to fetch Ketcher JS:", err); loadingDiv.innerHTML = "

Failed to fetch SMILES editor assets.

"; }); } """ # NEW: JavaScript to initialize clipboard paste functionality init_clipboard_paste_js = """ function() { console.log('Initializing clipboard paste functionality'); // Function to handle pasted image window.handlePastedImage = function(file) { console.log('Processing pasted image file:', file); // Create a FileReader to read the image const reader = new FileReader(); reader.onload = function(e) { const img = new Image(); img.onload = function() { // Create a canvas to convert the image to the format Gradio expects const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; // Set crossOrigin to avoid CORS issues img.crossOrigin = 'anonymous'; // Draw the image on canvas ctx.drawImage(img, 0, 0); // Convert canvas to blob canvas.toBlob(function(blob) { // Create a File object from the blob const pastedFile = new File([blob], 'pasted-image.png', { type: 'image/png' }); // Find the Gradio image input element const imageInput = document.querySelector('#molecule-image input[type="file"]'); if (imageInput) { // Create a new FileList containing our pasted file const dt = new DataTransfer(); dt.items.add(pastedFile); imageInput.files = dt.files; // Trigger the change event to notify Gradio const changeEvent = new Event('change', { bubbles: true }); imageInput.dispatchEvent(changeEvent); console.log('Pasted image successfully uploaded to Gradio'); } else { console.error('Could not find Gradio image input element'); } }, 'image/png'); }; img.src = e.target.result; }; reader.readAsDataURL(file); }; // Add paste event listener to the document document.addEventListener('paste', function(event) { console.log('Paste event detected'); const items = (event.clipboardData || event.originalEvent.clipboardData).items; if (items.length > 0) { for (let i = 0; i < items.length; i++) { const item = items[i]; // Check if the item is an image if (item.type.indexOf('image') === 0) { console.log('Image found in clipboard'); const file = item.getAsFile(); if (file) { window.handlePastedImage(file); event.preventDefault(); // Prevent default paste behavior break; // Only process the first image } } } } }); console.log('Clipboard paste functionality initialized successfully'); } """ # JavaScript to update molecules and render first one update_molecules_js = """ async (moleculesJson) => { function updateMolecules() { if (typeof window.ketcher !== 'undefined' && window.ketcher && typeof window.ketcher.setMolecule === 'function') { try { if (moleculesJson && moleculesJson.trim() !== "") { const molecules = JSON.parse(moleculesJson); window.currentMolecules = molecules; // Render the first molecule immediately if (molecules.length > 0 && molecules[0].molfile) { window.ketcher.setMolecule(molecules[0].molfile); window.currentActiveTab = 0; // console.log(`Loaded first molecule: ${molecules[0].smiles}`); } } else { // Clear Ketcher if no molecules window.ketcher.setMolecule(''); window.currentMolecules = []; console.log('Cleared Ketcher - no molecules provided'); } } catch (e) { console.error('Error updating molecules:', e); window.currentMolecules = []; } } else { console.log('Ketcher not ready yet, retrying...'); setTimeout(updateMolecules, 250); } } updateMolecules(); } """ # Modified JavaScript for getting SMILES from Ketcher, processing with sma2smi, and showing in sidebar get_smiles_js = """ async () => { try { // Get SMILES from Ketcher const ketcher_smiles = await ketcher.getSmiles(); // console.log("SMILES from Ketcher:", ketcher_smiles); if (ketcher_smiles && ketcher_smiles.trim()) { // Send the SMILES to Python for processing with sma2smi // This will be handled by the Python function return ketcher_smiles; } else { alert("No molecule structure found. Please draw a structure first."); return ""; } } catch (error) { console.error("Error getting SMILES:", error); alert("Error getting SMILES: " + error.message); return ""; } } """ # JavaScript for getting Molfile from Ketcher and showing in sidebar get_molfile_js = """ async () => { try { const molfile = await ketcher.getMolfile(); // console.log("Molfile from Ketcher (first 100 chars):", molfile ? molfile.substring(0, 100) + "..." : "None"); if (molfile && molfile.trim()) { // Show the sidebar with Molfile content window.showSidebar(molfile, 'molfile'); return molfile; } else { alert("No molecule structure found. Please draw a structure first."); return ""; } } catch (error) { console.error("Error getting Molfile:", error); alert("Error getting Molfile: " + error.message); return ""; } } """ # JavaScript to hide running indicator (add debug logging) hide_running_indicator_js = """ function() { console.log('Attempting to hide running indicator'); const indicator = document.getElementById('running-indicator'); if (indicator) { indicator.style.display = 'none'; console.log('Indicator hidden successfully'); } else { console.warn('Running indicator element not found'); } } """ # Main application function def create_app(): css = """ #app { background: #F5F5F5; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); padding: 20px; } #header { color: #333; text-align: center; margin-bottom: 20px; } #main-container { display: flex; flex-direction: row; gap: 20px; background-color: white; border-radius: 8px; padding: 15px; } #image-container, #ketcher-column { flex: 1; min-height: 600px; border: 1px dashed #ccc; border-radius: 8px; padding:10px; display: flex; flex-direction: column; justify-content: space-between;} #ketcher-display-wrapper { width: 100%; height: 580px; /* Fixed height for ketcher root */ } #button-container { display: flex; gap: 10px; margin-top: 10px; justify-content: center;} .btn { background-color: #003366; /* 深化按钮背景色,增强对比度 */ color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; transition: background-color 0.3s; font-weight: bold; /* 加粗按钮文字 */ font-size: 1.05em; /* 增大按钮文字 */ } .btn:hover { background-color: #002850; } .btn:disabled { background-color: #cccccc; cursor: not-allowed; } /* Molecule tab buttons */ .mol-tab-btn { background-color: #f5f5f5; color: #333; border: 1px solid #ddd; border-radius: 4px; padding: 6px 16px; cursor: pointer; transition: all 0.3s; font-weight: bold; font-size: 1em; margin-right: 5px; } .mol-tab-btn:hover { background-color: #e5e5e5; } .mol-tab-btn.active { background-color: #003366; color: white; } #smiles-btn:hover::after { content: "Get SMILES"; /* 显示的tooltip文本 */ position: absolute; bottom: 125%; /* 定位在按钮上方 */ left: 10%; transform: translateX(-50%); /* 居中对齐 */ background-color: #333; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.9em; white-space: nowrap; z-index: 1; } #molfile-btn:hover::after { content: "Get Molfile"; /* 显示的tooltip文本 */ position: absolute; bottom: 125%; /* 定位在按钮上方 */ left: 90%; transform: translateX(-50%); /* 居中对齐 */ background-color: #333; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.9em; white-space: nowrap; z-index: 1; } #examples-section { margin-top: 20px; background-color: white; border-radius: 8px; padding: 10px; } #examples-section h3 { margin: 0 0 10px 0; font-size: 1.2em; text-align: center;} .gr-gallery { width: 100% !important; padding: 0 !important; margin: 0 !important; display: flex !important; justify-content: center !important; flex-wrap: wrap !important; gap: 10px !important; } .gr-gallery img { width: 133px !important; height: 133px !important; object-fit: contain !important; cursor: pointer !important; transition: transform 0.2s !important; border: 1px solid #eee !important; border-radius: 4px !important; } .gr-gallery img:hover { transform: scale(1.05) !important; } #molecule-image { min-height: 600px !important; display:flex; align-items:center; justify-content:center; } #molecule-image div[data-testid="image"] { width:100% !important; height:100% !important;} /* Ensure inner div also takes full size */ #molecule-image img { object-fit: contain !important; max-width: 100%; max-height: 100%;} /* Ensure image scales within its container */ #root { width: 100%; height: 100%; } /* NEW: Enhanced styling for image upload area with hints */ #molecule-image .image-container { position: relative; width: 100%; height: 100%; min-height: 500px; border: 2px dashed #ccc; border-radius: 8px; display: flex; align-items: center; justify-content: center; background-color: #fafafa; transition: border-color 0.3s, background-color 0.3s; } #molecule-image .image-container:hover { border-color: #003366; background-color: #f0f8ff; } #molecule-image .upload-hint { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #666; font-size: 16px; pointer-events: none; z-index: 1; } #molecule-image .upload-hint .hint-icon { font-size: 48px; margin-bottom: 16px; color: #003366; } #molecule-image .upload-hint .hint-text { line-height: 1.5; } #molecule-image .upload-hint .hint-shortcut { font-weight: bold; color: #003366; margin-top: 8px; font-size: 14px; } /* Hide hint when image is present */ #molecule-image:has(img) .upload-hint { display: none; } /* 修改1: 移除图片展示界面和start button之间的空白 */ #image-container { padding-bottom: 0; } #image-container #button-container { margin-top: 0; } #processed-smiles-container { display: flex; width: 100%; /* 使用100%宽度与ketcher界面对齐 */ margin-top: 5px; gap: 2px; /* 进一步减小间距到2px */ align-items: stretch; /* 确保两个元素高度一致 */ box-sizing: border-box; /* 确保宽度计算包含内边距和边框 */ } #processed-smiles-box { width: 90%; /* 占据更多空间 */ min-width: 90%; /* 确保至少占据90%的空间 */ max-width: 90%; /* 限制最大宽度为90% */ box-sizing: border-box; /* 确保内边距和边框包含在宽度内 */ border: 1px solid #ddd; border-radius: 4px; padding: 8px 4px; font-family: monospace; min-height: 40px; background-color: #f9f9f9; overflow-x: auto; white-space: nowrap; margin: 0; /* 移除可能的额外间距 */ } #copy-btn { width: 10%; /* 占据更少空间 */ min-width: 10%; /* 确保按钮可见 */ max-width: 10%; /* 限制最大宽度为10% */ box-sizing: border-box; /* 确保内边距和边框包含在宽度内 */ border-radius: 4px; border: none; /* 移除边框 */ background-color: #003366; /* 深化按钮背景色,增强对比度 */ color: white; padding: 0; cursor: pointer; transition: background-color 0.3s; display: flex; align-items: center; justify-content: center; font-size: 1.3em; /* 优化图标大小以提高清晰度 */ margin: 0; /* 移除可能的额外间距 */ position: relative; /* 启用相对定位以支持tooltip */ } #copy-btn:hover { background-color: #002850; } #copy-btn:hover::after { content: "Copy to clipboard"; /* 显示的tooltip文本 */ position: absolute; bottom: 125%; /* 定位在按钮上方 */ left: 50%; transform: translateX(-50%); /* 居中对齐 */ background-color: #333; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.9em; white-space: nowrap; z-index: 1; } #copy-btn:disabled { background-color: #cccccc; cursor: not-allowed; } #copy-btn:disabled::after { display: none; /* 禁用时隐藏tooltip */ } /* Sidebar styles */ #sidebar-container { position: fixed; top: 0; right: -350px; /* Or your logic for showing/hiding */ width: 350px; height: 100%; background-color: white; box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); z-index: 1000; transition: right 0.3s ease; /* overflow: auto; /* Consider changing to overflow: hidden if content-area is the sole scroller */ padding: 20px; /* Add these flex properties */ display: flex; flex-direction: column; } .sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; /* This will create space below the header, above the actions */ border-bottom: 1px solid #eee; padding-bottom: 10px; /* flex-shrink: 0; /* This is default, header won't shrink */ } /* Styles for the MOVED actions container */ .sidebar-actions { display: flex; justify-content: flex-end; /* Keeps buttons to the right */ gap: 10px; padding: 10px 0; /* Existing padding, provides space above/below buttons */ /* Add margin-bottom for spacing between buttons and content-area */ margin-bottom: 15px; /* Adjust as needed */ /* flex-shrink: 0; /* This is default, actions won't shrink */ } .sidebar-content { display: flex; flex-direction: column; /* REMOVE: height: calc(100% - 60px); */ /* ADD these flex properties to make it take remaining space */ flex: 1; min-height: 0; /* Important for flex children that need to shrink and scroll internally */ /* overflow: hidden; /* If you want #content-area to be the only scroller within this */ } #content-area { flex: 1; /* Will take available space in .sidebar-content */ overflow: auto; /* Allows scrolling for the content itself */ background-color: #f9f9f9; border: 1px solid #eee; border-radius: 4px; padding: 10px; /* REMOVE: margin-bottom: 10px; /* No longer needed as actions are not below */ font-family: monospace; } .icon-button { background: none; border: none; cursor: pointer; padding: 5px; border-radius: 4px; color: #004080; transition: background-color 0.2s; } .icon-button:hover { background-color: #f0f0f0; } pre { margin: 0; white-space: pre-wrap; word-break: break-all; } /* Molecule tabs container */ #molecule-tabs-container { margin-bottom: 0px; padding: 0px 0; border-bottom: 1px solid #eee; } """ with gr.Blocks(css=css, title="CLIP-OCSR") as app: uploaded_image_state = gr.State(None) current_smiles_state = gr.State(None) # Will store the SMILES from OCSR or Ketcher current_molfile_path_state = gr.State(None) session_id_state = gr.State(value=lambda: str(uuid.uuid4())) # NEW: State to store multiple molecules multiple_molecules_state = gr.State([]) # Hidden components for JavaScript communication molecules_json_trigger = gr.Textbox(visible=False, label="Molecules JSON for JS") smiles_data = gr.Textbox(visible=False) molfile_data = gr.Textbox(visible=False) # hide_indicator_html_trigger = gr.HTML() with gr.Column(elem_id="app"): gr.HTML(sidebar_html) with gr.Row(elem_id="header"): gr.HTML("

CLIP-OCSR: CLIP-empowered Optical Chemical Structure Recognition

Online tool for chemical structure extraction

") with gr.Row(elem_id="main-container"): with gr.Column(elem_id="image-container"): image_display = gr.Image( type="numpy", label="Molecule Image", elem_id="molecule-image", height=500, interactive=True, show_label=False, show_download_button=True, show_share_button=False, placeholder="Click to upload, drag & drop, or press Ctrl+V to paste an image" ) gr.HTML("""
""") with gr.Row(elem_id="button-container"): start_btn = gr.Button("Start Recognition", elem_id="start-btn", variant="primary", elem_classes="btn") with gr.Column(elem_id="ketcher-column"): # NEW: Molecule tabs area molecule_tabs_area = gr.HTML(value="", elem_id="molecule-tabs-container") with gr.Column(elem_id="ketcher-display-wrapper"): ketcher_html_display = gr.HTML(value=viewer_html) with gr.Row(elem_id="button-container"): smiles_btn = gr.Button("SMILES", elem_id="smiles-btn", elem_classes="btn") molfile_btn = gr.Button("MOLfile", elem_id="molfile-btn", elem_classes="btn") with gr.Row(elem_id="processed-smiles-container"): processed_smiles_output = gr.HTML( value="
" ) copy_btn = gr.Button( "📋", elem_id="copy-btn", ) with gr.Column(elem_id="examples-section"): gr.HTML("

Examples

") examples_gallery = gr.Gallery( value=[(img_path, os.path.basename(img_path)) for img_path in EXAMPLE_IMAGES], label="", columns=6, height="200px", object_fit="contain", elem_id="examples-gallery" ) def handle_example_selection(evt: gr.SelectData): print(f"Example selection event triggered. Index: {evt.index}") if evt.index < len(EXAMPLE_IMAGES): file_path = EXAMPLE_IMAGES[evt.index] print(f"Selected example image: {file_path}") if os.path.exists(file_path): try: img = Image.open(file_path).convert("RGB") numpy_image = np.array(img) print(f"Successfully loaded example image. Shape: {numpy_image.shape}") return numpy_image, numpy_image, None, [], "", "" except Exception as e: print(f"Error loading example image: {e}") gr.Error(f"Failed to load example image: {e}") else: print(f"Example image file not found: {file_path}") gr.Error(f"Example image file not found: {file_path}") else: print(f"Invalid example index: {evt.index}") gr.Error("Invalid example selection") return gr.update(), gr.update(), None, [], "", "" def handle_image_upload(image_data, current_session_id): print(f"Image upload triggered for session: {current_session_id}") print(f"Type of received image_data: {type(image_data)}") if isinstance(image_data, np.ndarray): print(f"Image uploaded as NumPy array, shape: {image_data.shape}") return image_data, image_data, None, [], "", "" else: print(f"Received unexpected image data type: {type(image_data)}") gr.Warning("Could not process the uploaded image. Please try again.") return gr.update(), gr.update(), None, [], "", "" def handle_start_recognition(img_data_from_state, current_session_id): if img_data_from_state is None: gr.Warning("No image provided for recognition! Please upload or select an example image first.") return None, [], "", "" print(f"Starting recognition for session: {current_session_id}") pil_image = Image.fromarray(img_data_from_state.astype('uint8'), 'RGB') temp_dir_abs = os.path.abspath("temp") os.makedirs(temp_dir_abs, exist_ok=True) temp_img_filename = f"input_{current_session_id}_{uuid.uuid4().hex[:8]}.png" temp_img_path = os.path.join(temp_dir_abs, temp_img_filename) pil_image.save(temp_img_path) # print(f"Temporary image for prediction saved to: {temp_img_path}") predicted_smiles_for_state = "" molecules_list = [] molecules_json = "" tabs_html = "" files = {'image': open(temp_img_path, 'rb')} try: response1 = requests.post(f"{url}/predict", files=files, headers=headers) response1.raise_for_status() # Raise an error for bad status codes processed_smiles = response1.json().get('predicted_smiles') # print(f"Raw Predicted SMILES from OCSR: {raw_smiles}") if processed_smiles: # NEW: Check if processed_smiles contains "$" for multiple SMILES if "$" in processed_smiles: try: response4 = requests.post(f"{url}/rearrange", data={"smiles_v": processed_smiles}, headers=headers) response4.raise_for_status() # Raise an error for bad status codes smi_set = response4.json().get('smi_set') if isinstance(smi_set, list) and len(smi_set) > 1: # Multiple SMILES case for i, smiles in enumerate(smi_set): response2 = requests.post(f"{url}/smi2mol", data={"smiles": smiles}, headers=headers) response2.raise_for_status() # Raise an error for bad status codes mol_block = response2.json().get('mol_block') if mol_block: molecules_list.append({ 'smiles': smiles, 'molfile': mol_block, 'index': i }) # print(f"Molecule {i+1}: {smiles}") if molecules_list: # Use the first molecule for the main display predicted_smiles_for_state = molecules_list[0]['smiles'] molecules_json = json.dumps(molecules_list) # Create tabs HTML tabs_buttons = [] for i, mol in enumerate(molecules_list): active_class = "active" if i == 0 else "" tabs_buttons.append(f'') tabs_html = f'
{"".join(tabs_buttons)}
' print(f"Created {len(molecules_list)} molecules for tabbed interface") else: warning_msg = "No valid molecules could be generated from the multiple SMILES" gr.Warning(warning_msg) print(warning_msg) else: # rv returned single SMILES or empty single_smiles = smi_set[0] if isinstance(smi_set, list) and len(smi_set) > 0 else processed_smiles predicted_smiles_for_state = single_smiles response2 = requests.post(f"{url}/smi2mol", data={"smiles": single_smiles}, headers=headers) response2.raise_for_status() # Raise an error for bad status codes mol_block = response2.json().get('mol_block') if mol_block: molecules_list = [{'smiles': single_smiles, 'molfile': mol_block, 'index': 0}] molecules_json = json.dumps(molecules_list) tabs_html = '
' else: warning_msg = f"Invalid SMILES ('{single_smiles}') after rv processing" gr.Warning(warning_msg) print(warning_msg) except Exception as e: print(f"Error processing multiple SMILES with rv function: {e}") # Fallback to single SMILES processing predicted_smiles_for_state = processed_smiles response2 = requests.post(f"{url}/smi2mol", data={"smiles": processed_smiles}, headers=headers) response2.raise_for_status() # Raise an error for bad status codes mol_block = response2.json().get('mol_block') if mol_block: molecules_list = [{'smiles': processed_smiles, 'molfile': mol_block, 'index': 0}] molecules_json = json.dumps(molecules_list) tabs_html = '
' else: # Single SMILES case (no "$") predicted_smiles_for_state = processed_smiles response2 = requests.post(f"{url}/smi2mol", data={"smiles": processed_smiles}, headers=headers) response2.raise_for_status() # Raise an error for bad status codes mol_block = response2.json().get('mol_block') if mol_block: molecules_list = [{'smiles': processed_smiles, 'molfile': mol_block, 'index': 0}] molecules_json = json.dumps(molecules_list) tabs_html = '
' else: warning_msg = f"Invalid SMILES ('{processed_smiles}') after processing or from smi2mol, cannot create Molfile or reliably show in Ketcher." gr.Warning(warning_msg) print(warning_msg) molecules_json = "" predicted_smiles_for_state = f": {warning_msg}" else: info_msg = "No SMILES string was predicted by the model." gr.Info(info_msg) print(info_msg) molecules_json = "" except Exception as e: error_msg = f"Error during OCSR prediction or Molfile generation: {e}" print(error_msg) gr.Error(f"An error occurred during processing: {str(e)[:100]}...") molecules_json = "" predicted_smiles_for_state = f"Error: {str(e)[:100]}" finally: if os.path.exists(temp_img_path): try: os.remove(temp_img_path) # print(f"Removed temporary image: {temp_img_path}") except Exception as e_rem: print(f"Error removing temporary image {temp_img_path}: {e_rem}") return predicted_smiles_for_state, molecules_list, molecules_json, tabs_html def process_smiles_and_show(ketcher_smiles): if not ketcher_smiles or not ketcher_smiles.strip(): gr.Warning("No SMILES structure available. Please draw a structure first.") return gr.update(value="
") try: # print(f"Processing SMILES with sma2smi: {ketcher_smiles}") response3 = requests.post(f"{url}/sma2smi", data={"sma": ketcher_smiles}, headers=headers) response3.raise_for_status() # Raise an error for bad status codes processed_smiles = response3.json().get('smi') # print(f"Processed SMILES result: {processed_smiles}") escaped_smiles = processed_smiles.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') return gr.update(value=f"
{escaped_smiles}
") except Exception as e: error_msg = f"Error processing SMILES with sma2smi: {e}" print(error_msg) gr.Error(f"An error occurred during SMILES processing: {str(e)[:100]}...") return gr.update(value="
Error processing SMILES
") # MODIFIED SECTION START # Define the combined workflow function def combined_recognition_workflow(img_data_from_state, current_session_id): empty = gr.HTML(value="
") # 负责清空processed-smiles-box里的内容 predicted_smiles, molecules_list, molecules_json, tabs_html = handle_start_recognition(img_data_from_state, current_session_id) return predicted_smiles, molecules_list, molecules_json, tabs_html, empty #, "" image_display.upload( fn=handle_image_upload, inputs=[image_display, session_id_state], outputs=[image_display, uploaded_image_state, current_smiles_state, multiple_molecules_state, molecules_json_trigger, molecule_tabs_area], show_progress="upload" ) examples_gallery.select( fn=handle_example_selection, inputs=[], outputs=[image_display, uploaded_image_state, current_smiles_state, multiple_molecules_state, molecules_json_trigger, molecule_tabs_area] ) # Replace the .then() chain with a single .click() call start_btn.click( fn=combined_recognition_workflow, inputs=[uploaded_image_state, session_id_state], outputs=[ current_smiles_state, # Updated by combined_recognition_workflow's 1st return value multiple_molecules_state, # Updated by combined_recognition_workflow's 2nd return value molecules_json_trigger, # Updated by combined_recognition_workflow's 3rd return value molecule_tabs_area, # Updated by combined_recognition_workflow's 4th return value processed_smiles_output, # 负责清空processed-smiles-box里的内容 # hide_indicator_html_trigger, # 很重要,需要有这个变量才能展示识别的progress,现在mol tab区域显示progress,就不用这个了 ], ) # MODIFIED SECTION END # Update molecules in Ketcher when molecules_json_trigger changes molecules_json_trigger.change( fn=None, inputs=[molecules_json_trigger], outputs=[], js=update_molecules_js ) smiles_btn.click( fn=process_smiles_and_show, inputs=[smiles_data], outputs=[processed_smiles_output], js=""" async function() { try { console.log('Fetching SMILES from Ketcher for main display'); const ketcher = window.ketcher; if (!ketcher || typeof ketcher.getSmiles !== 'function') { alert('Ketcher is not ready. Please wait for it to load.'); return ''; } const smiles = await ketcher.getSmiles(); // console.log('SMILES from Ketcher (for main display):', smiles); if (!smiles || !smiles.trim()) { alert('No molecule structure found in Ketcher. Please draw a structure first.'); return ''; } return smiles; } catch (e) { console.error('Error fetching SMILES from Ketcher (for main display):', e); alert('Error fetching SMILES: ' + e.message); return ''; } } """ ) molfile_btn.click( fn=None, inputs=[], outputs=[molfile_data], js=get_molfile_js ) copy_btn.click( fn=None, inputs=[], outputs=[], js=""" function() { const smilesBox = document.getElementById('processed-smiles-box'); const textToCopy = smilesBox ? smilesBox.textContent : null; const mainCopyButtonElement = document.getElementById('copy-btn'); if (!mainCopyButtonElement) { console.error('Copy button element with id "copy-btn" not found.'); return; } if (textToCopy && textToCopy.trim() !== "") { const originalButtonText = mainCopyButtonElement.textContent; if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(textToCopy) .then(() => { mainCopyButtonElement.disabled = true; setTimeout(() => { mainCopyButtonElement.textContent = originalButtonText; mainCopyButtonElement.disabled = false; }, 2000); // console.log('Text copied to clipboard:', textToCopy); }) .catch(function(clipboardErr) { console.warn('Clipboard API failed:', clipboardErr); fallbackCopyTextToClipboard(); }); } else { console.warn('navigator.clipboard.writeText not available. Using fallback.'); fallbackCopyTextToClipboard(); } function fallbackCopyTextToClipboard() { let textarea = document.getElementById('copy-textarea'); let tempTextareaCreated = false; if (!textarea) { textarea = document.createElement('textarea'); textarea.id = 'copy-textarea-temp-main-fallback'; textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; textarea.style.top = '0'; textarea.setAttribute('aria-hidden', 'true'); document.body.appendChild(textarea); tempTextareaCreated = true; } textarea.value = textToCopy; textarea.focus(); textarea.select(); try { const successful = document.execCommand('copy'); if (successful) { console.log('Fallback copy successful'); mainCopyButtonElement.textContent = 'Copied!'; mainCopyButtonElement.disabled = true; setTimeout(() => { mainCopyButtonElement.textContent = originalButtonText; mainCopyButtonElement.disabled = false; }, 2000); } else { console.warn('Fallback copy command was unsuccessful'); alert('Copy failed. Please try selecting and copying the text manually.'); } } catch (err) { console.error('Error during fallback copy:', err); alert('Copy failed. Please try selecting and copying the text manually.'); } finally { if (tempTextareaCreated && textarea.parentNode) { textarea.parentNode.removeChild(textarea); } if (document.activeElement === textarea) { textarea.blur(); } else if (window.getSelection) { if (window.getSelection().empty) { window.getSelection().empty(); } else if (window.getSelection().removeAllRanges) { window.getSelection().removeAllRanges(); } } else if (document.selection) { document.selection.empty(); } } } } else { alert('No SMILES data to copy or data is empty.'); } } """ ) app.load( fn=None, inputs=None, outputs=None, js=init_sidebar_js ) app.load( fn=None, inputs=None, outputs=None, js=load_ketcher_js ) # NEW: Initialize clipboard paste functionality app.load( fn=None, inputs=None, outputs=None, js=init_clipboard_paste_js ) return app if __name__ == "__main__": current_time_sec = time.time() one_hour_ago_sec = current_time_sec - 3600 temp_dir_abs = os.path.abspath("temp") if os.path.isdir(temp_dir_abs): for old_file_pattern in ["*.mol", "*.png", "*.smi"]: for old_file_path in glob.glob(os.path.join(temp_dir_abs, old_file_pattern)): try: if os.path.getmtime(old_file_path) < one_hour_ago_sec: os.remove(old_file_path) print(f"Removed old temp file: {old_file_path}") except OSError as e: print(f"Error removing old temp file {old_file_path}: {e}") else: print(f"Temporary directory '{temp_dir_abs}' does not exist. Skipping cleanup.") gradio_app = create_app() try: gradio_app.launch(debug=True) except Exception as e: print(f"Failed to launch Gradio app: {e}") print("This might be due to a port conflict or other server setup issues.")