vajeeda's picture
feat(phase9): platform reward divergence — PlatformRegistry, R9 PlatformPacing, R1/R2 platform-aware, 20 tests PASS, gate PASS
998d987
Raw
History Blame Contribute Delete
3.56 kB
import hashlib
import re
from dataclasses import dataclass
from typing import Dict, Optional
import numpy as np
from viral_script_engine.rewards.base import BaseReward
from viral_script_engine.platforms.platform_spec import PlatformRegistry
@dataclass
class CoherenceRewardResult:
score: float
raw_similarity: float
interpretation: str
def _tokenize(text: str):
return re.findall(r"\b\w+\b", text.lower())
def _tfidf_vector(tokens: list, vocab: Dict[str, int]) -> np.ndarray:
vec = np.zeros(len(vocab), dtype=np.float32)
for t in tokens:
if t in vocab:
vec[vocab[t]] += 1
total = max(len(tokens), 1)
return vec / total
def _cosine(a: np.ndarray, b: np.ndarray) -> float:
n1 = np.linalg.norm(a)
n2 = np.linalg.norm(b)
if n1 == 0 or n2 == 0:
return 0.0 if n1 != n2 else 1.0
return float(np.dot(a, b) / (n1 * n2))
class CoherenceReward(BaseReward):
_cache: dict = {}
def __init__(self):
self._st_model: Optional[object] = None
self._use_st: Optional[bool] = None
self.platform_registry = PlatformRegistry()
def _try_load_st(self) -> bool:
if self._use_st is not None:
return self._use_st
try:
from sentence_transformers import SentenceTransformer # noqa: F401
self._st_model = SentenceTransformer("all-MiniLM-L6-v2")
self._use_st = True
except Exception:
self._use_st = False
return self._use_st
def _embed_st(self, text: str) -> "torch.Tensor":
key = hashlib.sha256(text.encode()).hexdigest()
if key not in self._cache:
self._cache[key] = self._st_model.encode(text, convert_to_tensor=True)
return self._cache[key]
def _cosine_st(self, a, b) -> float:
import torch
import torch.nn.functional as F
a = a.unsqueeze(0) if a.dim() == 1 else a
b = b.unsqueeze(0) if b.dim() == 1 else b
return float(F.cosine_similarity(a, b))
def _similarity(self, text1: str, text2: str) -> float:
if self._try_load_st():
return self._cosine_st(self._embed_st(text1), self._embed_st(text2))
t1 = _tokenize(text1)
t2 = _tokenize(text2)
vocab = {w: i for i, w in enumerate(set(t1 + t2))}
return _cosine(_tfidf_vector(t1, vocab), _tfidf_vector(t2, vocab))
def score(self, original: str, rewritten: str, platform: str = "Reels") -> CoherenceRewardResult:
spec = self.platform_registry.get(platform)
sim = self._similarity(original, rewritten)
if sim > 0.95:
score, interpretation = 0.8, "barely_changed"
elif sim >= 0.80:
score = 0.5 + (sim - 0.80) / 0.15 * 0.5
interpretation = "good_coherence"
elif sim >= 0.65:
score = (sim - 0.65) / 0.15 * 0.5
interpretation = "moderate_drift"
else:
score, interpretation = 0.0, "drifted_too_far"
# Platform length penalty: too long for the platform hurts coherence score
word_count = len(rewritten.split())
if word_count > spec.max_script_length_words:
length_penalty = min(
0.3,
(word_count - spec.max_script_length_words) / spec.max_script_length_words,
)
else:
length_penalty = 0.0
final_score = max(0.0, min(1.0, score - length_penalty))
return CoherenceRewardResult(score=final_score, raw_similarity=sim, interpretation=interpretation)