|
|
|
import gradio as gr |
|
import torch |
|
|
|
import pytorch_lightning as pl |
|
import os |
|
import json |
|
import logging |
|
from tokenizers import Tokenizer |
|
from huggingface_hub import hf_hub_download |
|
import gc |
|
try: |
|
from rdkit import Chem |
|
from rdkit import RDLogger |
|
RDLogger.DisableLog('rdApp.*') |
|
except ImportError: |
|
logging.warning("RDKit not found. SMILES canonicalization will be skipped. Install with 'pip install rdkit'") |
|
Chem = None |
|
|
|
|
|
MODEL_REPO_ID = ( |
|
"AdrianM0/smiles-to-iupac-translator" |
|
) |
|
CHECKPOINT_FILENAME = "last.ckpt" |
|
SMILES_TOKENIZER_FILENAME = "smiles_bytelevel_bpe_tokenizer_scaled.json" |
|
IUPAC_TOKENIZER_FILENAME = "iupac_unigram_tokenizer_scaled.json" |
|
CONFIG_FILENAME = "config.json" |
|
|
|
|
|
|
|
logging.basicConfig( |
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" |
|
) |
|
|
|
|
|
try: |
|
|
|
from enhanced_trainer import SmilesIupacLitModule, generate_square_subsequent_mask |
|
logging.info("Successfully imported from enhanced_trainer.py.") |
|
except ImportError as e: |
|
logging.error( |
|
f"Failed to import helper code from enhanced_trainer.py: {e}. " |
|
f"Make sure enhanced_trainer.py is in the root of the Hugging Face repo '{MODEL_REPO_ID}'." |
|
) |
|
|
|
SmilesIupacLitModule = None |
|
generate_square_subsequent_mask = None |
|
except Exception as e: |
|
logging.error( |
|
f"An unexpected error occurred during helper code import: {e}", exc_info=True |
|
) |
|
SmilesIupacLitModule = None |
|
generate_square_subsequent_mask = None |
|
|
|
|
|
|
|
model: pl.LightningModule | None = None |
|
smiles_tokenizer: Tokenizer | None = None |
|
iupac_tokenizer: Tokenizer | None = None |
|
device: torch.device | None = None |
|
config: dict | None = None |
|
|
|
|
|
|
|
def greedy_decode( |
|
model: pl.LightningModule, |
|
src: torch.Tensor, |
|
src_padding_mask: torch.Tensor, |
|
max_len: int, |
|
sos_idx: int, |
|
eos_idx: int, |
|
device: torch.device, |
|
) -> torch.Tensor: |
|
""" |
|
Performs greedy decoding using the LightningModule's model. |
|
Assumes model has 'model.encode', 'model.decode', 'model.generator' attributes. |
|
""" |
|
if not hasattr(model, 'model') or not hasattr(model.model, 'encode') or \ |
|
not hasattr(model.model, 'decode') or not hasattr(model.model, 'generator'): |
|
logging.error("Model object does not have the expected 'model.encode/decode/generator' structure.") |
|
raise AttributeError("Model structure mismatch for greedy decoding.") |
|
|
|
if generate_square_subsequent_mask is None: |
|
logging.error("generate_square_subsequent_mask function not imported.") |
|
raise ImportError("generate_square_subsequent_mask is required for greedy_decode.") |
|
|
|
|
|
model.eval() |
|
transformer_model = model.model |
|
|
|
try: |
|
with torch.no_grad(): |
|
|
|
|
|
memory = transformer_model.encode( |
|
src, src_mask=None |
|
) |
|
|
|
|
|
memory = memory.to(device) |
|
|
|
|
|
memory_key_padding_mask = src_padding_mask.to(device) |
|
|
|
|
|
|
|
ys = torch.ones(1, 1, dtype=torch.long, device=device).fill_( |
|
sos_idx |
|
) |
|
|
|
|
|
for i in range(max_len - 1): |
|
|
|
|
|
|
|
|
|
|
|
|
|
tgt_seq_len = ys.shape[1] |
|
|
|
tgt_mask = generate_square_subsequent_mask(tgt_seq_len, device).to( |
|
device |
|
) |
|
|
|
|
|
tgt_padding_mask = torch.zeros( |
|
ys.shape, dtype=torch.bool, device=device |
|
) |
|
|
|
|
|
|
|
ys_decoder_input = ys.transpose(0, 1).to(device) |
|
|
|
|
|
decoder_output = transformer_model.decode( |
|
tgt=ys_decoder_input, |
|
memory=memory, |
|
tgt_mask=tgt_mask, |
|
tgt_key_padding_mask=tgt_padding_mask, |
|
memory_key_padding_mask=memory_key_padding_mask, |
|
) |
|
|
|
|
|
|
|
next_token_logits = transformer_model.generator( |
|
decoder_output[-1, :, :] |
|
) |
|
|
|
|
|
next_word_id_tensor = torch.argmax(next_token_logits, dim=1) |
|
next_word_id = next_word_id_tensor.item() |
|
|
|
|
|
next_word_tensor = torch.ones(1, 1, dtype=torch.long, device=device).fill_(next_word_id) |
|
|
|
|
|
ys = torch.cat([ys, next_word_tensor], dim=1) |
|
|
|
|
|
if next_word_id == eos_idx: |
|
break |
|
|
|
|
|
|
|
return ys[:, 1:] |
|
|
|
except RuntimeError as e: |
|
logging.error(f"Runtime error during greedy decode: {e}", exc_info=True) |
|
if "CUDA out of memory" in str(e) and device.type == "cuda": |
|
gc.collect() |
|
torch.cuda.empty_cache() |
|
raise RuntimeError("CUDA out of memory during greedy decoding.") |
|
raise e |
|
except Exception as e: |
|
logging.error(f"Unexpected error during greedy decode: {e}", exc_info=True) |
|
raise e |
|
|
|
|
|
|
|
def translate( |
|
model: pl.LightningModule, |
|
src_sentence: str, |
|
smiles_tokenizer: Tokenizer, |
|
iupac_tokenizer: Tokenizer, |
|
device: torch.device, |
|
max_len_config: int, |
|
sos_idx: int, |
|
eos_idx: int, |
|
pad_idx: int, |
|
) -> str: |
|
""" |
|
Translates a single SMILES string using greedy decoding. |
|
""" |
|
if not all([model, smiles_tokenizer, iupac_tokenizer, device, config]): |
|
return "[Initialization Error: Components not loaded]" |
|
|
|
model.eval() |
|
|
|
|
|
try: |
|
|
|
smiles_tokenizer.enable_truncation(max_length=max_len_config) |
|
smiles_tokenizer.enable_padding(pad_id=pad_idx, pad_token="<pad>", length=max_len_config) |
|
|
|
src_encoded = smiles_tokenizer.encode(src_sentence) |
|
if not src_encoded or not src_encoded.ids: |
|
logging.warning(f"Encoding failed or empty for SMILES: {src_sentence}") |
|
return "[Encoding Error: Empty result]" |
|
src_ids = src_encoded.ids |
|
|
|
|
|
src_attention_mask = torch.tensor(src_encoded.attention_mask, dtype=torch.long) |
|
src_padding_mask = (src_attention_mask == 0) |
|
|
|
except Exception as e: |
|
logging.error(f"Error tokenizing SMILES '{src_sentence}': {e}", exc_info=True) |
|
return f"[Encoding Error: {e}]" |
|
|
|
|
|
|
|
src = torch.tensor(src_ids, dtype=torch.long).unsqueeze(0).to(device) |
|
|
|
src_padding_mask = src_padding_mask.unsqueeze(0).to(device) |
|
|
|
|
|
try: |
|
tgt_tokens_tensor = greedy_decode( |
|
model=model, |
|
src=src, |
|
src_padding_mask=src_padding_mask, |
|
max_len=max_len_config, |
|
sos_idx=sos_idx, |
|
eos_idx=eos_idx, |
|
device=device, |
|
) |
|
|
|
except (RuntimeError, AttributeError, ImportError, Exception) as e: |
|
logging.error(f"Error during greedy_decode call: {e}", exc_info=True) |
|
return f"[Decoding Error: {e}]" |
|
|
|
|
|
|
|
if tgt_tokens_tensor is None or tgt_tokens_tensor.numel() == 0: |
|
|
|
if len(src_ids) <= 2 and all(t in [pad_idx, eos_idx, sos_idx] for t in src_ids): |
|
logging.warning(f"Input SMILES '{src_sentence}' resulted in very short/empty encoding, leading to empty decode.") |
|
return "[Decoding Warning: Input potentially too short or invalid after tokenization]" |
|
else: |
|
logging.warning( |
|
f"Greedy decode returned empty tensor for SMILES: {src_sentence}" |
|
) |
|
return "[Decoding Error: Empty Output]" |
|
|
|
tgt_tokens = tgt_tokens_tensor.flatten().cpu().numpy().tolist() |
|
try: |
|
|
|
translation = iupac_tokenizer.decode(tgt_tokens, skip_special_tokens=True) |
|
return translation.strip() |
|
except Exception as e: |
|
logging.error( |
|
f"Error decoding target tokens {tgt_tokens}: {e}", |
|
exc_info=True, |
|
) |
|
return "[Decoding Error: Tokenizer failed]" |
|
|
|
|
|
|
|
def load_model_and_tokenizers(): |
|
"""Loads tokenizers, config, and model from Hugging Face Hub.""" |
|
global model, smiles_tokenizer, iupac_tokenizer, device, config, SmilesIupacLitModule, generate_square_subsequent_mask |
|
if model is not None: |
|
logging.info("Model and tokenizers already loaded.") |
|
return |
|
|
|
logging.info(f"Starting model and tokenizer loading from {MODEL_REPO_ID}...") |
|
|
|
|
|
if SmilesIupacLitModule is None or generate_square_subsequent_mask is None: |
|
error_msg = f"Initialization Error: Could not load required components from enhanced_trainer.py. Check Space logs and ensure the file exists in the repo root." |
|
logging.error(error_msg) |
|
raise gr.Error(error_msg) |
|
|
|
try: |
|
|
|
if torch.cuda.is_available(): |
|
device = torch.device("cuda") |
|
logging.info("CUDA available, using GPU.") |
|
else: |
|
device = torch.device("cpu") |
|
logging.info("CUDA not available, using CPU.") |
|
|
|
|
|
logging.info("Downloading files from Hugging Face Hub...") |
|
try: |
|
|
|
cache_dir = os.environ.get("GRADIO_CACHE", "./hf_cache") |
|
os.makedirs(cache_dir, exist_ok=True) |
|
logging.info(f"Using cache directory: {cache_dir}") |
|
|
|
|
|
checkpoint_path = hf_hub_download( |
|
repo_id=MODEL_REPO_ID, filename=CHECKPOINT_FILENAME, cache_dir=cache_dir, force_download=False |
|
) |
|
smiles_tokenizer_path = hf_hub_download( |
|
repo_id=MODEL_REPO_ID, filename=SMILES_TOKENIZER_FILENAME, cache_dir=cache_dir, force_download=False |
|
) |
|
iupac_tokenizer_path = hf_hub_download( |
|
repo_id=MODEL_REPO_ID, filename=IUPAC_TOKENIZER_FILENAME, cache_dir=cache_dir, force_download=False |
|
) |
|
config_path = hf_hub_download( |
|
repo_id=MODEL_REPO_ID, filename=CONFIG_FILENAME, cache_dir=cache_dir, force_download=False |
|
) |
|
logging.info("Files downloaded (or found in cache) successfully.") |
|
except Exception as e: |
|
logging.error( |
|
f"Failed to download files from {MODEL_REPO_ID}. Check filenames and repo status. Error: {e}", |
|
exc_info=True, |
|
) |
|
raise gr.Error( |
|
f"Download Error: Could not download required files from {MODEL_REPO_ID}. Check Space logs. Error: {e}" |
|
) |
|
|
|
|
|
logging.info("Loading configuration...") |
|
try: |
|
with open(config_path, "r") as f: |
|
config = json.load(f) |
|
logging.info("Configuration loaded.") |
|
|
|
required_keys = [ |
|
"src_vocab_size", |
|
"tgt_vocab_size", |
|
"emb_size", |
|
"nhead", |
|
"ffn_hid_dim", |
|
"num_encoder_layers", |
|
"num_decoder_layers", |
|
"dropout", |
|
"max_len", |
|
"pad_token_id", |
|
"bos_token_id", |
|
"eos_token_id", |
|
] |
|
|
|
|
|
config['src_vocab_size'] = config.get('src_vocab_size', config.get('SRC_VOCAB_SIZE')) |
|
config['tgt_vocab_size'] = config.get('tgt_vocab_size', config.get('TGT_VOCAB_SIZE')) |
|
config['emb_size'] = config.get('emb_size', config.get('EMB_SIZE')) |
|
config['nhead'] = config.get('nhead', config.get('NHEAD')) |
|
config['ffn_hid_dim'] = config.get('ffn_hid_dim', config.get('FFN_HID_DIM')) |
|
config['num_encoder_layers'] = config.get('num_encoder_layers', config.get('NUM_ENCODER_LAYERS')) |
|
config['num_decoder_layers'] = config.get('num_decoder_layers', config.get('NUM_DECODER_LAYERS')) |
|
config['dropout'] = config.get('dropout', config.get('DROPOUT')) |
|
config['max_len'] = config.get('max_len', config.get('MAX_LEN')) |
|
config['pad_token_id'] = config.get('pad_token_id', config.get('PAD_IDX')) |
|
config['bos_token_id'] = config.get('bos_token_id', config.get('BOS_IDX')) |
|
config['eos_token_id'] = config.get('eos_token_id', config.get('EOS_IDX')) |
|
|
|
|
|
|
|
missing_keys = [key for key in required_keys if config.get(key) is None] |
|
if missing_keys: |
|
raise ValueError( |
|
f"Config file '{CONFIG_FILENAME}' is missing required keys: {missing_keys}. " |
|
f"Ensure these were saved in the hyperparameters during training." |
|
) |
|
|
|
logging.info( |
|
f"Using config: src_vocab={config['src_vocab_size']}, tgt_vocab={config['tgt_vocab_size']}, " |
|
f"emb={config['emb_size']}, nhead={config['nhead']}, enc={config['num_encoder_layers']}, dec={config['num_decoder_layers']}, " |
|
f"pad={config['pad_token_id']}, sos={config['bos_token_id']}, eos={config['eos_token_id']}, max_len={config['max_len']}" |
|
) |
|
|
|
except FileNotFoundError: |
|
logging.error(f"Config file not found: {config_path}") |
|
raise gr.Error(f"Config Error: Config file '{CONFIG_FILENAME}' not found.") |
|
except json.JSONDecodeError as e: |
|
logging.error(f"Error decoding JSON from config file {config_path}: {e}") |
|
raise gr.Error( |
|
f"Config Error: Could not parse '{CONFIG_FILENAME}'. Error: {e}" |
|
) |
|
except ValueError as e: |
|
logging.error(f"Config validation error: {e}") |
|
raise gr.Error(f"Config Error: {e}") |
|
except Exception as e: |
|
logging.error(f"Unexpected error loading config: {e}", exc_info=True) |
|
raise gr.Error(f"Config Error: Unexpected error. Check logs. Error: {e}") |
|
|
|
|
|
logging.info("Loading tokenizers...") |
|
try: |
|
smiles_tokenizer = Tokenizer.from_file(smiles_tokenizer_path) |
|
iupac_tokenizer = Tokenizer.from_file(iupac_tokenizer_path) |
|
|
|
|
|
pad_token = "<pad>" |
|
sos_token = "<sos>" |
|
eos_token = "<eos>" |
|
unk_token = "<unk>" |
|
issues = [] |
|
|
|
|
|
smiles_pad_id = smiles_tokenizer.token_to_id(pad_token) |
|
smiles_unk_id = smiles_tokenizer.token_to_id(unk_token) |
|
if smiles_pad_id is None or smiles_pad_id != config["pad_token_id"]: |
|
issues.append(f"SMILES PAD ID mismatch (Tokenizer: {smiles_pad_id}, Config: {config['pad_token_id']})") |
|
if smiles_unk_id is None: |
|
issues.append("SMILES UNK token not found in tokenizer") |
|
|
|
|
|
iupac_pad_id = iupac_tokenizer.token_to_id(pad_token) |
|
iupac_sos_id = iupac_tokenizer.token_to_id(sos_token) |
|
iupac_eos_id = iupac_tokenizer.token_to_id(eos_token) |
|
iupac_unk_id = iupac_tokenizer.token_to_id(unk_token) |
|
if iupac_pad_id is None or iupac_pad_id != config["pad_token_id"]: |
|
issues.append(f"IUPAC PAD ID mismatch (Tokenizer: {iupac_pad_id}, Config: {config['pad_token_id']})") |
|
if iupac_sos_id is None or iupac_sos_id != config["bos_token_id"]: |
|
issues.append(f"IUPAC SOS ID mismatch (Tokenizer: {iupac_sos_id}, Config: {config['bos_token_id']})") |
|
if iupac_eos_id is None or iupac_eos_id != config["eos_token_id"]: |
|
issues.append(f"IUPAC EOS ID mismatch (Tokenizer: {iupac_eos_id}, Config: {config['eos_token_id']})") |
|
if iupac_unk_id is None: |
|
issues.append("IUPAC UNK token not found in tokenizer") |
|
|
|
if issues: |
|
logging.warning("Tokenizer validation issues detected: \n - " + "\n - ".join(issues)) |
|
|
|
|
|
else: |
|
logging.info("Tokenizers loaded and special tokens validated against config.") |
|
|
|
except Exception as e: |
|
logging.error(f"Failed to load tokenizers: {e}", exc_info=True) |
|
raise gr.Error( |
|
f"Tokenizer Error: Could not load tokenizers. Check logs and file paths. Error: {e}" |
|
) |
|
|
|
|
|
logging.info("Loading model from checkpoint...") |
|
try: |
|
|
|
|
|
model_instance = SmilesIupacLitModule(**config) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
model = SmilesIupacLitModule.load_from_checkpoint( |
|
checkpoint_path, |
|
map_location=device, |
|
|
|
|
|
**config, |
|
strict=True |
|
) |
|
|
|
|
|
model.to(device) |
|
model.eval() |
|
model.freeze() |
|
logging.info( |
|
f"Model loaded successfully from {checkpoint_path}, set to eval mode, frozen, and moved to device '{device}'." |
|
) |
|
|
|
except FileNotFoundError: |
|
logging.error(f"Checkpoint file not found: {checkpoint_path}") |
|
raise gr.Error( |
|
f"Model Error: Checkpoint file '{CHECKPOINT_FILENAME}' not found at expected path." |
|
) |
|
except RuntimeError as e: |
|
logging.error(f"Runtime error loading model checkpoint {checkpoint_path}: {e}", exc_info=True) |
|
if "size mismatch" in str(e): |
|
error_detail = f"Potential size mismatch. Check vocab sizes in config.json (src={config.get('src_vocab_size')}, tgt={config.get('tgt_vocab_size')}) vs checkpoint structure. Or config doesn't match model definition." |
|
logging.error(error_detail) |
|
raise gr.Error(f"Model Load Error: {error_detail} Original error: {e}") |
|
elif "CUDA out of memory" in str(e) or "memory" in str(e).lower(): |
|
logging.warning("Potential OOM error during model loading.") |
|
gc.collect() |
|
if device.type == "cuda": torch.cuda.empty_cache() |
|
raise gr.Error(f"Model Load Error: OOM loading model. Check Space resources. Error: {e}") |
|
else: |
|
raise gr.Error(f"Model Load Error: Runtime error. Check logs. Error: {e}") |
|
except Exception as e: |
|
logging.error( |
|
f"Unexpected error loading model checkpoint {checkpoint_path}: {e}", exc_info=True |
|
) |
|
raise gr.Error( |
|
f"Model Load Error: Failed to load checkpoint for unknown reason. Check logs. Error: {e}" |
|
) |
|
|
|
except gr.Error as ge: |
|
raise ge |
|
except Exception as e: |
|
logging.error(f"Unexpected error during loading process: {e}", exc_info=True) |
|
raise gr.Error( |
|
f"Initialization Error: Unexpected failure during setup. Check logs. Error: {e}" |
|
) |
|
|
|
|
|
|
|
def predict_iupac(smiles_string): |
|
""" |
|
Performs SMILES to IUPAC translation using the loaded model and greedy decoding. |
|
Handles input validation, canonicalization, translation, and output formatting. |
|
""" |
|
global model, smiles_tokenizer, iupac_tokenizer, device, config |
|
|
|
|
|
if not all([model, smiles_tokenizer, iupac_tokenizer, device, config]): |
|
error_msg = "Error: Model or tokenizers not loaded. App initialization failed. Check Space logs." |
|
logging.error(error_msg) |
|
|
|
return error_msg |
|
|
|
|
|
if not smiles_string or not smiles_string.strip(): |
|
return "Error: Please enter a valid SMILES string." |
|
|
|
smiles_input_raw = smiles_string.strip() |
|
|
|
|
|
smiles_input_canon = smiles_input_raw |
|
if Chem: |
|
try: |
|
mol = Chem.MolFromSmiles(smiles_input_raw) |
|
if mol: |
|
smiles_input_canon = Chem.MolToSmiles(mol, canonical=True) |
|
logging.info(f"Canonicalized SMILES: {smiles_input_raw} -> {smiles_input_canon}") |
|
else: |
|
|
|
logging.warning(f"Could not parse SMILES '{smiles_input_raw}' with RDKit. Using raw input.") |
|
|
|
|
|
except Exception as e: |
|
logging.error(f"Error during RDKit canonicalization for '{smiles_input_raw}': {e}", exc_info=True) |
|
|
|
|
|
|
|
|
|
try: |
|
sos_idx = config["bos_token_id"] |
|
eos_idx = config["eos_token_id"] |
|
pad_idx = config["pad_token_id"] |
|
gen_max_len = config["max_len"] |
|
|
|
predicted_name = translate( |
|
model=model, |
|
src_sentence=smiles_input_canon, |
|
smiles_tokenizer=smiles_tokenizer, |
|
iupac_tokenizer=iupac_tokenizer, |
|
device=device, |
|
max_len_config=gen_max_len, |
|
sos_idx=sos_idx, |
|
eos_idx=eos_idx, |
|
pad_idx=pad_idx, |
|
) |
|
logging.info(f"SMILES: '{smiles_input_canon}', Prediction: '{predicted_name}'") |
|
|
|
|
|
|
|
if predicted_name.startswith("[") and predicted_name.endswith("]"): |
|
|
|
output_text = ( |
|
f"Input SMILES: {smiles_input_canon}\n" |
|
f"(Raw Input: {smiles_input_raw})\n\n" |
|
f"Prediction Failed: {predicted_name}" |
|
) |
|
elif not predicted_name: |
|
output_text = ( |
|
f"Input SMILES: {smiles_input_canon}\n" |
|
f"(Raw Input: {smiles_input_raw})\n\n" |
|
f"Prediction: [No name generated]" |
|
) |
|
else: |
|
output_text = ( |
|
f"Input SMILES: {smiles_input_canon}\n" |
|
f"(Raw Input: {smiles_input_raw})\n\n" |
|
f"Predicted IUPAC Name (Greedy Decode):\n" |
|
f"{predicted_name}" |
|
) |
|
|
|
if smiles_input_raw == smiles_input_canon: |
|
output_text = output_text.replace(f"(Raw Input: {smiles_input_raw})\n", "") |
|
|
|
return output_text.strip() |
|
|
|
except Exception as e: |
|
|
|
logging.error(f"Unexpected error during prediction for '{smiles_input_canon}': {e}", exc_info=True) |
|
error_msg = f"Error: An unexpected error occurred during translation: {e}" |
|
return error_msg |
|
|
|
|
|
|
|
|
|
model_load_error = None |
|
try: |
|
load_model_and_tokenizers() |
|
except gr.Error as ge: |
|
logging.error(f"Gradio Initialization Error during load: {ge}") |
|
model_load_error = str(ge) |
|
except Exception as e: |
|
logging.critical(f"CRITICAL error during initial model loading: {e}", exc_info=True) |
|
model_load_error = f"Critical Error: {e}. Check Space logs." |
|
|
|
|
|
|
|
title = "SMILES to IUPAC Name Translator (Greedy Decoding)" |
|
description = f""" |
|
Enter a SMILES string to translate it into its IUPAC chemical name using a Transformer model ({MODEL_REPO_ID}) trained via PyTorch Lightning. |
|
Translation uses **greedy decoding** (picks the most likely next word at each step). |
|
""" |
|
if model_load_error: |
|
description += f"\n\n**WARNING: Failed to load model or components.**\nReason: {model_load_error}\nFunctionality will be limited." |
|
elif device: |
|
description += f"\n**Note:** Model loaded on **{str(device).upper()}**. Check `config.json` in the repo for model details." |
|
else: |
|
description += f"\n**Note:** Device information unavailable (loading might have failed)." |
|
|
|
|
|
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="cyan")) as iface: |
|
gr.Markdown(f"# {title}") |
|
gr.Markdown(description) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
smiles_input = gr.Textbox( |
|
label="SMILES String", |
|
placeholder="Enter SMILES string (e.g., CCO or c1ccccc1)", |
|
lines=2, |
|
) |
|
submit_btn = gr.Button("Translate", variant="primary") |
|
|
|
with gr.Column(scale=2): |
|
output_text = gr.Textbox( |
|
label="Result", |
|
lines=5, |
|
show_copy_button=True, |
|
interactive=False, |
|
) |
|
|
|
|
|
gr.Examples( |
|
examples=[ |
|
"CCO", |
|
"C1=CC=C(C=C1)C(=O)O", |
|
"CC(C)CC1=CC=C(C=C1)C(C)C(=O)O", |
|
"INVALID_SMILES", |
|
"ClC(Cl)(Cl)C1=CC=C(C=C1)C(C2=CC=C(Cl)C=C2)C(Cl)(Cl)Cl", |
|
], |
|
inputs=smiles_input, |
|
outputs=output_text, |
|
fn=predict_iupac, |
|
cache_examples=False, |
|
) |
|
|
|
|
|
submit_btn.click(fn=predict_iupac, inputs=smiles_input, outputs=output_text, api_name="translate_smiles") |
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
iface.launch(share=False, debug=False) |