import streamlit as st import tensorflow as tf import numpy as np import requests import zipfile import os import pickle # GitHub repository details (no longer used for direct download, but kept for context if needed) GITHUB_REPO_OWNER = "HeshamSaadi" GITHUB_REPO_NAME = "sentiment-app" GITHUB_RELEASE_TAG = "v1.0" # Assuming this is the tag for your release GITHUB_MODEL_ZIP_NAME = "models.zip" # Paths for model and tokenizer within the extracted zip MODEL_FILE_NAME = "simplified_lstm_20250622-170533_final.keras" # Updated to the correct filename TOKENIZER_FILE_NAME = "simplified_lstm_20250622-170533_tokenizer.pickle" LABEL_MAPPING_FILE_NAME = "simplified_lstm_20250622-170533_label_mapping.pickle" # Simple Attention Layer compatible with all Keras versions class SimpleAttention(tf.keras.layers.Layer): def __init__(self, **kwargs): super(SimpleAttention, self).__init__(**kwargs) def build(self, input_shape): self.W = self.add_weight( name="attention_weight", shape=(input_shape[-1], 1), initializer="glorot_uniform", trainable=True ) super(SimpleAttention, self).build(input_shape) def call(self, inputs): # inputs shape: (batch_size, seq_len, features) # Attention scores - using Keras backend operations e = tf.keras.backend.tanh(tf.keras.backend.dot(inputs, self.W)) e = tf.keras.backend.squeeze(e, axis=-1) # Attention weights alpha = tf.keras.backend.softmax(e) # Apply attention weights to inputs output = inputs * tf.keras.backend.expand_dims(alpha, axis=-1) output = tf.keras.backend.sum(output, axis=1) return output def get_config(self): config = super(SimpleAttention, self).get_config() return config # Custom FocalLoss class from the notebook class FocalLoss(tf.keras.losses.Loss): def __init__(self, gamma=2.0, alpha=0.25, name="focal_loss", **kwargs): super(FocalLoss, self).__init__(name=name, **kwargs) self.gamma = gamma self.alpha = alpha def call(self, y_true, y_pred): y_pred = tf.clip_by_value(y_pred, tf.keras.backend.epsilon(), 1. - tf.keras.backend.epsilon()) # Ensure 1 is a tensor for tf.where comparison # Use tf.cast to ensure dtype consistency for 1 and 1.0 pt = tf.where(tf.equal(y_true, tf.cast(1, dtype=y_true.dtype)), y_pred, tf.cast(1, dtype=y_true.dtype) - y_pred) loss = -tf.keras.backend.mean(self.alpha * tf.keras.backend.pow(tf.cast(1.0, dtype=pt.dtype) - pt, self.gamma) * tf.keras.backend.log(pt), axis=-1) return loss def get_config(self): config = super(FocalLoss, self).get_config() config.update({ "gamma": self.gamma, "alpha": self.alpha, }) return config @st.cache_resource def load_model_and_tokenizer(): model = None tokenizer = None label_mapping = None st.write("Extracting model from local models.zip...") try: zip_path = GITHUB_MODEL_ZIP_NAME if not os.path.exists(zip_path): st.error(f"Model zip file not found at {zip_path}. Please ensure models.zip is in the root of your Hugging Face Space.") return None, None, None with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(".") st.write("Extraction complete.") # Define paths to the extracted files # Assuming the zip extracts to a \"models\" directory at the root model_path = os.path.join("models", MODEL_FILE_NAME) tokenizer_path = os.path.join("models", TOKENIZER_FILE_NAME) label_mapping_path = os.path.join("models", LABEL_MAPPING_FILE_NAME) # Custom objects dictionary for model loading custom_objects = { "FocalLoss": FocalLoss, "SimpleAttention": SimpleAttention, } # Attempt to load the model with compile=False with tf.keras.utils.custom_object_scope(custom_objects): model = tf.keras.models.load_model(model_path, compile=False) # If the model loads successfully, it can be used for prediction directly. # Recompilation is not strictly necessary for inference. with open(tokenizer_path, "rb") as handle: tokenizer = pickle.load(handle) with open(label_mapping_path, "rb") as handle: label_mapping = pickle.load(handle) st.success("Model and tokenizer loaded successfully!") except FileNotFoundError as e: st.error(f"File not found after extraction: {e}. Please check the paths within your zip file.") except zipfile.BadZipFile: st.error("Downloaded file is not a valid zip file.") except Exception as e: st.error(f"An unexpected error occurred: {e}") st.error("Could not load the model. Please check the model files and paths.") return model, tokenizer, label_mapping model, tokenizer, label_mapping = load_model_and_tokenizer() if model and tokenizer and label_mapping: st.title("Sentiment Analysis App") user_input = st.text_area("Enter text for sentiment analysis:", "") if st.button("Analyze Sentiment"): if user_input: # Preprocess the input text (you might need to adapt this based on your model\"s preprocessing) # For an LSTM, typically tokenization and padding are needed # Ensure the tokenizer is fitted on the same vocabulary as during training # and the max_len matches your model\"s input shape. # Example preprocessing (adjust as per your model\"s requirements): # Assuming your tokenizer expects text and outputs sequences # And your model expects padded sequences sequence = tokenizer.texts_to_sequences([user_input]) padded_sequence = tf.keras.preprocessing.sequence.pad_sequences(sequence, maxlen=model.input_shape[1]) # Make prediction prediction = model.predict(padded_sequence) st.write(f"Raw prediction probabilities: {prediction}") # Added for debugging predicted_class = np.argmax(prediction, axis=1)[0] st.write(f"Predicted class index: {predicted_class}") # Added for debugging # Map prediction to sentiment label sentiment_labels = {v: k for k, v in label_mapping.items()} predicted_sentiment = sentiment_labels.get(predicted_class, "Unknown") st.write(f"Sentiment: **{predicted_sentiment}**") else: st.warning("Please enter some text to analyze.") else: st.warning("Model could not be loaded. Please check the logs above for details.")