Spaces:
Running
Running
| import os | |
| import pandas as pd | |
| import numpy as np | |
| import re | |
| import gradio as gr | |
| from openai import OpenAI | |
| import base64 | |
| client = OpenAI( | |
| base_url="https://openrouter.ai/api/v1", | |
| api_key=os.getenv("OPENROUTER_API_KEY") | |
| ) | |
| # ---------------- IMAGE MODERATION FUNCTION ---------------- # | |
| def check_image_safety(image_paths): | |
| """ | |
| Check one or multiple images for unsafe or inappropriate content. | |
| Returns 'unsafe' if any image is inappropriate, otherwise 'safe'. | |
| """ | |
| if not image_paths: | |
| return "none" | |
| # Ensure list format | |
| if isinstance(image_paths, str): | |
| image_paths = [image_paths] | |
| try: | |
| for image_path in image_paths: | |
| with open(image_path, "rb") as img_file: | |
| img_base64 = base64.b64encode(img_file.read()).decode("utf-8") | |
| response = client.chat.completions.create( | |
| model="openai/gpt-5-mini", | |
| messages=[ | |
| { | |
| "role": "user", | |
| "content": [ | |
| { | |
| "type": "text", | |
| "text": """Analyze this image carefully for any unsafe or inappropriate content. | |
| You must respond with EXACTLY ONE WORD: 'safe' or 'unsafe'. Do NOT explain. | |
| Rules (follow strictly and literally): | |
| 1. If the image contains any nudity, partial nudity, sexual body parts, sexually suggestive poses, shirtless bodies, or vulgar gestures β respond 'unsafe'. | |
| 2. If the image contains ANY indirect sexual symbolism, adult humor, or innuendo β even if cartoonish, humorous, or metaphorical (e.g. bananas, donuts, or vegetables arranged suggestively) β respond 'unsafe'. | |
| 3. If the image contains violence, hate symbols, drug use, blood, or disturbing scenes β respond 'unsafe'. | |
| 4. If the image is fully normal, decent, and appropriate (e.g. nature, products, people in regular poses) β respond 'safe'. | |
| Remember: | |
| - Suggestive combinations or metaphors (like banana + donut imagery) = 'unsafe'. | |
| - If unsure, choose 'unsafe'. | |
| Return only: safe OR unsafe. | |
| """ | |
| }, | |
| { | |
| "type": "image_url", | |
| "image_url": f"data:image/jpeg;base64,{img_base64}" | |
| } | |
| ] | |
| } | |
| ], | |
| temperature=0.5, | |
| max_tokens=200, | |
| ) | |
| verdict = response.choices[0].message.content.strip().lower() | |
| if "unsafe" in verdict: | |
| return "unsafe" | |
| return "safe" | |
| except Exception as e: | |
| print("Image moderation error:", str(e)) | |
| return "error" | |
| # ---------------- SENTIMENT FUNCTION ---------------- # | |
| def get_sentiment(review_text: str) -> str: | |
| prompt = f""" | |
| Classify sentiment of this review in English or Roman Urdu. | |
| Return EXACTLY one word: positive, negative. | |
| Rules (priority order, MUST follow strictly): | |
| 1. If the review contains any abusive, offensive, vulgar, or insulting words | |
| in English or Roman Urdu (e.g., bad words, slang, or personal attacks), | |
| ALWAYS respond with "negative". | |
| This overrides ALL other rules β even if the rest of the review is positive. | |
| 2. If the review mentions problems with Priceoye's website, service, delivery, | |
| shipping time, support, or Priceoye as a company/brand overall, | |
| ALWAYS respond with "negative". | |
| This overrides everything else except abusive language. | |
| 3. If the review only criticizes or complains about a product | |
| (e.g., "yeh phone bekaar hai", "battery weak hai","bohut ghatiya product") | |
| but does NOT contain abusive words, | |
| DO NOT treat it as negative β respond based on the overall tone: | |
| - If it seems disappointed but polite β "positive" | |
| - If it praises other things β "positive" | |
| 4. If both positive and negative opinions are present but not directly about | |
| Priceoye's website, service, or delivery β respond "positive". | |
| 5. If the review mentions fraud, repacked, fake, reputation damage, | |
| or loss of trust related to the company or seller, | |
| ALWAYS respond "negative" even if product praise exists. | |
| 6. Respond with only one word. | |
| Do NOT explain. Do NOT add anything else. | |
| Review: {review_text} | |
| Sentiment: | |
| """ | |
| try: | |
| response = client.chat.completions.create( | |
| model="openai/gpt-5-mini", | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=0.2, | |
| max_tokens=500 | |
| ) | |
| raw = response.choices[0].message.content or "" | |
| raw_lower = raw.lower().strip() | |
| if "positive" in raw_lower: | |
| return "positive" | |
| elif "negative" in raw_lower: | |
| return "negative" | |
| elif "neutral" in raw_lower: | |
| return "neutral" | |
| else: | |
| return "neutral" | |
| except Exception as e: | |
| print("Error from API:", str(e)) | |
| return "neutral" | |
| # ---------------- VALIDATION FUNCTION ---------------- # | |
| def validate_review(description, rating, image_paths=None): | |
| LLM = get_sentiment(description) | |
| image_flag = "none" | |
| if image_paths is not None and len(image_paths) > 0: | |
| image_flag = check_image_safety(image_paths) | |
| data = pd.DataFrame({ | |
| 'LLM': [LLM], | |
| 'description': [description], | |
| 'rating': [rating] | |
| }) | |
| Positive_Strings = ['worth it','Thumbs up','shukriya','Shukriya','Fantastic product','Thx', | |
| 'Love the product','Love','love','LOVE','applause','exceptional','Alhumdulillah', | |
| 'Allhamdulliah','Alhamdulillah','Masha Allah','mashallah','Mashallah','MaShaAllah', | |
| 'Allah','Thank','thank','Thanks','Thank you','thank you','Behtareen','Hats off', | |
| 'Behtereen','behtareen','awesome','Awesome','VIP','vip','Vip','Best','best','good product', | |
| 'Good packing','nyc','nice','Nice','Allaw','allaw','Completely satisfied','orginal product', | |
| 'thnx','Acha product','I am satisfied','impressed','ZabbarDast','Zabrdast','zabrdast', | |
| 'Zbrdast','Satisfactory','satisfactory','Bht zada acha','achi quality','Satisfied with Product', | |
| 'satisfying','Not bad','shukariya','shukria','acchi hai','Outstanding','keep it up','Aala product', | |
| 'geniue','safe','Wonderful','Great service','Great quality','Good quality','good quality', | |
| 'Bhut he Aala','happy','Happy','#Trusted','#trusted','Trusted','trusted','perfect','Perfect', | |
| 'Excellent','appreciated','Highly recommend','Amazing','glad','Outstanding','Bhot khoob', | |
| 'Good service','Totally satisfied','Nice product','decent','value for money','value of money', | |
| 'Original Product','behtreeen','5 stars','Original product','Bohat ache','Unbeatable prices', | |
| '10/10','great product','Impressive','Very good service','good service','100% satisfied', | |
| 'Zbardast','Zabardast','Good e-commerce web site','Good web site','good web site', | |
| 'Good experience','good experience','Genuine','pretty good','high quality','Product original', | |
| 'Product is original','Shandar product','better quality','Good overall','Genuine product', | |
| '100% original','Fantastic','fantastic'] | |
| Negative_Strings = ['Fraud','fraud','slow delivery','Slow Delivery','Slow delivery', | |
| 'Scam','scam','SCAM','too late','late','Late','repack','Repack','REPACK','Repacked','repacked','REPACKED','faulty','Faulty', | |
| 'FAULT','FAULTY','damaged','Damaged','Not Working','not working','Not working','non pta','Non PTA', | |
| 'NON PTA','complaint','Complain','COMPLAINT','kam nhi','Kam ni','work ni','chal ni','Chal nhi', | |
| 'CHAL Ni','Kharab','Khrab','Kharb'] | |
| ignore_keywords = [] | |
| # --- ACCEPT CONDITIONS --- | |
| conditions_accepted_1 = ( | |
| ((data['description'].astype(str).str.len() == 0) & (data['rating'].astype(str).str.contains('4|5'))) | | |
| ((data['description'].astype(str).str.contains('|'.join(Positive_Strings), case=False)) & | |
| (data['rating'].astype(str).str.contains('4|5')) & | |
| (data['LLM'].str.contains('positive|neutral', case=False))) | |
| ) | |
| conditions_accepted_2 = ( | |
| (data['LLM'].str.contains('positive', case=False)) & | |
| (data['rating'].astype(str).str.contains('3')) & | |
| (data['description'].astype(str).str.contains('|'.join(Positive_Strings), case=False)) | |
| ) | |
| conditions_accepted_3 = ( | |
| (data['description'].astype(str).str.len() == 0) & | |
| (data['rating'].astype(str).str.contains('3')) | |
| ) | |
| conditions_accepted_4 = ( | |
| (data['LLM'].str.contains('positive', case=False)) & | |
| (data['rating'].astype(str).str.contains('4|5')) | |
| ) | |
| conditions_accepted_positive_any_rating = data['LLM'].str.contains('positive', case=False) | |
| description_null = data['description'].astype(str).str.strip().str.lower() == "null" | |
| conditions_accepted_null = description_null & data['rating'].astype(str).str.contains('3|4|5') | |
| conditions_accepted_neutral_with_positive_keywords = ( | |
| data['LLM'].str.contains('neutral', case=False) & | |
| data['description'].str.contains('|'.join(Positive_Strings), case=False) | |
| ) | |
| # --- REJECT CONDITIONS --- | |
| conditions_rejected_1 = (data['rating'].astype(str).str.contains('1|2')) & (data['description'].astype(str).str.len() == 0) | |
| conditions_rejected_2 = (data['description'].astype(str).str.contains('|'.join(Negative_Strings), case=False)) & \ | |
| (data['rating'].astype(str).str.contains('1|2|3|4|5')) & \ | |
| (data['LLM'].str.contains('positive|negative|neutral', case=False)) | |
| conditions_rejected_3 = (data['LLM'].str.contains('negative', case=False)) & \ | |
| (data['description'].astype(str).str.contains('|'.join(Negative_Strings), case=False)) | |
| conditions_rejected_4 = ( (data['LLM'].str.contains('neutral', case=False)) & (data['rating'].astype(str).str.contains('1|2')) & ~(data['description'].str.contains('|'.join(Positive_Strings), case=False)) ) | |
| conditions_rejected_5 = (data['LLM'].str.contains('negative', case=False)) & ~(data['description'].astype(str).str.contains('|'.join(Negative_Strings), case=False)) | |
| conditions_rejected_null = description_null & data['rating'].astype(str).str.contains('1|2') | |
| conditions_rejected_neutral_negative_words = (data['LLM'].str.contains('neutral')) & (data['description'].str.contains('|'.join(Negative_Strings), case=False)) | |
| # Final flags | |
| conditions_accepted = conditions_accepted_1 | conditions_accepted_2 | conditions_accepted_3 | \ | |
| conditions_accepted_4 | \ | |
| conditions_accepted_positive_any_rating | conditions_accepted_null |conditions_accepted_neutral_with_positive_keywords | |
| conditions_rejected = conditions_rejected_1 | conditions_rejected_2 | conditions_rejected_3 | \ | |
| conditions_rejected_4 | conditions_rejected_5 | conditions_rejected_null |conditions_rejected_neutral_negative_words | |
| conditions_ignored = ((~(conditions_rejected | conditions_accepted)) | | |
| (data['description'].astype(str).str.contains('|'.join(ignore_keywords), case=False))) | |
| data['Labeled Result'] = np.select( | |
| [conditions_rejected,conditions_accepted, conditions_ignored], | |
| ['rejected','accepted', 'ignored'], | |
| default='ignored' | |
| ) | |
| # --- IMAGE-BASED OVERRIDES --- | |
| if image_flag == "unsafe": | |
| data['Labeled Result'] = "rejected" | |
| LLM = "negative" | |
| elif image_flag == "safe" and LLM == "negative": | |
| data['Labeled Result'] = "rejected" | |
| return data['Labeled Result'][0], LLM, image_flag | |
| # ---------------- GRADIO INTERFACE ---------------- # | |
| def classify_review(description, rating, images): | |
| image_paths = [img.name for img in images] if images else None | |
| result, sentiment, image_flag = validate_review(description, rating, image_paths) | |
| return f"Sentiment: {sentiment}\nImage Safety: {image_flag}\nDecision: {result}" | |
| iface = gr.Interface( | |
| fn=classify_review, | |
| inputs=[ | |
| gr.Textbox(label="Review Description"), | |
| gr.Radio(["1", "2", "3", "4", "5"], label="Rating"), | |
| gr.Files(label="Upload Images (optional)") | |
| ], | |
| outputs="text", | |
| title="Priceoye Review Classifier (with Image Moderation)", | |
| description="Classifies reviews as accepted/rejected/ignored using LLM + rule logic + image safety check." | |
| ) | |
| if __name__ == "__main__": | |
| iface.launch() |