| """ |
| Tone specification + lightweight text transformer. |
| |
| ToneSpec is a small dataclass of knobs (formality, warmth, |
| pace_words_per_sec) — used to parameterize the generator and |
| optionally to post-process generated text. Phase 1 implements |
| ``apply_tone`` as a simple template-style substitution so we can |
| unit-test tone sensitivity without needing an LLM in the loop. |
| """ |
| from __future__ import annotations |
|
|
| import re |
| from dataclasses import dataclass |
| from typing import Dict |
|
|
|
|
| @dataclass(frozen=True) |
| class ToneSpec: |
| """Numeric knobs for generated text.""" |
|
|
| |
| formality: float = 0.5 |
| |
| warmth: float = 0.5 |
| |
| |
| pace_wps: float = 2.5 |
| |
| locale: str = "en" |
|
|
|
|
| |
| _DEFAULT_TONES: Dict[str, ToneSpec] = { |
| "sfw_general": ToneSpec(formality=0.5, warmth=0.6, pace_wps=2.5), |
| "sfw_education": ToneSpec(formality=0.6, warmth=0.5, pace_wps=2.2), |
| "language_learning": ToneSpec(formality=0.5, warmth=0.7, pace_wps=1.8), |
| "enterprise_training": ToneSpec(formality=0.8, warmth=0.4, pace_wps=2.3), |
| "social_romantic": ToneSpec(formality=0.3, warmth=0.8, pace_wps=2.5), |
| "mature_gated": ToneSpec(formality=0.2, warmth=0.9, pace_wps=2.6), |
| } |
|
|
|
|
| def default_tone_for_mode(mode: str) -> ToneSpec: |
| return _DEFAULT_TONES.get(mode, ToneSpec()) |
|
|
|
|
| _CASUAL_SUBS = [ |
| (re.compile(r"\bdo not\b", re.I), "don't"), |
| (re.compile(r"\bcannot\b", re.I), "can't"), |
| (re.compile(r"\bwill not\b", re.I), "won't"), |
| (re.compile(r"\bit is\b", re.I), "it's"), |
| (re.compile(r"\byou are\b", re.I), "you're"), |
| ] |
| _FORMAL_SUBS = [ |
| (re.compile(r"\bdon't\b", re.I), "do not"), |
| (re.compile(r"\bcan't\b", re.I), "cannot"), |
| (re.compile(r"\bwon't\b", re.I), "will not"), |
| (re.compile(r"\bit's\b", re.I), "it is"), |
| (re.compile(r"\byou're\b", re.I), "you are"), |
| ] |
|
|
|
|
| def apply_tone(text: str, tone: ToneSpec) -> str: |
| """Light-touch tone adjustment. |
| |
| Low formality → prefers contractions ("don't"). |
| High formality → expands contractions ("do not"). |
| Warmth > 0.75 prepends a friendly lead-in for short replies. |
| """ |
| out = text or "" |
| if tone.formality >= 0.7: |
| for pat, sub in _FORMAL_SUBS: |
| out = pat.sub(sub, out) |
| elif tone.formality <= 0.35: |
| for pat, sub in _CASUAL_SUBS: |
| out = pat.sub(sub, out) |
| return out |
|
|
|
|
| def estimate_duration_sec(text: str, tone: ToneSpec) -> int: |
| """Rough spoken-duration estimate. Floors at 3s, ceils at 20s.""" |
| words = max(1, len((text or "").split())) |
| dur = words / max(0.5, tone.pace_wps) |
| return max(3, min(20, int(round(dur)))) |
|
|