import easyocr import numpy as np # create a reader once ocr_reader = easyocr.Reader(["en"], gpu=False) import gradio as gr import torch from transformers import pipeline as hf_pipeline, AutoModelForSequenceClassification, AutoTokenizer from PIL import Image import io # ——— 1) Emotion Pipeline ———————————————————————————————————————————————— emotion_pipeline = hf_pipeline( "text-classification", model="j-hartmann/emotion-english-distilroberta-base", top_k=None, truncation=True ) def get_emotion_profile(text): """ Returns a dict of emotion scores for the input text. """ results = emotion_pipeline(text) # Some pipelines return a list of lists if isinstance(results, list) and isinstance(results[0], list): results = results[0] return {r["label"].lower(): round(r["score"], 3) for r in results} # apology keywords for pleading concern APOLOGY_KEYWORDS = ["sorry", "apolog", "forgive"] # ——— 2) Abuse-Patterns Model —————————————————————————————————————————————— model_name = "SamanthaStorm/tether-multilabel-v3" model = AutoModelForSequenceClassification.from_pretrained(model_name) tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False) LABELS = [ "blame shifting", "contradictory statements", "control", "dismissiveness", "gaslighting", "guilt tripping", "insults", "obscure language", "projection", "recovery phase", "threat" ] THRESHOLDS = { "blame shifting": 0.28, "contradictory statements": 0.27, "control": 0.08, "dismissiveness": 0.32, "gaslighting": 0.27, "guilt tripping": 0.31, "insults": 0.10, "obscure language": 0.55, "projection": 0.09, "recovery phase": 0.33, "threat": 0.15 } # ——— 3) Emotional-Tone Tagging ————————————————————————————————————————————— def get_emotional_tone_tag(emotion_profile, patterns, text_lower): """ Assigns one of 18 nuanced tone categories based on emotion scores, patterns, and text. """ sadness = emotion_profile.get("sadness", 0) joy = emotion_profile.get("joy", 0) neutral = emotion_profile.get("neutral", 0) disgust = emotion_profile.get("disgust", 0) anger = emotion_profile.get("anger", 0) fear = emotion_profile.get("fear", 0) surprise = emotion_profile.get("surprise", 0) def get_emotional_tone_tag(emotion_profile, patterns, text_lower): if "support" in text_lower or "hope" in text_lower or "grace" in text_lower: return "supportive" if ( sadness > 0.4 and any(p in patterns for p in ["blame shifting", "guilt tripping", "recovery phase"]) ): return "performative regret" # 2. Coercive Warmth if ( (joy > 0.3 or sadness > 0.4) and any(p in patterns for p in ["control", "gaslighting"]) ): return "coercive warmth" # 3. Cold Invalidation if ( (neutral + disgust) > 0.5 and any(p in patterns for p in ["dismissiveness", "projection", "obscure language"]) ): return "cold invalidation" # 4. Genuine Vulnerability if ( (sadness + fear) > 0.5 and all(p == "recovery phase" for p in patterns) ): return "genuine vulnerability" # 5. Emotional Threat if ( (anger + disgust) > 0.5 and any(p in patterns for p in ["control", "threat", "insults", "dismissiveness"]) ): return "emotional threat" # 6. Weaponized Sadness if ( sadness > 0.6 and any(p in patterns for p in ["guilt tripping", "projection"]) ): return "weaponized sadness" # 7. Toxic Resignation if ( neutral > 0.5 and any(p in patterns for p in ["dismissiveness", "obscure language"]) ): return "toxic resignation" # 8. Indignant Reproach if ( anger > 0.5 and any(p in patterns for p in ["guilt tripping", "contradictory statements"]) ): return "indignant reproach" # 9. Confrontational if anger > 0.6 and patterns: return "confrontational" # 10. Passive Aggression if ( neutral > 0.6 and any(p in patterns for p in ["dismissiveness", "projection"]) ): return "passive aggression" # 11. Sarcastic Mockery if joy > 0.3 and "insults" in patterns: return "sarcastic mockery" # 12. Menacing Threat if fear > 0.3 and "threat" in patterns: return "menacing threat" # 13. Pleading Concern if ( sadness > 0.3 and any(k in text_lower for k in APOLOGY_KEYWORDS) and not patterns ): return "pleading concern" # 14. Fear-mongering if (fear + disgust) > 0.5 and "projection" in patterns: return "fear-mongering" # 15. Disbelieving Accusation if surprise > 0.3 and "blame shifting" in patterns: return "disbelieving accusation" # 16. Empathetic Solidarity if joy > 0.2 and sadness > 0.2 and not patterns: return "empathetic solidarity" # 17. Assertive Boundary if anger > 0.4 and "control" in patterns: return "assertive boundary" # 18. Stonewalling if neutral > 0.7 and not patterns: return "stonewalling" return None # ——— 4) Single-message Analysis ——————————————————————————————————————————— def analyze_message(text): text_lower = text.lower() # 1) Emotion emotion_profile = get_emotion_profile(text) # 2) Patterns toks = tokenizer(text, return_tensors="pt", truncation=True, padding=True) with torch.no_grad(): logits = model(**toks).logits.squeeze(0) scores = torch.sigmoid(logits).cpu().numpy() active_patterns = [lab for lab, sc in zip(LABELS, scores) if sc >= THRESHOLDS[lab]] # append recovery-phase if apology if any(k in text_lower for k in APOLOGY_KEYWORDS) and "recovery phase" not in active_patterns: active_patterns.append("recovery phase") # 3) Tone tone_tag = get_emotional_tone_tag(emotion_profile, active_patterns, text_lower) return { "emotion_profile": emotion_profile, "active_patterns": active_patterns, "tone_tag": tone_tag } # ——— 5) Composite Wrapper ———————————————————————————————————————————————— def analyze_composite(uploaded_file, *texts): outputs = [] # 1) Handle an uploaded file if uploaded_file is not None: # uploaded_file may be a file-like object or just a path string try: raw = uploaded_file.read() except Exception: # fall back to opening the path with open(uploaded_file, "rb") as f: raw = f.read() # get the filename (or just lowercase the string if it's a path) name = ( uploaded_file.name.lower() if hasattr(uploaded_file, "name") else uploaded_file.lower() ) if name.endswith((".png",".jpg",".jpeg",".tiff",".bmp",".gif")): img = Image.open(io.BytesIO(raw)) arr = np.array(img.convert("RGB")) texts_ocr = ocr_reader.readtext(arr, detail=0) content = "\n".join(texts_ocr) else: try: content = raw.decode("utf-8") except UnicodeDecodeError: content = raw.decode("latin-1") r = analyze_message(content) outputs.append( "── Uploaded File ──\n" f"Emotion Profile : {r['emotion_profile']}\n" f"Active Patterns : {r['active_patterns']}\n" f"Emotional Tone : {r['tone_tag']}\n" ) for idx, txt in enumerate(texts, start=1): if not txt: continue r = analyze_message(txt) outputs.append( f"── Message {idx} ──\n" f"Emotion Profile : {r['emotion_profile']}\n" f"Active Patterns : {r['active_patterns']}\n" f"Emotional Tone : {r['tone_tag']}\n" ) if not outputs: return "Please enter at least one message." return "\n".join(outputs) # ——— 7) Gradio interface ——————————————————————————————————————————————— message_inputs = [gr.Textbox(label="Message")] iface = gr.Interface( fn=analyze_composite, inputs=[gr.File(file_types=[".txt",".png",".jpg",".jpeg"], label="Upload text or image")] + ] + message_inputs, outputs=gr.Textbox(label="Analysis"), title="Tether Analyzer (extended tone tags)", description="Emotion profiling, pattern tags, and a wide set of nuanced tone categories—no abuse score or DARVO." ) if __name__ == "__main__": iface.launch()