SenseShift-large / senseshift /text_utils.py
shawhed's picture
Upload folder using huggingface_hub
9d135a2 verified
Raw
History Blame Contribute Delete
3.3 kB
"""Sentence segmentation, VADER scoring and output cleanup.
Self-contained copies of the helpers the research repo keeps in
``generate_utils.py``, so the released package does not depend on it.
"""
from __future__ import annotations
import re
from functools import lru_cache
from typing import List, Sequence, Tuple
# The control vocabulary the model was trained with: 21 tokens on a 0.1 grid.
SENTIMENT_GRID: Tuple[float, ...] = tuple(round(i / 10, 1) + 0.0 for i in range(-10, 11))
def sentiment_token(value: float) -> str:
"""Map a sentiment value to the special token the model expects."""
return f"[{snap_to_grid(value)}]"
def snap_to_grid(value: float) -> float:
"""Clamp to [-1, 1] and round to the nearest 0.1 (never returns -0.0)."""
value = float(value)
if value != value: # NaN
raise ValueError("sentiment must be a real number, got NaN")
value = max(-1.0, min(1.0, value))
return round(value, 1) + 0.0
@lru_cache(maxsize=1)
def _analyzer():
import nltk
from nltk.sentiment import SentimentIntensityAnalyzer
try:
nltk.data.find("sentiment/vader_lexicon.zip")
except LookupError:
nltk.download("vader_lexicon", quiet=True)
return SentimentIntensityAnalyzer()
def split_sentences(text: str) -> List[str]:
parts = re.split(r"(?<=[.!?])\s+", text.strip())
return [p.strip() for p in parts if p.strip()]
def score_sentence(sentence: str) -> float:
return snap_to_grid(_analyzer().polarity_scores(sentence)["compound"])
def compute_vader_sentiment(text: str) -> Tuple[List[str], List[float], float]:
"""Return (sentences, per-sentence sentiment on the 0.1 grid, overall)."""
sentences = split_sentences(text)
sentiments = [score_sentence(s) for s in sentences]
overall = snap_to_grid(_analyzer().polarity_scores(text)["compound"])
return sentences, sentiments, overall
def strip_sentiment_marker(text: str) -> str:
"""Drop a leading ``[0.3]`` style control token."""
return re.sub(r"^\s*\[[+-]?\d+(\.\d+)?\]\s*", "", text)
def clean_generated_text(text: str) -> str:
"""Detokenisation cleanup for text decoded out of the MLM."""
cleaned = re.sub(r"\b(\w+)\s+##(\w+)", r"\1\2", text)
cleaned = re.sub(r"<[^>]*>", "", cleaned)
cleaned = re.sub(r"\[[+-]?\d+(\.\d+)?\]", " ", cleaned)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
parts = [p.strip() for p in re.split(r"\s{2,}", cleaned) if p.strip()]
if not parts:
return cleaned
result_parts = []
for p in parts:
if p and p[-1] not in ".!?":
p += "."
result_parts.append(p)
out = " ".join(result_parts)
out = re.sub(r"\s+", " ", out).strip()
out = re.sub(r"\s+([,.;:!?])", r"\1", out)
out = re.sub(r"\s*'\s*", "'", out)
out = re.sub(r"\.\.+", ".", out)
out = re.sub(r'"', "", out)
return out
def choose_random_sentiment(exclude: float | None = None, rng=None) -> float:
"""Pick a grid value, optionally excluding the current one."""
import random as _random
rng = rng or _random
options: Sequence[float] = SENTIMENT_GRID
if exclude is not None:
exclude = snap_to_grid(exclude)
options = [v for v in SENTIMENT_GRID if v != exclude]
return float(rng.choice(list(options)))