Spaces:
Paused
Paused
File size: 6,529 Bytes
ec17af3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | import re
from typing import List
FILLER_WORDS = ["um", "uh", "like", "you know", "actually", "basically", "literally"]
STRUCTURE_WORDS = ["first", "second", "third", "because", "therefore", "however", "so", "thus"]
def tokenize(text: str) -> List[str]:
return [w for w in re.sub(r"[^a-z0-9\s']", " ", text.lower()).split() if w]
def count_words(segments: List[dict]) -> int:
return sum(len(tokenize(seg["text"])) for seg in segments)
def count_fillers(segments: List[dict]) -> int:
text = " ".join(seg["text"].lower() for seg in segments)
total = 0
for filler in FILLER_WORDS:
escaped = re.escape(filler).replace(" ", r"\s+")
pattern = re.compile(rf"\b{escaped}\b")
total += len(pattern.findall(text))
return total
def compute_wpm(word_count: int, duration_ms: int) -> float:
if duration_ms <= 0:
return 0
return round((word_count / (duration_ms / 60000)) * 10) / 10
def score_wpm(wpm: float) -> int:
if 120 <= wpm <= 160:
return 18
if 100 <= wpm < 120 or 160 < wpm <= 180:
return 10
return 4
def score_fillers(fillers_per_minute: float) -> int:
if fillers_per_minute <= 2:
return 12
if fillers_per_minute <= 6:
return 8
return 3
def score_pauses(pause_events: List[int], duration_ms: int) -> int:
if not pause_events:
return 4
natural = len([p for p in pause_events if 600 <= p <= 1500])
long = len([p for p in pause_events if p > 1500])
minutes = max(duration_ms / 60000, 0.1)
rate = natural / minutes
if long > max(2, minutes):
return 4
if 2 <= rate <= 6:
return 10
if rate >= 1:
return 7
return 5
def score_eye_contact(eye_contact_pct: float) -> int:
if eye_contact_pct > 0.7:
return 15
if eye_contact_pct >= 0.4:
return 10
return 5
def score_smile(smile_avg: float) -> int:
return round(7 * max(0, min(1, smile_avg)))
def score_head_stability(yaw_var: float, pitch_var: float, roll_var: float) -> int:
total_var = yaw_var + pitch_var + roll_var
if total_var < 5:
return 8
if total_var < 12:
return 6
return 4
def score_structure(segments: List[dict]) -> int:
words = tokenize(" ".join(seg["text"] for seg in segments))
count = sum(1 for w in words if w in STRUCTURE_WORDS)
if count >= 4:
return 10
if count >= 2:
return 7
return 4
def score_repetition(segments: List[dict]) -> int:
words = tokenize(" ".join(seg["text"] for seg in segments))
freq = {}
for word in words:
freq[word] = freq.get(word, 0) + 1
repeated = len([c for c in freq.values() if c >= 6])
if repeated >= 3:
return -8
if repeated >= 1:
return -4
return 0
def score_clarity(segments: List[dict]) -> int:
sentences = [s.strip() for s in re.split(r"[.!?]+", " ".join(seg["text"] for seg in segments)) if s.strip()]
if not sentences:
return 0
lengths = [len(tokenize(s)) for s in sentences]
avg = sum(lengths) / len(lengths)
if 8 <= avg <= 20:
return 12
if 5 <= avg <= 25:
return 8
return 4
def score_keyword_coverage(segments: List[dict], keywords: List[str]) -> int:
if not keywords:
return 0
text = " ".join(seg["text"].lower() for seg in segments)
covered = sum(1 for k in keywords if k.lower() in text)
ratio = covered / len(keywords)
if ratio >= 0.8:
return 8
if ratio >= 0.5:
return 5
return 2
def avg(values: List[float]) -> float:
return sum(values) / len(values) if values else 0
def variance(values: List[float]) -> float:
if not values:
return 0
mean = avg(values)
return avg([(v - mean) ** 2 for v in values])
def compute_score(segments: List[dict], metrics: List[dict], duration_ms: int, keywords: List[str]):
word_count = count_words(segments)
wpm = compute_wpm(word_count, duration_ms)
filler_count = count_fillers(segments)
minutes = max(duration_ms / 60000, 0.1)
fillers_per_minute = filler_count / minutes
pause_events = [m.get("pause_ms", 0) for m in metrics if m.get("pause_ms", 0) > 0]
wpm_score = score_wpm(wpm)
filler_score = score_fillers(fillers_per_minute)
pause_score = score_pauses(pause_events, duration_ms)
speech = wpm_score + filler_score + pause_score
eye_contact_pct = avg([m.get("eye_contact", 0) for m in metrics])
smile_avg = avg([m.get("smile", 0) for m in metrics])
yaw_var = variance([m.get("yaw", 0) for m in metrics])
pitch_var = variance([m.get("pitch", 0) for m in metrics])
roll_var = variance([m.get("roll", 0) for m in metrics])
eye_score = score_eye_contact(eye_contact_pct)
smile_score = score_smile(smile_avg)
head_score = score_head_stability(yaw_var, pitch_var, roll_var)
delivery = eye_score + smile_score + head_score
structure_score = score_structure(segments)
repetition_score = score_repetition(segments)
clarity_score = score_clarity(segments)
keyword_score = score_keyword_coverage(segments, keywords)
content = max(0, structure_score + repetition_score + clarity_score + keyword_score)
total = max(0, min(100, speech + delivery + content))
fixes = []
if wpm_score <= 10:
fixes.append("Adjust pace to 120-160 WPM.")
if filler_score <= 8:
fixes.append("Reduce filler words with pauses.")
if eye_score <= 10:
fixes.append("Increase eye contact with the lens.")
if clarity_score <= 8:
fixes.append("Shorten sentences for clarity.")
if keyword_score <= 5 and keywords:
fixes.append("Cover target keywords.")
drill = (
"60-second pace drill"
if speech < delivery and speech < content
else "Eye contact mirror drill"
if delivery < content
else "Story arc outline drill"
)
return {
"total": total,
"speech": speech,
"delivery": delivery,
"content": content,
"details": {
"wpm": wpm_score,
"fillers": filler_score,
"pauses": pause_score,
"eyeContact": eye_score,
"smile": smile_score,
"headStability": head_score,
"structure": structure_score,
"repetition": repetition_score,
"clarity": clarity_score,
"keywordCoverage": keyword_score,
},
"topFixes": fixes[:3],
"recommendedDrill": drill,
}
|