CLIP-OCSR / app.py
Sunin's picture
Update app.py
6da3604 verified
Raw
History Blame Contribute Delete
57.8 kB
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 = """
<div id="loading-ketcher" style="display:flex;justify-content:center;align-items:center; width: 100%; height: 100%;">
<p style="padding:0.2rem 1rem 0 0;color:#c1c1c1;font-size:1rem">loading SMILES editor</p>
<svg version="1.1" id="L4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve" width="5rem">
<circle fill="#FF7C00" stroke="none" cx="6" cy="50" r="6">
<animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.1"/>
</circle>
<circle fill="#FF7C00" stroke="none" cx="26" cy="50" r="6">
<animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.2"/>
</circle>
<circle fill="#FF7C00" stroke="none" cx="46" cy="50" r="6">
<animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.3"/>
</circle>
</svg>
</div>
<div id="root" style="width: 100%; height: 550px; display: none;"></div>
"""
# HTML for sidebar with icons - without JavaScript functions
sidebar_html = """
<div id="sidebar-container" style="display: none;"> <div class="sidebar-header">
<h3 id="sidebar-title">Molecule Data</h3>
<button id="close-sidebar" class="icon-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div class="sidebar-actions">
<button id="copy-btn-s" class="icon-button" title="Copy to clipboard">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</button>
<button id="download-btn" class="icon-button" title="Download file">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
</button>
</div>
<div class="sidebar-content">
<div id="content-area"></div>
</div>
</div>
<textarea id="copy-textarea" style="position: absolute; left: -9999px; top: 0;" aria-hidden="true"></textarea>
"""
# 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 = '<pre>' + content.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</pre>';
} else {
// For SMILES, word-wrap but don't add pre tags
formattedContent = '<div style="word-break: break-all;">' + content.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</div>';
}
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 = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
setTimeout(() => {
copyBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2 2v1"></path></svg>';
}, 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 = "<p style='color:red;'>Failed to load SMILES editor.</p>";
};
document.head.appendChild(script);
}).catch(err => {
console.error("Failed to fetch Ketcher JS:", err);
loadingDiv.innerHTML = "<p style='color:red;'>Failed to fetch SMILES editor assets.</p>";
});
}
"""
# 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("<h1>CLIP-OCSR: CLIP-empowered Optical Chemical Structure Recognition</h1><p style='font-size: 18px;'>Online tool for chemical structure extraction</p>")
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("""
<div id="running-indicator">
</div>
""")
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="<div id='processed-smiles-box'></div>"
)
copy_btn = gr.Button(
"📋",
elem_id="copy-btn",
)
with gr.Column(elem_id="examples-section"):
gr.HTML("<h3>Examples</h3>")
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'<button class="mol-tab-btn {active_class}" onclick="window.switchToMolecule({i})">Mol{i+1}</button>')
tabs_html = f'<div style="margin-bottom: 10px;">{"".join(tabs_buttons)}</div>'
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 = '<div style="margin-bottom: 10px;"><button class="mol-tab-btn active" onclick="window.switchToMolecule(0)">Mol1</button></div>'
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 = '<div style="margin-bottom: 10px;"><button class="mol-tab-btn active" onclick="window.switchToMolecule(0)">Mol1</button></div>'
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 = '<div style="margin-bottom: 10px;"><button class="mol-tab-btn active" onclick="window.switchToMolecule(0)">Mol1</button></div>'
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="<div id='processed-smiles-box'></div>")
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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;')
return gr.update(value=f"<div id='processed-smiles-box'>{escaped_smiles}</div>")
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="<div id='processed-smiles-box'>Error processing SMILES</div>")
# MODIFIED SECTION START
# Define the combined workflow function
def combined_recognition_workflow(img_data_from_state, current_session_id):
empty = gr.HTML(value="<div id='processed-smiles-box'></div>") # 负责清空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.")