Spaces:
Sleeping
Sleeping
Phase 6 implemented
Browse files- docs/progress.md +12 -2
- session/phase-log.md +1 -0
- viral_script_engine/agents/moderation_agent.py +114 -0
- viral_script_engine/agents/originality_agent.py +117 -0
- viral_script_engine/data/shadowban_triggers.json +134 -0
- viral_script_engine/data/viral_templates.json +102 -0
- viral_script_engine/environment/env.py +34 -0
- viral_script_engine/environment/observations.py +10 -1
- viral_script_engine/rewards/r6_safety.py +52 -0
- viral_script_engine/rewards/r7_originality.py +46 -0
- viral_script_engine/rewards/reward_aggregator.py +21 -0
- viral_script_engine/scripts/run_dummy_episode.py +67 -16
- viral_script_engine/tests/test_phase6.py +318 -0
docs/progress.md
CHANGED
|
@@ -67,8 +67,18 @@ Do not read entire codebase to understand progress β read this file.
|
|
| 67 |
β
r5_defender_preservation.py β rewritten to TF-IDF cosine sim (pyarrow DLL workaround)
|
| 68 |
β
Phase 5 gate β submission_check 10/10 PASS, demo runs end-to-end
|
| 69 |
|
| 70 |
-
## Phase 6 β
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
## Phase 7 β [Pending]
|
| 74 |
β³ [feature name] β [one line description]
|
|
|
|
| 67 |
β
r5_defender_preservation.py β rewritten to TF-IDF cosine sim (pyarrow DLL workaround)
|
| 68 |
β
Phase 5 gate β submission_check 10/10 PASS, demo runs end-to-end
|
| 69 |
|
| 70 |
+
## Phase 6 β Moderation Agent + Originality Agent
|
| 71 |
+
β
ModerationAgent β zero-LLM rule-based shadowban detection, 6 categories, severity mapping
|
| 72 |
+
β
OriginalityAgent β zero-LLM fuzzy template matching, difflib SequenceMatcher at 0.75 threshold
|
| 73 |
+
β
SafetyReward (R6) β hard zero on high-severity, tiered scoring for medium/low/clean
|
| 74 |
+
β
OriginalityReward (R7) β cliff at 0.4, continuous scoring above
|
| 75 |
+
β
data/shadowban_triggers.json β 20+ entries per 6 categories
|
| 76 |
+
β
data/viral_templates.json β 20+ entries per 4 categories (hooks, structures, CTAs, transitions)
|
| 77 |
+
β
observations.py β R6/R7 fields in RewardComponents, moderation/originality outputs in DebateRound
|
| 78 |
+
β
env.py β ModerationAgent + OriginalityAgent wired into reset() and step()
|
| 79 |
+
β
reward_aggregator.py β new weights (R6: 0.10, R7: 0.10), R6 hard-zero fires before catastrophic drop check
|
| 80 |
+
β
test_phase6.py β 16 tests, all passing
|
| 81 |
+
β
Phase 6 gate β PHASE 6 GATE: PASS, R6+R7 active, 7 total reward components
|
| 82 |
|
| 83 |
## Phase 7 β [Pending]
|
| 84 |
β³ [feature name] β [one line description]
|
session/phase-log.md
CHANGED
|
@@ -24,6 +24,7 @@ ROLLED BACK β changes reverted, reason in line
|
|
| 24 |
[2026-04-26] [Phase 3] COMPLETE β curriculum tiers, GRPO pipeline, rollout fn, dry-run gate PASS
|
| 25 |
[2026-04-26] [Phase 4] COMPLETE β DifficultyTracker, CriticEscalationEngine, env wiring, 6 tests pass, gate PASS
|
| 26 |
[2026-04-26] [Phase 5] COMPLETE β HF deploy infra, demo, README, submission_check 10/10 PASS, demo end-to-end ok
|
|
|
|
| 27 |
|
| 28 |
---
|
| 29 |
|
|
|
|
| 24 |
[2026-04-26] [Phase 3] COMPLETE β curriculum tiers, GRPO pipeline, rollout fn, dry-run gate PASS
|
| 25 |
[2026-04-26] [Phase 4] COMPLETE β DifficultyTracker, CriticEscalationEngine, env wiring, 6 tests pass, gate PASS
|
| 26 |
[2026-04-26] [Phase 5] COMPLETE β HF deploy infra, demo, README, submission_check 10/10 PASS, demo end-to-end ok
|
| 27 |
+
[2026-04-26] [Phase 6] COMPLETE β ModerationAgent, OriginalityAgent, R6/R7 rewards, 16 tests PASS, gate PASS
|
| 28 |
|
| 29 |
---
|
| 30 |
|
viral_script_engine/agents/moderation_agent.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Dict, List
|
| 5 |
+
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ModerationFlag(BaseModel):
|
| 10 |
+
category: str
|
| 11 |
+
trigger_phrase: str
|
| 12 |
+
position: str
|
| 13 |
+
severity: str
|
| 14 |
+
suggestion: str
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ModerationOutput(BaseModel):
|
| 18 |
+
flags: List[ModerationFlag]
|
| 19 |
+
is_safe: bool
|
| 20 |
+
overall_risk: str
|
| 21 |
+
total_flags: int
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
_CATEGORY_LABEL_MAP = {
|
| 25 |
+
"hate_speech_patterns": "hate_speech",
|
| 26 |
+
"misleading_health_claims": "misleading_health",
|
| 27 |
+
"copyright_bait_phrases": "copyright_bait",
|
| 28 |
+
"engagement_bait": "engagement_bait",
|
| 29 |
+
"spam_signals": "spam",
|
| 30 |
+
"platform_policy_violations": "policy_violation",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
_SEVERITY_MAP = {
|
| 34 |
+
"hate_speech_patterns": "high",
|
| 35 |
+
"misleading_health_claims": "high",
|
| 36 |
+
"copyright_bait_phrases": "medium",
|
| 37 |
+
"engagement_bait": "low",
|
| 38 |
+
"spam_signals": "medium",
|
| 39 |
+
"platform_policy_violations": "high",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
_SUGGESTIONS = {
|
| 43 |
+
"hate_speech_patterns": "Remove or replace with respectful, inclusive language.",
|
| 44 |
+
"misleading_health_claims": "Replace with evidence-based language; avoid absolute health guarantees.",
|
| 45 |
+
"copyright_bait_phrases": "Remove references to free/leaked content to avoid DMCA flags.",
|
| 46 |
+
"engagement_bait": "Replace with a genuine question or value-based CTA.",
|
| 47 |
+
"spam_signals": "Remove external link bait; focus on in-app value delivery.",
|
| 48 |
+
"platform_policy_violations": "Remove policy-violating claims; keep messaging compliant.",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _split_script(script: str) -> Dict[str, str]:
|
| 53 |
+
sentences = re.split(r'(?<=[.!?])\s+', script.strip())
|
| 54 |
+
if len(sentences) <= 5:
|
| 55 |
+
return {"hook": script, "body": "", "cta": ""}
|
| 56 |
+
hook = " ".join(sentences[:3])
|
| 57 |
+
cta = " ".join(sentences[-2:])
|
| 58 |
+
body = " ".join(sentences[3:-2])
|
| 59 |
+
return {"hook": hook, "body": body, "cta": cta}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class ModerationAgent:
|
| 63 |
+
"""
|
| 64 |
+
Checks scripts for content that would get flagged or shadowbanned on Reels.
|
| 65 |
+
Zero LLM calls β purely rule-based against shadowban_triggers.json.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
def __init__(self, kb_path: str = "data/shadowban_triggers.json"):
|
| 69 |
+
resolved = Path(kb_path)
|
| 70 |
+
if not resolved.is_absolute():
|
| 71 |
+
resolved = Path(__file__).parent.parent / kb_path
|
| 72 |
+
with open(resolved) as f:
|
| 73 |
+
self._kb: Dict[str, List[str]] = json.load(f)
|
| 74 |
+
|
| 75 |
+
def check(self, script: str) -> ModerationOutput:
|
| 76 |
+
sections = _split_script(script)
|
| 77 |
+
flags: List[ModerationFlag] = []
|
| 78 |
+
|
| 79 |
+
for category, triggers in self._kb.items():
|
| 80 |
+
severity = _SEVERITY_MAP.get(category, "low")
|
| 81 |
+
label = _CATEGORY_LABEL_MAP.get(category, category)
|
| 82 |
+
suggestion = _SUGGESTIONS.get(category, "Review and revise this content.")
|
| 83 |
+
for position, text in sections.items():
|
| 84 |
+
if not text:
|
| 85 |
+
continue
|
| 86 |
+
text_lower = text.lower()
|
| 87 |
+
for trigger in triggers:
|
| 88 |
+
if trigger in text_lower:
|
| 89 |
+
flags.append(ModerationFlag(
|
| 90 |
+
category=label,
|
| 91 |
+
trigger_phrase=trigger,
|
| 92 |
+
position=position,
|
| 93 |
+
severity=severity,
|
| 94 |
+
suggestion=suggestion,
|
| 95 |
+
))
|
| 96 |
+
|
| 97 |
+
has_high = any(f.severity == "high" for f in flags)
|
| 98 |
+
has_medium = any(f.severity == "medium" for f in flags)
|
| 99 |
+
|
| 100 |
+
if has_high:
|
| 101 |
+
overall_risk = "high_risk"
|
| 102 |
+
elif has_medium:
|
| 103 |
+
overall_risk = "medium_risk"
|
| 104 |
+
elif flags:
|
| 105 |
+
overall_risk = "low_risk"
|
| 106 |
+
else:
|
| 107 |
+
overall_risk = "safe"
|
| 108 |
+
|
| 109 |
+
return ModerationOutput(
|
| 110 |
+
flags=flags,
|
| 111 |
+
is_safe=not has_high,
|
| 112 |
+
overall_risk=overall_risk,
|
| 113 |
+
total_flags=len(flags),
|
| 114 |
+
)
|
viral_script_engine/agents/originality_agent.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
from difflib import SequenceMatcher
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class OriginalityFlag(BaseModel):
|
| 11 |
+
template_type: str
|
| 12 |
+
matched_pattern: str
|
| 13 |
+
script_excerpt: str
|
| 14 |
+
suggestion: str
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class OriginalityOutput(BaseModel):
|
| 18 |
+
flags: List[OriginalityFlag]
|
| 19 |
+
originality_score: float
|
| 20 |
+
is_generic: bool
|
| 21 |
+
unique_elements: List[str]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
_TEMPLATE_TYPE_MAP = {
|
| 25 |
+
"overused_hooks": "overused_hook",
|
| 26 |
+
"overused_structures": "overused_structure",
|
| 27 |
+
"overused_cta_phrases": "overused_cta",
|
| 28 |
+
"overused_transitions": "overused_transition",
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
_SUGGESTIONS = {
|
| 32 |
+
"overused_hook": "Rewrite the hook with a specific data point, personal story, or unexpected angle.",
|
| 33 |
+
"overused_structure": "Try an unconventional narrative arc β start mid-story or end with the question.",
|
| 34 |
+
"overused_cta": "Replace with a specific, contextual call-to-action tied to the video's content.",
|
| 35 |
+
"overused_transition": "Cut the transition filler and jump directly to the next point.",
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
_FUZZY_THRESHOLD = 0.75
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _split_script(script: str) -> Dict[str, str]:
|
| 42 |
+
sentences = re.split(r'(?<=[.!?])\s+', script.strip())
|
| 43 |
+
if len(sentences) <= 5:
|
| 44 |
+
return {"hook": script, "body": "", "cta": ""}
|
| 45 |
+
hook = " ".join(sentences[:3])
|
| 46 |
+
cta = " ".join(sentences[-2:])
|
| 47 |
+
body = " ".join(sentences[3:-2])
|
| 48 |
+
return {"hook": hook, "body": body, "cta": cta}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _fuzzy_match(text: str, pattern: str) -> bool:
|
| 52 |
+
text_lower = text.lower()
|
| 53 |
+
pattern_lower = pattern.lower()
|
| 54 |
+
if pattern_lower in text_lower:
|
| 55 |
+
return True
|
| 56 |
+
ratio = SequenceMatcher(None, text_lower, pattern_lower).ratio()
|
| 57 |
+
return ratio >= _FUZZY_THRESHOLD
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class OriginalityAgent:
|
| 61 |
+
"""
|
| 62 |
+
Measures how distinct the script sounds compared to overused Reels formats.
|
| 63 |
+
Zero LLM calls β fuzzy string matching against viral_templates.json.
|
| 64 |
+
Uses difflib.SequenceMatcher (threshold: 0.75 similarity).
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def __init__(self, templates_path: str = "data/viral_templates.json"):
|
| 68 |
+
resolved = Path(templates_path)
|
| 69 |
+
if not resolved.is_absolute():
|
| 70 |
+
resolved = Path(__file__).parent.parent / templates_path
|
| 71 |
+
with open(resolved) as f:
|
| 72 |
+
self._templates: Dict[str, List[str]] = json.load(f)
|
| 73 |
+
|
| 74 |
+
def check(self, script: str) -> OriginalityOutput:
|
| 75 |
+
sections = _split_script(script)
|
| 76 |
+
flags: List[OriginalityFlag] = []
|
| 77 |
+
matched_sections = set()
|
| 78 |
+
|
| 79 |
+
for category, patterns in self._templates.items():
|
| 80 |
+
template_type = _TEMPLATE_TYPE_MAP.get(category, category)
|
| 81 |
+
suggestion = _SUGGESTIONS.get(template_type, "Make this section more original.")
|
| 82 |
+
for pos, text in sections.items():
|
| 83 |
+
if not text:
|
| 84 |
+
continue
|
| 85 |
+
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
|
| 86 |
+
for sentence in sentences:
|
| 87 |
+
for pattern in patterns:
|
| 88 |
+
if _fuzzy_match(sentence, pattern):
|
| 89 |
+
section_key = f"{pos}:{sentence[:40]}"
|
| 90 |
+
matched_sections.add(section_key)
|
| 91 |
+
flags.append(OriginalityFlag(
|
| 92 |
+
template_type=template_type,
|
| 93 |
+
matched_pattern=pattern,
|
| 94 |
+
script_excerpt=sentence[:80],
|
| 95 |
+
suggestion=suggestion,
|
| 96 |
+
))
|
| 97 |
+
|
| 98 |
+
all_sentences = re.split(r'(?<=[.!?])\s+', script.strip())
|
| 99 |
+
total = max(len(all_sentences), 1)
|
| 100 |
+
matched_count = len(matched_sections)
|
| 101 |
+
originality_score = max(0.0, min(1.0, 1.0 - (matched_count / total)))
|
| 102 |
+
|
| 103 |
+
unique_elements = [
|
| 104 |
+
s for s in all_sentences
|
| 105 |
+
if not any(
|
| 106 |
+
_fuzzy_match(s, pattern)
|
| 107 |
+
for patterns in self._templates.values()
|
| 108 |
+
for pattern in patterns
|
| 109 |
+
)
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
return OriginalityOutput(
|
| 113 |
+
flags=flags,
|
| 114 |
+
originality_score=originality_score,
|
| 115 |
+
is_generic=originality_score < 0.4,
|
| 116 |
+
unique_elements=unique_elements,
|
| 117 |
+
)
|
viral_script_engine/data/shadowban_triggers.json
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"hate_speech_patterns": [
|
| 3 |
+
"go back to your country",
|
| 4 |
+
"sub-human",
|
| 5 |
+
"vermin",
|
| 6 |
+
"parasite",
|
| 7 |
+
"pest",
|
| 8 |
+
"replace the whites",
|
| 9 |
+
"white genocide",
|
| 10 |
+
"great replacement",
|
| 11 |
+
"race traitor",
|
| 12 |
+
"race mixing is wrong",
|
| 13 |
+
"demographic replacement",
|
| 14 |
+
"they control everything",
|
| 15 |
+
"globalist agenda",
|
| 16 |
+
"kalergi plan",
|
| 17 |
+
"blood and soil",
|
| 18 |
+
"14 words",
|
| 19 |
+
"heil",
|
| 20 |
+
"racial purity",
|
| 21 |
+
"ethnic cleansing",
|
| 22 |
+
"gas the"
|
| 23 |
+
],
|
| 24 |
+
"misleading_health_claims": [
|
| 25 |
+
"cure",
|
| 26 |
+
"doctors don't want you to know",
|
| 27 |
+
"guaranteed weight loss",
|
| 28 |
+
"100% natural treatment",
|
| 29 |
+
"miracle remedy",
|
| 30 |
+
"big pharma hiding",
|
| 31 |
+
"instant cure",
|
| 32 |
+
"detox your body in 24 hours",
|
| 33 |
+
"reverses diabetes naturally",
|
| 34 |
+
"shrinks tumors",
|
| 35 |
+
"eliminate cancer",
|
| 36 |
+
"fda banned",
|
| 37 |
+
"secret ingredient doctors hate",
|
| 38 |
+
"heal yourself at home",
|
| 39 |
+
"no side effects guaranteed",
|
| 40 |
+
"clears arteries overnight",
|
| 41 |
+
"secret ancient remedy",
|
| 42 |
+
"boosts immunity by 1000%",
|
| 43 |
+
"prevents all diseases",
|
| 44 |
+
"one weird trick doctors"
|
| 45 |
+
],
|
| 46 |
+
"copyright_bait_phrases": [
|
| 47 |
+
"full movie",
|
| 48 |
+
"free download",
|
| 49 |
+
"watch without ads",
|
| 50 |
+
"leaked footage",
|
| 51 |
+
"full episode free",
|
| 52 |
+
"no copyright",
|
| 53 |
+
"watch full film",
|
| 54 |
+
"download for free",
|
| 55 |
+
"stream free online",
|
| 56 |
+
"bypass paywall",
|
| 57 |
+
"full series free",
|
| 58 |
+
"watch hd without paying",
|
| 59 |
+
"crack version",
|
| 60 |
+
"pirated",
|
| 61 |
+
"torrent link",
|
| 62 |
+
"free streaming no sign up",
|
| 63 |
+
"leaked scene",
|
| 64 |
+
"free premium account",
|
| 65 |
+
"account generator free",
|
| 66 |
+
"watch full length movie"
|
| 67 |
+
],
|
| 68 |
+
"engagement_bait": [
|
| 69 |
+
"comment if you agree",
|
| 70 |
+
"share to save",
|
| 71 |
+
"follow or bad luck",
|
| 72 |
+
"tag 3 friends",
|
| 73 |
+
"double tap if",
|
| 74 |
+
"like if you think",
|
| 75 |
+
"share if you care",
|
| 76 |
+
"type yes if",
|
| 77 |
+
"comment amen",
|
| 78 |
+
"repost to spread awareness",
|
| 79 |
+
"tag someone who needs to see this",
|
| 80 |
+
"like for part 2",
|
| 81 |
+
"comment your answer below",
|
| 82 |
+
"share to your story if you agree",
|
| 83 |
+
"send this to 10 people",
|
| 84 |
+
"type 1 if yes 2 if no",
|
| 85 |
+
"follow to find out",
|
| 86 |
+
"like to support",
|
| 87 |
+
"comment done when you finish",
|
| 88 |
+
"share with every parent you know"
|
| 89 |
+
],
|
| 90 |
+
"spam_signals": [
|
| 91 |
+
"link in bio for free",
|
| 92 |
+
"dm me the word",
|
| 93 |
+
"click the link below for",
|
| 94 |
+
"comment to get access",
|
| 95 |
+
"free gift in my bio",
|
| 96 |
+
"limited time only dm",
|
| 97 |
+
"get yours now in bio",
|
| 98 |
+
"dm for price",
|
| 99 |
+
"free pdf in bio",
|
| 100 |
+
"drop your email below",
|
| 101 |
+
"click link in story",
|
| 102 |
+
"comment your phone number",
|
| 103 |
+
"dm for exclusive access",
|
| 104 |
+
"free course in bio link",
|
| 105 |
+
"type below to get the template",
|
| 106 |
+
"visit my website in bio",
|
| 107 |
+
"dm for the checklist",
|
| 108 |
+
"grab the free guide in link below",
|
| 109 |
+
"swipe up and get it free",
|
| 110 |
+
"first 100 comments get free"
|
| 111 |
+
],
|
| 112 |
+
"platform_policy_violations": [
|
| 113 |
+
"buy followers",
|
| 114 |
+
"get rich quick",
|
| 115 |
+
"make $x in y days guaranteed",
|
| 116 |
+
"buy instagram likes",
|
| 117 |
+
"boost your followers instantly",
|
| 118 |
+
"earn money doing nothing",
|
| 119 |
+
"passive income no effort",
|
| 120 |
+
"100k in 30 days",
|
| 121 |
+
"work from home no experience $500/day",
|
| 122 |
+
"financial freedom in 30 days",
|
| 123 |
+
"easy money from your phone",
|
| 124 |
+
"turn $100 into $10,000",
|
| 125 |
+
"forex signals guaranteed profit",
|
| 126 |
+
"crypto pump group",
|
| 127 |
+
"ponzi scheme",
|
| 128 |
+
"pyramid scheme",
|
| 129 |
+
"make money fast guaranteed",
|
| 130 |
+
"binary options guaranteed",
|
| 131 |
+
"offshore account secret",
|
| 132 |
+
"tax evasion method"
|
| 133 |
+
]
|
| 134 |
+
}
|
viral_script_engine/data/viral_templates.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"overused_hooks": [
|
| 3 |
+
"pov: you finally figured out",
|
| 4 |
+
"nobody talks about this but",
|
| 5 |
+
"things that are actually red flags",
|
| 6 |
+
"tell me you're x without telling me you're x",
|
| 7 |
+
"as someone who has done x for y years",
|
| 8 |
+
"the reason you're not seeing results is",
|
| 9 |
+
"stop doing x immediately",
|
| 10 |
+
"x things i wish i knew before",
|
| 11 |
+
"wait for it",
|
| 12 |
+
"this changed everything for me",
|
| 13 |
+
"i can't believe i didn't know this sooner",
|
| 14 |
+
"if you're struggling with x watch this",
|
| 15 |
+
"the truth about x nobody tells you",
|
| 16 |
+
"i tried x for 30 days and here's what happened",
|
| 17 |
+
"you've been doing x wrong your whole life",
|
| 18 |
+
"why i quit x after y years",
|
| 19 |
+
"the x that changed my life",
|
| 20 |
+
"how i went from x to y in z days",
|
| 21 |
+
"day x of doing y",
|
| 22 |
+
"let me show you something crazy",
|
| 23 |
+
"here's what they don't want you to know",
|
| 24 |
+
"this is your sign to",
|
| 25 |
+
"can we normalize",
|
| 26 |
+
"unpopular opinion but"
|
| 27 |
+
],
|
| 28 |
+
"overused_structures": [
|
| 29 |
+
"hook β 3 numbered tips β cta to follow",
|
| 30 |
+
"controversial take β explanation β agree with me?",
|
| 31 |
+
"before and after β what changed β product mention",
|
| 32 |
+
"problem β agitate β solution",
|
| 33 |
+
"story time β lesson learned β apply it",
|
| 34 |
+
"myth busting β real answer β follow for more",
|
| 35 |
+
"countdown from 5 to 1 β reveal at end",
|
| 36 |
+
"reaction to trending topic β opinion β like if you agree",
|
| 37 |
+
"day in my life β relatable moments β subscribe",
|
| 38 |
+
"this or that comparison β winner announced β share",
|
| 39 |
+
"product review β pros cons β buy link in bio",
|
| 40 |
+
"step 1 step 2 step 3 β results revealed β cta",
|
| 41 |
+
"question hook β answer delayed β follow to find out",
|
| 42 |
+
"challenge accepted β process montage β final result",
|
| 43 |
+
"morning routine β productivity tips β follow for inspo",
|
| 44 |
+
"hot take β justification β engage in comments",
|
| 45 |
+
"transformation reveal β how i did it β coaching plug",
|
| 46 |
+
"ask me anything β batch answers β comment more questions",
|
| 47 |
+
"mistakes i made β lessons β save this post",
|
| 48 |
+
"trend recreation β personal twist β duet with me",
|
| 49 |
+
"storytime gossip β drama recap β give your opinion",
|
| 50 |
+
"meet my x β intro β follow for more content"
|
| 51 |
+
],
|
| 52 |
+
"overused_cta_phrases": [
|
| 53 |
+
"follow for more",
|
| 54 |
+
"save this for later",
|
| 55 |
+
"share with someone who needs this",
|
| 56 |
+
"comment your thoughts below",
|
| 57 |
+
"like if you agree",
|
| 58 |
+
"drop a comment if this helped",
|
| 59 |
+
"hit the follow button",
|
| 60 |
+
"bookmark this",
|
| 61 |
+
"share to your story",
|
| 62 |
+
"tag a friend who needs to hear this",
|
| 63 |
+
"let me know in the comments",
|
| 64 |
+
"smash that like button",
|
| 65 |
+
"turn on notifications",
|
| 66 |
+
"click the link in bio",
|
| 67 |
+
"follow for part 2",
|
| 68 |
+
"share this before it gets removed",
|
| 69 |
+
"comment yes if you want more",
|
| 70 |
+
"send this to your bestie",
|
| 71 |
+
"like and subscribe",
|
| 72 |
+
"save this post it will help you later",
|
| 73 |
+
"follow now don't regret it",
|
| 74 |
+
"repost if you found this useful"
|
| 75 |
+
],
|
| 76 |
+
"overused_transitions": [
|
| 77 |
+
"but wait there's more",
|
| 78 |
+
"and here's the thing",
|
| 79 |
+
"plot twist",
|
| 80 |
+
"but actually",
|
| 81 |
+
"real talk though",
|
| 82 |
+
"here's where it gets interesting",
|
| 83 |
+
"stay with me",
|
| 84 |
+
"now here's the kicker",
|
| 85 |
+
"this is the part nobody talks about",
|
| 86 |
+
"i know what you're thinking",
|
| 87 |
+
"but before i get into that",
|
| 88 |
+
"and this is the important part",
|
| 89 |
+
"okay so",
|
| 90 |
+
"so basically",
|
| 91 |
+
"long story short",
|
| 92 |
+
"here's the truth",
|
| 93 |
+
"i have to be honest",
|
| 94 |
+
"not gonna lie",
|
| 95 |
+
"the crazy part is",
|
| 96 |
+
"fast forward to",
|
| 97 |
+
"spoiler alert",
|
| 98 |
+
"and that's when it hit me",
|
| 99 |
+
"here's what blew my mind",
|
| 100 |
+
"and guess what happened next"
|
| 101 |
+
]
|
| 102 |
+
}
|
viral_script_engine/environment/env.py
CHANGED
|
@@ -11,11 +11,15 @@ from viral_script_engine.environment.episode_state import EpisodeState
|
|
| 11 |
from viral_script_engine.environment.observations import (
|
| 12 |
DebateRound, Observation, RewardComponents,
|
| 13 |
)
|
|
|
|
|
|
|
| 14 |
from viral_script_engine.rewards.r1_hook_strength import HookStrengthReward
|
| 15 |
from viral_script_engine.rewards.r2_coherence import CoherenceReward
|
| 16 |
from viral_script_engine.rewards.r3_cultural_alignment import CulturalAlignmentReward
|
| 17 |
from viral_script_engine.rewards.r4_debate_resolution import DebateResolutionReward
|
| 18 |
from viral_script_engine.rewards.r5_defender_preservation import DefenderPreservationReward
|
|
|
|
|
|
|
| 19 |
from viral_script_engine.rewards.reward_aggregator import RewardAggregator
|
| 20 |
|
| 21 |
_TIERS = {
|
|
@@ -59,6 +63,10 @@ class ViralScriptEnv:
|
|
| 59 |
self.r3 = CulturalAlignmentReward(knowledge_base_path=cultural_kb_path)
|
| 60 |
self.r4 = DebateResolutionReward(critic_agent=self.critic)
|
| 61 |
self.r5 = DefenderPreservationReward()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
self.aggregator = RewardAggregator()
|
| 63 |
self._state: Optional[EpisodeState] = None
|
| 64 |
|
|
@@ -125,10 +133,16 @@ class ViralScriptEnv:
|
|
| 125 |
r1_result = self.r1.score(script["script_text"])
|
| 126 |
r2_result = self.r2.score(script["script_text"], script["script_text"])
|
| 127 |
r3_result = self.r3.score(script["script_text"], script.get("region", "pan_india_english"))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
initial_rewards = RewardComponents(
|
| 129 |
r1_hook_strength=r1_result.score,
|
| 130 |
r2_coherence=r2_result.score,
|
| 131 |
r3_cultural_alignment=r3_result.score,
|
|
|
|
|
|
|
| 132 |
)
|
| 133 |
initial_rewards.compute_total()
|
| 134 |
|
|
@@ -186,12 +200,19 @@ class ViralScriptEnv:
|
|
| 186 |
|
| 187 |
r5_result = self.r5.score(defender_output, new_script)
|
| 188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
components = RewardComponents(
|
| 190 |
r1_hook_strength=r1_result.score,
|
| 191 |
r2_coherence=r2_result.score,
|
| 192 |
r3_cultural_alignment=r3_result.score,
|
| 193 |
r4_debate_resolution=r4_result.score if r4_result else None,
|
| 194 |
r5_defender_preservation=r5_result.score,
|
|
|
|
|
|
|
| 195 |
)
|
| 196 |
|
| 197 |
self._state.action_history.append(arb_action.action_type)
|
|
@@ -222,6 +243,8 @@ class ViralScriptEnv:
|
|
| 222 |
arbitrator_action=arb_action,
|
| 223 |
rewrite_diff=rewrite_result.diff,
|
| 224 |
reward_components=components,
|
|
|
|
|
|
|
| 225 |
)
|
| 226 |
self._state.debate_history.append(round_)
|
| 227 |
self._state.current_script = new_script
|
|
@@ -251,6 +274,8 @@ class ViralScriptEnv:
|
|
| 251 |
"anti_gaming_triggered": anti_log.triggered,
|
| 252 |
"penalty_reason": anti_log.rule_triggered,
|
| 253 |
"anti_gaming_log": anti_log.model_dump(),
|
|
|
|
|
|
|
| 254 |
}
|
| 255 |
return self._build_observation().model_dump(), components.total, terminated, False, info
|
| 256 |
|
|
@@ -278,6 +303,13 @@ class ViralScriptEnv:
|
|
| 278 |
|
| 279 |
def _build_observation(self) -> Observation:
|
| 280 |
s = self._state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
return Observation(
|
| 282 |
current_script=s.current_script,
|
| 283 |
original_script=s.original_script,
|
|
@@ -290,4 +322,6 @@ class ViralScriptEnv:
|
|
| 290 |
reward_components=s.last_reward_components,
|
| 291 |
difficulty_level=s.difficulty_level,
|
| 292 |
episode_id=s.episode_id,
|
|
|
|
|
|
|
| 293 |
)
|
|
|
|
| 11 |
from viral_script_engine.environment.observations import (
|
| 12 |
DebateRound, Observation, RewardComponents,
|
| 13 |
)
|
| 14 |
+
from viral_script_engine.agents.moderation_agent import ModerationAgent
|
| 15 |
+
from viral_script_engine.agents.originality_agent import OriginalityAgent
|
| 16 |
from viral_script_engine.rewards.r1_hook_strength import HookStrengthReward
|
| 17 |
from viral_script_engine.rewards.r2_coherence import CoherenceReward
|
| 18 |
from viral_script_engine.rewards.r3_cultural_alignment import CulturalAlignmentReward
|
| 19 |
from viral_script_engine.rewards.r4_debate_resolution import DebateResolutionReward
|
| 20 |
from viral_script_engine.rewards.r5_defender_preservation import DefenderPreservationReward
|
| 21 |
+
from viral_script_engine.rewards.r6_safety import SafetyReward
|
| 22 |
+
from viral_script_engine.rewards.r7_originality import OriginalityReward
|
| 23 |
from viral_script_engine.rewards.reward_aggregator import RewardAggregator
|
| 24 |
|
| 25 |
_TIERS = {
|
|
|
|
| 63 |
self.r3 = CulturalAlignmentReward(knowledge_base_path=cultural_kb_path)
|
| 64 |
self.r4 = DebateResolutionReward(critic_agent=self.critic)
|
| 65 |
self.r5 = DefenderPreservationReward()
|
| 66 |
+
self.r6 = SafetyReward()
|
| 67 |
+
self.r7 = OriginalityReward()
|
| 68 |
+
self.moderation_agent = ModerationAgent()
|
| 69 |
+
self.originality_agent = OriginalityAgent()
|
| 70 |
self.aggregator = RewardAggregator()
|
| 71 |
self._state: Optional[EpisodeState] = None
|
| 72 |
|
|
|
|
| 133 |
r1_result = self.r1.score(script["script_text"])
|
| 134 |
r2_result = self.r2.score(script["script_text"], script["script_text"])
|
| 135 |
r3_result = self.r3.score(script["script_text"], script.get("region", "pan_india_english"))
|
| 136 |
+
mod_out = self.moderation_agent.check(script["script_text"])
|
| 137 |
+
orig_out = self.originality_agent.check(script["script_text"])
|
| 138 |
+
r6_result = self.r6.score(mod_out)
|
| 139 |
+
r7_result = self.r7.score(orig_out)
|
| 140 |
initial_rewards = RewardComponents(
|
| 141 |
r1_hook_strength=r1_result.score,
|
| 142 |
r2_coherence=r2_result.score,
|
| 143 |
r3_cultural_alignment=r3_result.score,
|
| 144 |
+
r6_safety=r6_result.score,
|
| 145 |
+
r7_originality=r7_result.score,
|
| 146 |
)
|
| 147 |
initial_rewards.compute_total()
|
| 148 |
|
|
|
|
| 200 |
|
| 201 |
r5_result = self.r5.score(defender_output, new_script)
|
| 202 |
|
| 203 |
+
moderation_out = self.moderation_agent.check(new_script)
|
| 204 |
+
originality_out = self.originality_agent.check(new_script)
|
| 205 |
+
r6_result = self.r6.score(moderation_out)
|
| 206 |
+
r7_result = self.r7.score(originality_out)
|
| 207 |
+
|
| 208 |
components = RewardComponents(
|
| 209 |
r1_hook_strength=r1_result.score,
|
| 210 |
r2_coherence=r2_result.score,
|
| 211 |
r3_cultural_alignment=r3_result.score,
|
| 212 |
r4_debate_resolution=r4_result.score if r4_result else None,
|
| 213 |
r5_defender_preservation=r5_result.score,
|
| 214 |
+
r6_safety=r6_result.score,
|
| 215 |
+
r7_originality=r7_result.score,
|
| 216 |
)
|
| 217 |
|
| 218 |
self._state.action_history.append(arb_action.action_type)
|
|
|
|
| 243 |
arbitrator_action=arb_action,
|
| 244 |
rewrite_diff=rewrite_result.diff,
|
| 245 |
reward_components=components,
|
| 246 |
+
moderation_output=moderation_out.model_dump(),
|
| 247 |
+
originality_output=originality_out.model_dump(),
|
| 248 |
)
|
| 249 |
self._state.debate_history.append(round_)
|
| 250 |
self._state.current_script = new_script
|
|
|
|
| 274 |
"anti_gaming_triggered": anti_log.triggered,
|
| 275 |
"penalty_reason": anti_log.rule_triggered,
|
| 276 |
"anti_gaming_log": anti_log.model_dump(),
|
| 277 |
+
"moderation_output": moderation_out.model_dump(),
|
| 278 |
+
"originality_output": originality_out.model_dump(),
|
| 279 |
}
|
| 280 |
return self._build_observation().model_dump(), components.total, terminated, False, info
|
| 281 |
|
|
|
|
| 303 |
|
| 304 |
def _build_observation(self) -> Observation:
|
| 305 |
s = self._state
|
| 306 |
+
last_round = s.debate_history[-1] if s.debate_history else None
|
| 307 |
+
mod_flags = []
|
| 308 |
+
orig_flags = []
|
| 309 |
+
if last_round and last_round.moderation_output:
|
| 310 |
+
mod_flags = last_round.moderation_output.get("flags", [])
|
| 311 |
+
if last_round and last_round.originality_output:
|
| 312 |
+
orig_flags = last_round.originality_output.get("flags", [])
|
| 313 |
return Observation(
|
| 314 |
current_script=s.current_script,
|
| 315 |
original_script=s.original_script,
|
|
|
|
| 322 |
reward_components=s.last_reward_components,
|
| 323 |
difficulty_level=s.difficulty_level,
|
| 324 |
episode_id=s.episode_id,
|
| 325 |
+
current_moderation_flags=mod_flags,
|
| 326 |
+
current_originality_flags=orig_flags,
|
| 327 |
)
|
viral_script_engine/environment/observations.py
CHANGED
|
@@ -6,7 +6,8 @@ from viral_script_engine.agents.critic import CritiqueClaim
|
|
| 6 |
from viral_script_engine.environment.actions import ArbitratorAction
|
| 7 |
|
| 8 |
_WEIGHTS: Dict[str, float] = {
|
| 9 |
-
"r1": 0.
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
|
|
@@ -16,6 +17,8 @@ class RewardComponents(BaseModel):
|
|
| 16 |
r3_cultural_alignment: Optional[float] = None
|
| 17 |
r4_debate_resolution: Optional[float] = None
|
| 18 |
r5_defender_preservation: Optional[float] = None
|
|
|
|
|
|
|
| 19 |
anti_gaming_penalty: float = 0.0
|
| 20 |
total: float = 0.0
|
| 21 |
|
|
@@ -26,6 +29,8 @@ class RewardComponents(BaseModel):
|
|
| 26 |
"r3": self.r3_cultural_alignment,
|
| 27 |
"r4": self.r4_debate_resolution,
|
| 28 |
"r5": self.r5_defender_preservation,
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
active = {k: v for k, v in vals.items() if v is not None}
|
| 31 |
if not active:
|
|
@@ -44,6 +49,8 @@ class DebateRound(BaseModel):
|
|
| 44 |
arbitrator_action: Optional[ArbitratorAction] = None
|
| 45 |
rewrite_diff: Optional[str] = None
|
| 46 |
reward_components: Optional[RewardComponents] = None
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
class Observation(BaseModel):
|
|
@@ -58,3 +65,5 @@ class Observation(BaseModel):
|
|
| 58 |
reward_components: RewardComponents
|
| 59 |
difficulty_level: str
|
| 60 |
episode_id: str
|
|
|
|
|
|
|
|
|
| 6 |
from viral_script_engine.environment.actions import ArbitratorAction
|
| 7 |
|
| 8 |
_WEIGHTS: Dict[str, float] = {
|
| 9 |
+
"r1": 0.20, "r2": 0.15, "r3": 0.15, "r4": 0.15, "r5": 0.15,
|
| 10 |
+
"r6": 0.10, "r7": 0.10,
|
| 11 |
}
|
| 12 |
|
| 13 |
|
|
|
|
| 17 |
r3_cultural_alignment: Optional[float] = None
|
| 18 |
r4_debate_resolution: Optional[float] = None
|
| 19 |
r5_defender_preservation: Optional[float] = None
|
| 20 |
+
r6_safety: Optional[float] = None
|
| 21 |
+
r7_originality: Optional[float] = None
|
| 22 |
anti_gaming_penalty: float = 0.0
|
| 23 |
total: float = 0.0
|
| 24 |
|
|
|
|
| 29 |
"r3": self.r3_cultural_alignment,
|
| 30 |
"r4": self.r4_debate_resolution,
|
| 31 |
"r5": self.r5_defender_preservation,
|
| 32 |
+
"r6": self.r6_safety,
|
| 33 |
+
"r7": self.r7_originality,
|
| 34 |
}
|
| 35 |
active = {k: v for k, v in vals.items() if v is not None}
|
| 36 |
if not active:
|
|
|
|
| 49 |
arbitrator_action: Optional[ArbitratorAction] = None
|
| 50 |
rewrite_diff: Optional[str] = None
|
| 51 |
reward_components: Optional[RewardComponents] = None
|
| 52 |
+
moderation_output: Optional[Any] = None
|
| 53 |
+
originality_output: Optional[Any] = None
|
| 54 |
|
| 55 |
|
| 56 |
class Observation(BaseModel):
|
|
|
|
| 65 |
reward_components: RewardComponents
|
| 66 |
difficulty_level: str
|
| 67 |
episode_id: str
|
| 68 |
+
current_moderation_flags: List[Any] = []
|
| 69 |
+
current_originality_flags: List[Any] = []
|
viral_script_engine/rewards/r6_safety.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
from viral_script_engine.agents.moderation_agent import ModerationOutput
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class SafetyRewardResult(BaseModel):
|
| 7 |
+
score: float
|
| 8 |
+
flag_count: int
|
| 9 |
+
highest_severity: str
|
| 10 |
+
breakdown: str
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SafetyReward:
|
| 14 |
+
"""
|
| 15 |
+
Converts ModerationOutput into a reward signal.
|
| 16 |
+
|
| 17 |
+
Scoring:
|
| 18 |
+
- Zero flags: 1.0
|
| 19 |
+
- Only low-severity flags: 0.8
|
| 20 |
+
- Any medium-severity flag: 0.5
|
| 21 |
+
- Any high-severity flag: 0.0 (hard zero β non-negotiable per platform policy)
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def score(self, moderation_output: ModerationOutput) -> SafetyRewardResult:
|
| 25 |
+
if moderation_output.total_flags == 0:
|
| 26 |
+
return SafetyRewardResult(
|
| 27 |
+
score=1.0,
|
| 28 |
+
flag_count=0,
|
| 29 |
+
highest_severity="none",
|
| 30 |
+
breakdown="No flags detected.",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
severities = [f.severity for f in moderation_output.flags]
|
| 34 |
+
if "high" in severities:
|
| 35 |
+
highest = "high"
|
| 36 |
+
reward = 0.0
|
| 37 |
+
breakdown = "Hard zero: high-severity flag present (platform policy violation)."
|
| 38 |
+
elif "medium" in severities:
|
| 39 |
+
highest = "medium"
|
| 40 |
+
reward = 0.5
|
| 41 |
+
breakdown = f"Medium-severity flags detected ({moderation_output.total_flags} total)."
|
| 42 |
+
else:
|
| 43 |
+
highest = "low"
|
| 44 |
+
reward = 0.8
|
| 45 |
+
breakdown = f"Only low-severity flags detected ({moderation_output.total_flags} total)."
|
| 46 |
+
|
| 47 |
+
return SafetyRewardResult(
|
| 48 |
+
score=reward,
|
| 49 |
+
flag_count=moderation_output.total_flags,
|
| 50 |
+
highest_severity=highest,
|
| 51 |
+
breakdown=breakdown,
|
| 52 |
+
)
|
viral_script_engine/rewards/r7_originality.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
from viral_script_engine.agents.originality_agent import OriginalityOutput
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class OriginalityRewardResult(BaseModel):
|
| 7 |
+
score: float
|
| 8 |
+
originality_score: float
|
| 9 |
+
flag_count: int
|
| 10 |
+
breakdown: str
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class OriginalityReward:
|
| 14 |
+
"""
|
| 15 |
+
Converts OriginalityOutput into a reward signal.
|
| 16 |
+
|
| 17 |
+
Scoring maps directly from originality_score:
|
| 18 |
+
- originality_score >= 0.8: reward = 1.0 (genuinely distinctive)
|
| 19 |
+
- originality_score 0.6β0.8: reward = originality_score
|
| 20 |
+
- originality_score 0.4β0.6: reward = 0.3 (mediocre β generic but not terrible)
|
| 21 |
+
- originality_score < 0.4: reward = 0.0 (template clone β cliff penalty)
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def score(self, originality_output: OriginalityOutput) -> OriginalityRewardResult:
|
| 25 |
+
os_ = originality_output.originality_score
|
| 26 |
+
flag_count = len(originality_output.flags)
|
| 27 |
+
|
| 28 |
+
if os_ >= 0.8:
|
| 29 |
+
reward = 1.0
|
| 30 |
+
breakdown = f"Highly original (score={os_:.2f}). No dominant template patterns."
|
| 31 |
+
elif os_ >= 0.6:
|
| 32 |
+
reward = os_
|
| 33 |
+
breakdown = f"Moderately original (score={os_:.2f}). Some template overlap detected."
|
| 34 |
+
elif os_ >= 0.4:
|
| 35 |
+
reward = 0.3
|
| 36 |
+
breakdown = f"Generic content (score={os_:.2f}). Multiple overused patterns present."
|
| 37 |
+
else:
|
| 38 |
+
reward = 0.0
|
| 39 |
+
breakdown = f"Template clone (score={os_:.2f}). Script heavily relies on overused formats."
|
| 40 |
+
|
| 41 |
+
return OriginalityRewardResult(
|
| 42 |
+
score=reward,
|
| 43 |
+
originality_score=os_,
|
| 44 |
+
flag_count=flag_count,
|
| 45 |
+
breakdown=breakdown,
|
| 46 |
+
)
|
viral_script_engine/rewards/reward_aggregator.py
CHANGED
|
@@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
|
|
| 11 |
_COMPONENT_FIELDS = [
|
| 12 |
"r1_hook_strength", "r2_coherence", "r3_cultural_alignment",
|
| 13 |
"r4_debate_resolution", "r5_defender_preservation",
|
|
|
|
| 14 |
]
|
| 15 |
|
| 16 |
_DROP_THRESHOLD = 0.25
|
|
@@ -36,6 +37,26 @@ class RewardAggregator:
|
|
| 36 |
episode_id: str = "",
|
| 37 |
step_num: int = 0,
|
| 38 |
) -> Tuple[RewardComponents, AntiGamingLog]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
components.compute_total()
|
| 40 |
pre_penalty_total = components.total
|
| 41 |
|
|
|
|
| 11 |
_COMPONENT_FIELDS = [
|
| 12 |
"r1_hook_strength", "r2_coherence", "r3_cultural_alignment",
|
| 13 |
"r4_debate_resolution", "r5_defender_preservation",
|
| 14 |
+
"r6_safety", "r7_originality",
|
| 15 |
]
|
| 16 |
|
| 17 |
_DROP_THRESHOLD = 0.25
|
|
|
|
| 37 |
episode_id: str = "",
|
| 38 |
step_num: int = 0,
|
| 39 |
) -> Tuple[RewardComponents, AntiGamingLog]:
|
| 40 |
+
# Hard zero: if R6 (safety) is 0.0, the entire step reward is zeroed out
|
| 41 |
+
# regardless of other component scores β any shadowban trigger is non-negotiable.
|
| 42 |
+
if components.r6_safety is not None and components.r6_safety == 0.0:
|
| 43 |
+
components.compute_total()
|
| 44 |
+
pre_penalty_total = components.total
|
| 45 |
+
components.total = 0.0
|
| 46 |
+
components.anti_gaming_penalty = 1.0
|
| 47 |
+
logger.warning("R6 safety hard zero triggered β shadowban content detected, zeroing step reward.")
|
| 48 |
+
log = AntiGamingLog(
|
| 49 |
+
episode_id=episode_id,
|
| 50 |
+
step_num=step_num,
|
| 51 |
+
triggered=True,
|
| 52 |
+
rule_triggered="r6_safety_hard_zero",
|
| 53 |
+
component_that_dropped="r6_safety",
|
| 54 |
+
penalty_applied=1.0,
|
| 55 |
+
pre_penalty_total=pre_penalty_total,
|
| 56 |
+
post_penalty_total=0.0,
|
| 57 |
+
)
|
| 58 |
+
return components, log
|
| 59 |
+
|
| 60 |
components.compute_total()
|
| 61 |
pre_penalty_total = components.total
|
| 62 |
|
viral_script_engine/scripts/run_dummy_episode.py
CHANGED
|
@@ -21,6 +21,11 @@ console = Console()
|
|
| 21 |
BASE_DIR = Path(__file__).parent.parent
|
| 22 |
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def build_random_action(action_type: ActionType) -> dict:
|
| 25 |
labels = {
|
| 26 |
ActionType.HOOK_REWRITE: ("hook", "Rewrite the hook to open with a specific number or bold claim."),
|
|
@@ -48,7 +53,7 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
|
|
| 48 |
f"Difficulty: {difficulty} | Max steps: {steps}\n"
|
| 49 |
f"Region: {obs['region']} | Platform: {obs['platform']} | Niche: {obs['niche']}\n"
|
| 50 |
f"Episode ID: {obs['episode_id']}",
|
| 51 |
-
title="[bold blue]Phase
|
| 52 |
border_style="blue",
|
| 53 |
))
|
| 54 |
|
|
@@ -65,22 +70,61 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
|
|
| 65 |
|
| 66 |
obs, reward, terminated, truncated, info = env.step(action)
|
| 67 |
rc = info["reward_components"]
|
|
|
|
|
|
|
| 68 |
|
| 69 |
if verbose:
|
| 70 |
t = Table(title=f"Step {step_num + 1} β {action_type.value}", box=box.SIMPLE_HEAD)
|
| 71 |
t.add_column("Metric", style="cyan", min_width=22)
|
| 72 |
-
t.add_column("
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
if info.get("anti_gaming_triggered"):
|
| 79 |
-
t.add_row("Anti-Gaming Penalty", f"[red]{rc.get('anti_gaming_penalty', 0):.3f}[/red]")
|
| 80 |
-
t.add_row("Penalty Reason", f"[red]{info.get('penalty_reason', '')}[/red]")
|
| 81 |
-
t.add_row("Terminated", str(terminated))
|
| 82 |
console.print(t)
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
if obs.get("debate_history"):
|
| 85 |
latest = obs["debate_history"][-1]
|
| 86 |
if latest.get("rewrite_diff"):
|
|
@@ -95,6 +139,8 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
|
|
| 95 |
"action": action,
|
| 96 |
"reward": reward,
|
| 97 |
"reward_components": rc,
|
|
|
|
|
|
|
| 98 |
"anti_gaming": info.get("anti_gaming_triggered", False),
|
| 99 |
"terminated": terminated,
|
| 100 |
})
|
|
@@ -108,8 +154,10 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
|
|
| 108 |
|
| 109 |
console.print(Panel(
|
| 110 |
f"[bold green]Final Reward:[/bold green] {final_rc.get('total', 0):.3f}\n"
|
| 111 |
-
f"R1 Hook Strength:
|
| 112 |
-
f"R2 Coherence:
|
|
|
|
|
|
|
| 113 |
f"Steps completed: {final_state['step_num']}",
|
| 114 |
title="Episode Summary",
|
| 115 |
border_style="green",
|
|
@@ -119,7 +167,7 @@ def run_episode(difficulty: str, steps: int, verbose: bool) -> dict:
|
|
| 119 |
|
| 120 |
|
| 121 |
def main():
|
| 122 |
-
parser = argparse.ArgumentParser(description="Run Phase
|
| 123 |
parser.add_argument("--difficulty", default="easy", choices=["easy", "medium", "hard"])
|
| 124 |
parser.add_argument("--steps", type=int, default=3)
|
| 125 |
parser.add_argument("--verbose", action="store_true")
|
|
@@ -136,12 +184,15 @@ def main():
|
|
| 136 |
|
| 137 |
final_rc = episode_log["final_state"]["reward_components"]
|
| 138 |
gate_pass = (
|
| 139 |
-
final_rc.get("
|
| 140 |
-
and final_rc.get("
|
| 141 |
and log_path.exists()
|
| 142 |
)
|
| 143 |
style = "bold green" if gate_pass else "bold red"
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
| 145 |
console.print(Panel(f"[{style}]{label}[/{style}]", border_style="green" if gate_pass else "red"))
|
| 146 |
|
| 147 |
|
|
|
|
| 21 |
BASE_DIR = Path(__file__).parent.parent
|
| 22 |
|
| 23 |
|
| 24 |
+
def _bar(score: float, width: int = 8) -> str:
|
| 25 |
+
filled = round(score * width)
|
| 26 |
+
return "#" * filled + "." * (width - filled)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
def build_random_action(action_type: ActionType) -> dict:
|
| 30 |
labels = {
|
| 31 |
ActionType.HOOK_REWRITE: ("hook", "Rewrite the hook to open with a specific number or bold claim."),
|
|
|
|
| 53 |
f"Difficulty: {difficulty} | Max steps: {steps}\n"
|
| 54 |
f"Region: {obs['region']} | Platform: {obs['platform']} | Niche: {obs['niche']}\n"
|
| 55 |
f"Episode ID: {obs['episode_id']}",
|
| 56 |
+
title="[bold blue]Phase 6 Demo Episode[/bold blue]",
|
| 57 |
border_style="blue",
|
| 58 |
))
|
| 59 |
|
|
|
|
| 70 |
|
| 71 |
obs, reward, terminated, truncated, info = env.step(action)
|
| 72 |
rc = info["reward_components"]
|
| 73 |
+
mod_out = info.get("moderation_output", {})
|
| 74 |
+
orig_out = info.get("originality_output", {})
|
| 75 |
|
| 76 |
if verbose:
|
| 77 |
t = Table(title=f"Step {step_num + 1} β {action_type.value}", box=box.SIMPLE_HEAD)
|
| 78 |
t.add_column("Metric", style="cyan", min_width=22)
|
| 79 |
+
t.add_column("Score", min_width=12)
|
| 80 |
+
t.add_column("Bar", min_width=10)
|
| 81 |
+
|
| 82 |
+
def _row(label, key, suffix=""):
|
| 83 |
+
val = rc.get(key)
|
| 84 |
+
score_str = f"{val:.3f}" if val is not None else "N/A"
|
| 85 |
+
bar_str = _bar(val) if val is not None else ""
|
| 86 |
+
t.add_row(label, score_str + suffix, bar_str)
|
| 87 |
+
|
| 88 |
+
_row("R1 Hook Strength", "r1_hook_strength")
|
| 89 |
+
_row("R2 Coherence", "r2_coherence")
|
| 90 |
+
_row("R3 Cultural", "r3_cultural_alignment")
|
| 91 |
+
_row("R4 Resolution", "r4_debate_resolution")
|
| 92 |
+
_row("R5 Preservation", "r5_defender_preservation")
|
| 93 |
+
|
| 94 |
+
r6_val = rc.get("r6_safety")
|
| 95 |
+
r6_suffix = " [OK] No flags" if mod_out.get("total_flags", 0) == 0 else f" [!] {mod_out.get('total_flags', 0)} flag(s)"
|
| 96 |
+
r6_str = (f"{r6_val:.3f}{r6_suffix}" if r6_val is not None else "N/A")
|
| 97 |
+
t.add_row("R6 Safety", r6_str, _bar(r6_val) if r6_val is not None else "")
|
| 98 |
+
|
| 99 |
+
r7_val = rc.get("r7_originality")
|
| 100 |
+
orig_flags = len(orig_out.get("flags", []))
|
| 101 |
+
r7_suffix = f" [!] {orig_flags} template match(es)" if orig_flags > 0 else " [OK] Original"
|
| 102 |
+
r7_str = (f"{r7_val:.3f}{r7_suffix}" if r7_val is not None else "N/A")
|
| 103 |
+
t.add_row("R7 Originality", r7_str, _bar(r7_val) if r7_val is not None else "")
|
| 104 |
+
|
| 105 |
+
t.add_row("-" * 22, "-" * 12, "-" * 10)
|
| 106 |
+
t.add_row("[bold]Total[/bold]", f"[bold]{reward:.3f}[/bold]", _bar(reward))
|
| 107 |
+
|
| 108 |
if info.get("anti_gaming_triggered"):
|
| 109 |
+
t.add_row("Anti-Gaming Penalty", f"[red]{rc.get('anti_gaming_penalty', 0):.3f}[/red]", "")
|
| 110 |
+
t.add_row("Penalty Reason", f"[red]{info.get('penalty_reason', '')}[/red]", "")
|
| 111 |
+
t.add_row("Terminated", str(terminated), "")
|
| 112 |
console.print(t)
|
| 113 |
|
| 114 |
+
# Show moderation flags in red panel if any
|
| 115 |
+
mod_flags = mod_out.get("flags", [])
|
| 116 |
+
if mod_flags:
|
| 117 |
+
flag_lines = []
|
| 118 |
+
for fl in mod_flags:
|
| 119 |
+
flag_lines.append(
|
| 120 |
+
f" [{fl['severity']}] {fl['category']} in {fl['position']}: \"{fl['trigger_phrase']}\" β {fl['suggestion']}"
|
| 121 |
+
)
|
| 122 |
+
console.print(Panel(
|
| 123 |
+
"\n".join(flag_lines),
|
| 124 |
+
title="[bold red]!! MODERATION FLAGS DETECTED[/bold red]",
|
| 125 |
+
border_style="red",
|
| 126 |
+
))
|
| 127 |
+
|
| 128 |
if obs.get("debate_history"):
|
| 129 |
latest = obs["debate_history"][-1]
|
| 130 |
if latest.get("rewrite_diff"):
|
|
|
|
| 139 |
"action": action,
|
| 140 |
"reward": reward,
|
| 141 |
"reward_components": rc,
|
| 142 |
+
"moderation_output": mod_out,
|
| 143 |
+
"originality_output": orig_out,
|
| 144 |
"anti_gaming": info.get("anti_gaming_triggered", False),
|
| 145 |
"terminated": terminated,
|
| 146 |
})
|
|
|
|
| 154 |
|
| 155 |
console.print(Panel(
|
| 156 |
f"[bold green]Final Reward:[/bold green] {final_rc.get('total', 0):.3f}\n"
|
| 157 |
+
f"R1 Hook Strength: {final_rc.get('r1_hook_strength', 'N/A')}\n"
|
| 158 |
+
f"R2 Coherence: {final_rc.get('r2_coherence', 'N/A')}\n"
|
| 159 |
+
f"R6 Safety: {final_rc.get('r6_safety', 'N/A')}\n"
|
| 160 |
+
f"R7 Originality: {final_rc.get('r7_originality', 'N/A')}\n"
|
| 161 |
f"Steps completed: {final_state['step_num']}",
|
| 162 |
title="Episode Summary",
|
| 163 |
border_style="green",
|
|
|
|
| 167 |
|
| 168 |
|
| 169 |
def main():
|
| 170 |
+
parser = argparse.ArgumentParser(description="Run Phase 6 dummy episode")
|
| 171 |
parser.add_argument("--difficulty", default="easy", choices=["easy", "medium", "hard"])
|
| 172 |
parser.add_argument("--steps", type=int, default=3)
|
| 173 |
parser.add_argument("--verbose", action="store_true")
|
|
|
|
| 184 |
|
| 185 |
final_rc = episode_log["final_state"]["reward_components"]
|
| 186 |
gate_pass = (
|
| 187 |
+
final_rc.get("r6_safety") is not None
|
| 188 |
+
and final_rc.get("r7_originality") is not None
|
| 189 |
and log_path.exists()
|
| 190 |
)
|
| 191 |
style = "bold green" if gate_pass else "bold red"
|
| 192 |
+
if gate_pass:
|
| 193 |
+
label = "PHASE 6 GATE: PASS β R6 (safety) and R7 (originality) active. Total reward components: 7."
|
| 194 |
+
else:
|
| 195 |
+
label = "PHASE 6 GATE: FAIL β R6 or R7 missing from reward output."
|
| 196 |
console.print(Panel(f"[{style}]{label}[/{style}]", border_style="green" if gate_pass else "red"))
|
| 197 |
|
| 198 |
|
viral_script_engine/tests/test_phase6.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 6 tests: ModerationAgent, OriginalityAgent, R6/R7 rewards, aggregator, env.step()."""
|
| 2 |
+
import sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 8 |
+
|
| 9 |
+
from viral_script_engine.agents.moderation_agent import ModerationAgent, ModerationOutput
|
| 10 |
+
from viral_script_engine.agents.originality_agent import OriginalityAgent, OriginalityOutput
|
| 11 |
+
from viral_script_engine.rewards.r6_safety import SafetyReward
|
| 12 |
+
from viral_script_engine.rewards.r7_originality import OriginalityReward
|
| 13 |
+
from viral_script_engine.rewards.reward_aggregator import RewardAggregator
|
| 14 |
+
from viral_script_engine.environment.observations import RewardComponents
|
| 15 |
+
|
| 16 |
+
BASE_DIR = Path(__file__).parent.parent
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@pytest.fixture
|
| 20 |
+
def moderation_agent():
|
| 21 |
+
return ModerationAgent(kb_path=str(BASE_DIR / "data" / "shadowban_triggers.json"))
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@pytest.fixture
|
| 25 |
+
def originality_agent():
|
| 26 |
+
return OriginalityAgent(templates_path=str(BASE_DIR / "data" / "viral_templates.json"))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@pytest.fixture
|
| 30 |
+
def r6():
|
| 31 |
+
return SafetyReward()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@pytest.fixture
|
| 35 |
+
def r7():
|
| 36 |
+
return OriginalityReward()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ββ ModerationAgent ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
+
|
| 41 |
+
class TestModerationAgent:
|
| 42 |
+
def test_flags_high_severity_health_claim(self, moderation_agent):
|
| 43 |
+
script = (
|
| 44 |
+
"Doctors don't want you to know this miracle remedy. "
|
| 45 |
+
"It is a guaranteed weight loss solution that cures everything. "
|
| 46 |
+
"Get started today."
|
| 47 |
+
)
|
| 48 |
+
result = moderation_agent.check(script)
|
| 49 |
+
assert result.total_flags > 0
|
| 50 |
+
severities = [f.severity for f in result.flags]
|
| 51 |
+
assert "high" in severities
|
| 52 |
+
|
| 53 |
+
def test_flags_policy_violation(self, moderation_agent):
|
| 54 |
+
script = (
|
| 55 |
+
"Want to grow your audience fast? Buy followers today. "
|
| 56 |
+
"Make $5000 in 30 days guaranteed β it's the easiest passive income. "
|
| 57 |
+
"Get rich quick with our proven method."
|
| 58 |
+
)
|
| 59 |
+
result = moderation_agent.check(script)
|
| 60 |
+
assert result.total_flags > 0
|
| 61 |
+
categories = [f.category for f in result.flags]
|
| 62 |
+
assert "policy_violation" in categories
|
| 63 |
+
|
| 64 |
+
def test_flags_engagement_bait(self, moderation_agent):
|
| 65 |
+
script = (
|
| 66 |
+
"Here's something nobody talks about in fitness. "
|
| 67 |
+
"These 3 exercises changed my life completely. "
|
| 68 |
+
"Comment if you agree and tag 3 friends who need this."
|
| 69 |
+
)
|
| 70 |
+
result = moderation_agent.check(script)
|
| 71 |
+
assert result.total_flags > 0
|
| 72 |
+
|
| 73 |
+
def test_clean_script_is_safe(self, moderation_agent):
|
| 74 |
+
script = (
|
| 75 |
+
"I tested three different morning routines for 30 days each. "
|
| 76 |
+
"The data surprised me β the simplest one produced the best results. "
|
| 77 |
+
"Here's what I found and what I now do every day."
|
| 78 |
+
)
|
| 79 |
+
result = moderation_agent.check(script)
|
| 80 |
+
assert result.is_safe is True
|
| 81 |
+
assert result.overall_risk == "safe"
|
| 82 |
+
|
| 83 |
+
def test_high_severity_flag_sets_is_safe_false(self, moderation_agent):
|
| 84 |
+
script = (
|
| 85 |
+
"This miracle remedy cures cancer permanently. "
|
| 86 |
+
"Big pharma is hiding this from you. "
|
| 87 |
+
"Share before it gets taken down."
|
| 88 |
+
)
|
| 89 |
+
result = moderation_agent.check(script)
|
| 90 |
+
assert result.is_safe is False
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ββ SafetyReward βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
|
| 95 |
+
class TestSafetyReward:
|
| 96 |
+
def test_zero_score_on_high_severity(self, moderation_agent, r6):
|
| 97 |
+
script = (
|
| 98 |
+
"This miracle remedy cures all diseases overnight. "
|
| 99 |
+
"Buy followers now to grow faster. "
|
| 100 |
+
"Make $10,000 in 30 days guaranteed."
|
| 101 |
+
)
|
| 102 |
+
mod_out = moderation_agent.check(script)
|
| 103 |
+
result = r6.score(mod_out)
|
| 104 |
+
assert result.score == 0.0
|
| 105 |
+
|
| 106 |
+
def test_full_score_on_clean_script(self, moderation_agent, r6):
|
| 107 |
+
script = (
|
| 108 |
+
"I spent 6 months studying sleep science. "
|
| 109 |
+
"These 3 evidence-based habits improved my sleep quality measurably. "
|
| 110 |
+
"Start with the simplest one first."
|
| 111 |
+
)
|
| 112 |
+
mod_out = moderation_agent.check(script)
|
| 113 |
+
result = r6.score(mod_out)
|
| 114 |
+
assert result.score == 1.0
|
| 115 |
+
|
| 116 |
+
def test_medium_score_on_medium_severity(self, moderation_agent, r6):
|
| 117 |
+
script = (
|
| 118 |
+
"This app is a must-have. "
|
| 119 |
+
"Watch the full movie version for free on our site. "
|
| 120 |
+
"Click the link below for the download."
|
| 121 |
+
)
|
| 122 |
+
mod_out = moderation_agent.check(script)
|
| 123 |
+
result = r6.score(mod_out)
|
| 124 |
+
# medium or high severity β score should be <= 0.5
|
| 125 |
+
assert result.score <= 0.5
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# ββ OriginalityAgent ββββββββββββοΏ½οΏ½ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 129 |
+
|
| 130 |
+
class TestOriginalityAgent:
|
| 131 |
+
def test_detects_overused_hook(self, originality_agent):
|
| 132 |
+
script = (
|
| 133 |
+
"Nobody talks about this but your morning routine is wrong. "
|
| 134 |
+
"Here are three things I wish I knew before starting. "
|
| 135 |
+
"Follow for more tips."
|
| 136 |
+
)
|
| 137 |
+
result = originality_agent.check(script)
|
| 138 |
+
assert len(result.flags) > 0
|
| 139 |
+
template_types = [f.template_type for f in result.flags]
|
| 140 |
+
assert any(t in ("overused_hook", "overused_cta") for t in template_types)
|
| 141 |
+
|
| 142 |
+
def test_unique_script_scores_high(self, originality_agent):
|
| 143 |
+
script = (
|
| 144 |
+
"In 2019, the average Indian millennial checked their phone 94 times a day. "
|
| 145 |
+
"I tracked my own usage for a month and found a pattern nobody warned me about. "
|
| 146 |
+
"The solution had nothing to do with willpower."
|
| 147 |
+
)
|
| 148 |
+
result = originality_agent.check(script)
|
| 149 |
+
assert result.originality_score >= 0.8
|
| 150 |
+
|
| 151 |
+
def test_template_clone_is_generic(self, originality_agent):
|
| 152 |
+
script = (
|
| 153 |
+
"Nobody talks about this but you have been doing it wrong your whole life. "
|
| 154 |
+
"Stop doing this immediately and save this for later. "
|
| 155 |
+
"Follow for more and share with someone who needs this."
|
| 156 |
+
)
|
| 157 |
+
result = originality_agent.check(script)
|
| 158 |
+
assert result.is_generic is True or result.originality_score < 0.6
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# ββ OriginalityReward ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 162 |
+
|
| 163 |
+
class TestOriginalityReward:
|
| 164 |
+
def test_zero_score_on_template_clone(self, originality_agent, r7):
|
| 165 |
+
script = (
|
| 166 |
+
"Nobody talks about this but you have been doing it wrong your whole life. "
|
| 167 |
+
"Stop doing this immediately and save this for later. "
|
| 168 |
+
"Follow for more and share with someone who needs this."
|
| 169 |
+
)
|
| 170 |
+
orig_out = originality_agent.check(script)
|
| 171 |
+
# Force a low originality_score scenario
|
| 172 |
+
from viral_script_engine.agents.originality_agent import OriginalityOutput
|
| 173 |
+
low_out = OriginalityOutput(
|
| 174 |
+
flags=orig_out.flags,
|
| 175 |
+
originality_score=0.2,
|
| 176 |
+
is_generic=True,
|
| 177 |
+
unique_elements=[],
|
| 178 |
+
)
|
| 179 |
+
result = r7.score(low_out)
|
| 180 |
+
assert result.score == 0.0
|
| 181 |
+
|
| 182 |
+
def test_full_score_on_high_originality(self, originality_agent, r7):
|
| 183 |
+
from viral_script_engine.agents.originality_agent import OriginalityOutput
|
| 184 |
+
high_out = OriginalityOutput(
|
| 185 |
+
flags=[],
|
| 186 |
+
originality_score=0.95,
|
| 187 |
+
is_generic=False,
|
| 188 |
+
unique_elements=["unique sentence 1", "unique sentence 2"],
|
| 189 |
+
)
|
| 190 |
+
result = r7.score(high_out)
|
| 191 |
+
assert result.score == 1.0
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
# ββ RewardAggregator with R6/R7 ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 195 |
+
|
| 196 |
+
class TestRewardAggregatorPhase6:
|
| 197 |
+
def test_r6_r7_included_in_total(self):
|
| 198 |
+
agg = RewardAggregator()
|
| 199 |
+
components = RewardComponents(
|
| 200 |
+
r1_hook_strength=0.8,
|
| 201 |
+
r2_coherence=0.7,
|
| 202 |
+
r3_cultural_alignment=0.75,
|
| 203 |
+
r4_debate_resolution=0.6,
|
| 204 |
+
r5_defender_preservation=0.7,
|
| 205 |
+
r6_safety=1.0,
|
| 206 |
+
r7_originality=0.9,
|
| 207 |
+
)
|
| 208 |
+
start = RewardComponents(
|
| 209 |
+
r1_hook_strength=0.5,
|
| 210 |
+
r2_coherence=0.5,
|
| 211 |
+
r3_cultural_alignment=0.5,
|
| 212 |
+
r4_debate_resolution=0.5,
|
| 213 |
+
r5_defender_preservation=0.5,
|
| 214 |
+
r6_safety=1.0,
|
| 215 |
+
r7_originality=0.9,
|
| 216 |
+
)
|
| 217 |
+
result, log = agg.compute(components, start, [], episode_id="test", step_num=1)
|
| 218 |
+
assert result.total > 0.0
|
| 219 |
+
assert not log.triggered
|
| 220 |
+
|
| 221 |
+
def test_catastrophic_drop_fires_on_r6_zero(self):
|
| 222 |
+
agg = RewardAggregator()
|
| 223 |
+
components = RewardComponents(
|
| 224 |
+
r1_hook_strength=0.8,
|
| 225 |
+
r2_coherence=0.7,
|
| 226 |
+
r3_cultural_alignment=0.75,
|
| 227 |
+
r4_debate_resolution=0.6,
|
| 228 |
+
r5_defender_preservation=0.7,
|
| 229 |
+
r6_safety=0.0,
|
| 230 |
+
r7_originality=0.9,
|
| 231 |
+
)
|
| 232 |
+
start = RewardComponents(
|
| 233 |
+
r1_hook_strength=0.8,
|
| 234 |
+
r2_coherence=0.7,
|
| 235 |
+
r3_cultural_alignment=0.75,
|
| 236 |
+
r4_debate_resolution=0.6,
|
| 237 |
+
r5_defender_preservation=0.7,
|
| 238 |
+
r6_safety=1.0,
|
| 239 |
+
r7_originality=0.9,
|
| 240 |
+
)
|
| 241 |
+
result, log = agg.compute(components, start, [], episode_id="test", step_num=1)
|
| 242 |
+
assert result.total == 0.0
|
| 243 |
+
assert log.triggered
|
| 244 |
+
assert log.rule_triggered == "r6_safety_hard_zero"
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# ββ env.step() integration ββββββββββββββββββββββββββοΏ½οΏ½οΏ½ββββββββββββββββββββββββββ
|
| 248 |
+
|
| 249 |
+
class TestEnvStepPhase6:
|
| 250 |
+
def test_step_includes_moderation_and_originality(self, monkeypatch):
|
| 251 |
+
from unittest.mock import MagicMock
|
| 252 |
+
from viral_script_engine.environment.env import ViralScriptEnv
|
| 253 |
+
from viral_script_engine.agents.critic import CritiqueOutput, CritiqueClaim
|
| 254 |
+
from viral_script_engine.agents.defender import DefenderOutput
|
| 255 |
+
from viral_script_engine.agents.rewriter import RewriteResult
|
| 256 |
+
|
| 257 |
+
env = ViralScriptEnv(
|
| 258 |
+
scripts_path=str(BASE_DIR / "data" / "test_scripts" / "scripts.json"),
|
| 259 |
+
max_steps=1,
|
| 260 |
+
difficulty="easy",
|
| 261 |
+
use_escalation=False,
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
dummy_claim = CritiqueClaim(
|
| 265 |
+
claim_id="C1",
|
| 266 |
+
critique_class="hook_weakness",
|
| 267 |
+
claim_text="Hook is weak",
|
| 268 |
+
timestamp_range="0-3s",
|
| 269 |
+
evidence="Opening is vague",
|
| 270 |
+
is_falsifiable=True,
|
| 271 |
+
severity="medium",
|
| 272 |
+
)
|
| 273 |
+
dummy_critique = CritiqueOutput(
|
| 274 |
+
claims=[dummy_claim],
|
| 275 |
+
overall_severity="medium",
|
| 276 |
+
raw_response="Hook is weak",
|
| 277 |
+
)
|
| 278 |
+
dummy_defender = DefenderOutput(
|
| 279 |
+
core_strength="The hook has genuine curiosity value.",
|
| 280 |
+
core_strength_quote="First sentence",
|
| 281 |
+
defense_argument="The structure is sound; only specificity needs improvement.",
|
| 282 |
+
flagged_critic_claims=["C1"],
|
| 283 |
+
regional_voice_elements=["regional phrase"],
|
| 284 |
+
)
|
| 285 |
+
dummy_rewrite = RewriteResult(
|
| 286 |
+
rewritten_script="3 things nobody tells you about morning routines that actually work.",
|
| 287 |
+
diff="- Old hook\n+ 3 things nobody tells you about morning routines that actually work.",
|
| 288 |
+
word_count_delta=2,
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
from viral_script_engine.rewards.r4_debate_resolution import DebateResolutionResult
|
| 292 |
+
dummy_r4 = DebateResolutionResult(
|
| 293 |
+
score=0.7,
|
| 294 |
+
resolution_status="resolved",
|
| 295 |
+
original_claim_id="C1",
|
| 296 |
+
original_claim_class="hook_weakness",
|
| 297 |
+
new_claims_count=0,
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
monkeypatch.setattr(env.critic, "critique", lambda *a, **kw: dummy_critique)
|
| 301 |
+
monkeypatch.setattr(env.defender, "defend", lambda **kw: dummy_defender)
|
| 302 |
+
monkeypatch.setattr(env.rewriter, "rewrite", lambda script, action: dummy_rewrite)
|
| 303 |
+
monkeypatch.setattr(env.r4, "score", lambda **kw: dummy_r4)
|
| 304 |
+
|
| 305 |
+
env.reset()
|
| 306 |
+
action = {
|
| 307 |
+
"action_type": "hook_rewrite",
|
| 308 |
+
"target_section": "hook",
|
| 309 |
+
"instruction": "Make the hook more engaging.",
|
| 310 |
+
"critique_claim_id": "C1",
|
| 311 |
+
"reasoning": "test",
|
| 312 |
+
}
|
| 313 |
+
_, _, _, _, info = env.step(action)
|
| 314 |
+
assert "moderation_output" in info
|
| 315 |
+
assert "originality_output" in info
|
| 316 |
+
rc = info["reward_components"]
|
| 317 |
+
assert rc.get("r6_safety") is not None
|
| 318 |
+
assert rc.get("r7_originality") is not None
|