ClaudBarbara commited on
Commit
3dadb97
Β·
verified Β·
1 Parent(s): 2a91394

Create confidence_scoring_v2.py

Browse files
Files changed (1) hide show
  1. confidence_scoring_v2.py +677 -0
confidence_scoring_v2.py ADDED
@@ -0,0 +1,677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Confidence Scoring Module v2 for Khmer Legal Bridge
3
+ ====================================================
4
+
5
+ This module provides scientifically-validated confidence scoring using:
6
+ 1. COMETKiwi (PRIMARY) - Reference-free neural QE, correlates with human judgments
7
+ 2. Back-Translation Verification (SECONDARY) - Catches semantic drift
8
+ 3. Legal Terminology Coverage (DOMAIN) - Domain-specific quality signal
9
+
10
+ Based on findings from:
11
+ - Fomicheva et al. (2020) - Token probabilities don't correlate with quality
12
+ - Rei et al. (2022) - COMETKiwi for reference-free QE
13
+ - WMT 2022/2023 - COMETKiwi winning system
14
+
15
+ Author: Khmer Legal Bridge Project
16
+ License: MIT
17
+ """
18
+
19
+ import logging
20
+ import re
21
+ from dataclasses import dataclass, field
22
+ from typing import Dict, List, Optional, Tuple, Callable
23
+ from difflib import SequenceMatcher
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # ============================================================================
28
+ # Data Classes
29
+ # ============================================================================
30
+
31
+ @dataclass
32
+ class ConfidenceResult:
33
+ """Result of confidence scoring with full transparency."""
34
+ overall_score: float # 0.0 to 1.0
35
+ quality_level: str # "excellent", "good", "acceptable", "low", "very_low"
36
+ human_review_recommended: bool
37
+ components: Dict[str, float] # Individual signal scores
38
+ explanations: List[str] # Human-readable explanations
39
+ word_level_scores: Optional[List[Tuple[str, float]]] = None
40
+
41
+
42
+ # ============================================================================
43
+ # COMETKiwi Scorer (PRIMARY)
44
+ # ============================================================================
45
+
46
+ class COMETKiwiScorer:
47
+ """
48
+ Quality Estimation using COMETKiwi (reference-free).
49
+
50
+ This is the PRIMARY confidence method - scientifically validated
51
+ to correlate with human judgments even for low-resource languages.
52
+
53
+ Supports Khmer (khm_Khmr) natively.
54
+ """
55
+
56
+ def __init__(self, model_name: str = "Unbabel/wmt22-cometkiwi-da"):
57
+ """
58
+ Initialize COMETKiwi model.
59
+
60
+ Args:
61
+ model_name: HuggingFace model identifier
62
+ - "Unbabel/wmt22-cometkiwi-da" (560M params, recommended)
63
+ - "Unbabel/wmt23-cometkiwi-da-xl" (3.5B params, requires GPU)
64
+ """
65
+ self.model = None
66
+ self.model_name = model_name
67
+ self._loaded = False
68
+
69
+ def _load_model(self):
70
+ """Lazy load the model to save memory on startup."""
71
+ if self._loaded:
72
+ return
73
+
74
+ try:
75
+ from comet import download_model, load_from_checkpoint
76
+
77
+ logger.info(f"Loading COMETKiwi model: {self.model_name}")
78
+ model_path = download_model(self.model_name)
79
+ self.model = load_from_checkpoint(model_path)
80
+ self._loaded = True
81
+ logger.info("COMETKiwi model loaded successfully")
82
+
83
+ except ImportError:
84
+ logger.warning(
85
+ "unbabel-comet not installed. "
86
+ "Install with: pip install unbabel-comet"
87
+ )
88
+ raise
89
+ except Exception as e:
90
+ logger.error(f"Failed to load COMETKiwi: {e}")
91
+ raise
92
+
93
+ def score(
94
+ self,
95
+ source_text: str,
96
+ translation: str,
97
+ batch_size: int = 1,
98
+ gpus: int = 0 # CPU by default
99
+ ) -> float:
100
+ """
101
+ Score a single translation.
102
+
103
+ Args:
104
+ source_text: Original text
105
+ translation: Translated text
106
+ batch_size: Batch size for inference
107
+ gpus: Number of GPUs (0 for CPU)
108
+
109
+ Returns:
110
+ Quality score between 0.0 and 1.0
111
+ """
112
+ self._load_model()
113
+
114
+ data = [{
115
+ "src": source_text,
116
+ "mt": translation
117
+ }]
118
+
119
+ output = self.model.predict(data, batch_size=batch_size, gpus=gpus)
120
+
121
+ # COMETKiwi returns scores in output.scores (already 0-1 range)
122
+ return float(output.scores[0])
123
+
124
+ def score_batch(
125
+ self,
126
+ pairs: List[Dict[str, str]],
127
+ batch_size: int = 8,
128
+ gpus: int = 0
129
+ ) -> List[float]:
130
+ """
131
+ Score multiple translations efficiently.
132
+
133
+ Args:
134
+ pairs: List of {"src": ..., "mt": ...} dicts
135
+ batch_size: Batch size for inference
136
+ gpus: Number of GPUs
137
+
138
+ Returns:
139
+ List of quality scores
140
+ """
141
+ self._load_model()
142
+
143
+ if not pairs:
144
+ return []
145
+
146
+ output = self.model.predict(pairs, batch_size=batch_size, gpus=gpus)
147
+ return [float(s) for s in output.scores]
148
+
149
+
150
+ # ============================================================================
151
+ # Back-Translation Verifier (SECONDARY)
152
+ # ============================================================================
153
+
154
+ class BackTranslationVerifier:
155
+ """
156
+ Verify translation quality via round-trip translation.
157
+
158
+ Process: source -> translation -> back_translation
159
+ Then compare source with back_translation using semantic similarity.
160
+ """
161
+
162
+ def __init__(self, translator_func: Callable):
163
+ """
164
+ Args:
165
+ translator_func: Function that takes (text, src_lang, tgt_lang)
166
+ and returns translated text
167
+ """
168
+ self.translate = translator_func
169
+
170
+ def verify(
171
+ self,
172
+ source_text: str,
173
+ translation: str,
174
+ src_lang: str,
175
+ tgt_lang: str
176
+ ) -> Dict:
177
+ """
178
+ Perform back-translation verification.
179
+
180
+ Args:
181
+ source_text: Original text
182
+ translation: Forward translation
183
+ src_lang: Source language code (e.g., "eng_Latn")
184
+ tgt_lang: Target language code (e.g., "khm_Khmr")
185
+
186
+ Returns:
187
+ Dict with similarity score and quality flags
188
+ """
189
+ try:
190
+ # Back-translate: tgt_lang -> src_lang
191
+ back_translation = self.translate(translation, tgt_lang, src_lang)
192
+
193
+ # Compute similarity
194
+ similarity = self._compute_similarity(source_text, back_translation)
195
+
196
+ # Determine quality flag
197
+ if similarity >= 0.7:
198
+ quality_flag = "good"
199
+ elif similarity >= 0.5:
200
+ quality_flag = "acceptable"
201
+ elif similarity >= 0.3:
202
+ quality_flag = "concerning"
203
+ else:
204
+ quality_flag = "poor"
205
+
206
+ return {
207
+ "similarity": similarity,
208
+ "back_translation": back_translation,
209
+ "quality_flag": quality_flag
210
+ }
211
+
212
+ except Exception as e:
213
+ logger.warning(f"Back-translation failed: {e}")
214
+ return {
215
+ "similarity": 0.5, # Neutral fallback
216
+ "back_translation": None,
217
+ "quality_flag": "unknown"
218
+ }
219
+
220
+ def _compute_similarity(self, text1: str, text2: str) -> float:
221
+ """
222
+ Compute semantic similarity between two texts.
223
+ Uses character-level similarity as a lightweight proxy.
224
+ """
225
+ # Normalize texts
226
+ t1 = text1.lower().strip()
227
+ t2 = text2.lower().strip()
228
+
229
+ # Use SequenceMatcher for character-level similarity
230
+ # This is a lightweight alternative to embedding-based similarity
231
+ ratio = SequenceMatcher(None, t1, t2).ratio()
232
+
233
+ return ratio
234
+
235
+
236
+ # ============================================================================
237
+ # Legal Terminology Checker (DOMAIN)
238
+ # ============================================================================
239
+
240
+ class LegalTerminologyChecker:
241
+ """
242
+ Check coverage and accuracy of legal terminology.
243
+
244
+ Verifies that domain-specific terms are translated correctly
245
+ according to the legal glossary.
246
+ """
247
+
248
+ def __init__(self, glossary: Dict[str, str]):
249
+ """
250
+ Args:
251
+ glossary: Dict mapping source terms to target terms
252
+ """
253
+ self.glossary = glossary
254
+ # Pre-compile patterns for efficiency
255
+ self._source_patterns = {
256
+ term: re.compile(r'\b' + re.escape(term) + r'\b', re.IGNORECASE)
257
+ for term in glossary.keys()
258
+ }
259
+
260
+ def check(
261
+ self,
262
+ source_text: str,
263
+ translation: str,
264
+ direction: str = "en2km"
265
+ ) -> Dict:
266
+ """
267
+ Check terminology coverage and accuracy.
268
+
269
+ Args:
270
+ source_text: Original text
271
+ translation: Translated text
272
+ direction: "en2km" or "km2en"
273
+
274
+ Returns:
275
+ Dict with coverage score and term details
276
+ """
277
+ found_terms = []
278
+ correct_terms = []
279
+ missing_terms = []
280
+
281
+ # For en2km: source terms are English (glossary keys)
282
+ # For km2en: we'd need reverse glossary
283
+
284
+ for term, expected_translation in self.glossary.items():
285
+ pattern = self._source_patterns.get(term)
286
+ if pattern and pattern.search(source_text):
287
+ found_terms.append(term)
288
+
289
+ # Check if expected translation appears in output
290
+ if expected_translation.lower() in translation.lower():
291
+ correct_terms.append(term)
292
+ else:
293
+ missing_terms.append({
294
+ "term": term,
295
+ "expected": expected_translation
296
+ })
297
+
298
+ # Calculate coverage score
299
+ if found_terms:
300
+ coverage = len(correct_terms) / len(found_terms)
301
+ else:
302
+ coverage = 1.0 # No terms to check = full coverage
303
+
304
+ return {
305
+ "coverage": coverage,
306
+ "found_terms": len(found_terms),
307
+ "correct_terms": len(correct_terms),
308
+ "missing_terms": missing_terms
309
+ }
310
+
311
+
312
+ # ============================================================================
313
+ # Lightweight Scorer (FALLBACK)
314
+ # ============================================================================
315
+
316
+ class LightweightScorer:
317
+ """
318
+ Fast heuristic-based scoring when COMETKiwi is too slow.
319
+
320
+ Uses:
321
+ - Length ratio (source vs translation)
322
+ - Character coverage
323
+ - Basic sanity checks
324
+
325
+ ~50ms per sentence vs ~3-5s for COMETKiwi
326
+ """
327
+
328
+ # Expected length ratios (empirically determined)
329
+ LENGTH_RATIOS = {
330
+ "en2km": (0.8, 2.5), # Khmer is often longer due to syllabic script
331
+ "km2en": (0.4, 1.2) # English is often shorter
332
+ }
333
+
334
+ def score(
335
+ self,
336
+ source_text: str,
337
+ translation: str,
338
+ direction: str = "en2km"
339
+ ) -> Dict:
340
+ """
341
+ Compute lightweight confidence score.
342
+
343
+ Returns:
344
+ Dict with score and explanation
345
+ """
346
+ scores = []
347
+ explanations = []
348
+
349
+ # 1. Length ratio check
350
+ src_len = len(source_text)
351
+ tgt_len = len(translation)
352
+
353
+ if src_len > 0:
354
+ ratio = tgt_len / src_len
355
+ min_ratio, max_ratio = self.LENGTH_RATIOS.get(direction, (0.5, 2.0))
356
+
357
+ if min_ratio <= ratio <= max_ratio:
358
+ length_score = 1.0
359
+ else:
360
+ # Penalize based on how far outside range
361
+ if ratio < min_ratio:
362
+ length_score = max(0.3, ratio / min_ratio)
363
+ explanations.append(f"Translation may be too short (ratio: {ratio:.2f})")
364
+ else:
365
+ length_score = max(0.3, max_ratio / ratio)
366
+ explanations.append(f"Translation may be too long (ratio: {ratio:.2f})")
367
+
368
+ scores.append(length_score)
369
+
370
+ # 2. Empty/trivial check
371
+ if not translation.strip():
372
+ return {
373
+ "score": 0.0,
374
+ "explanations": ["Translation is empty"]
375
+ }
376
+
377
+ # 3. Repetition check
378
+ words = translation.split()
379
+ if len(words) > 3:
380
+ unique_ratio = len(set(words)) / len(words)
381
+ if unique_ratio < 0.3:
382
+ scores.append(0.3)
383
+ explanations.append("High word repetition detected")
384
+ else:
385
+ scores.append(min(1.0, unique_ratio + 0.3))
386
+
387
+ # 4. Script check for Khmer output
388
+ if direction == "en2km":
389
+ khmer_chars = sum(1 for c in translation if '\u1780' <= c <= '\u17FF')
390
+ khmer_ratio = khmer_chars / len(translation) if translation else 0
391
+
392
+ if khmer_ratio < 0.3:
393
+ scores.append(0.5)
394
+ explanations.append("Low Khmer script ratio")
395
+ else:
396
+ scores.append(1.0)
397
+
398
+ # Average all scores
399
+ final_score = sum(scores) / len(scores) if scores else 0.5
400
+
401
+ return {
402
+ "score": final_score,
403
+ "explanations": explanations
404
+ }
405
+
406
+
407
+ # ============================================================================
408
+ # Transparency Scorer (ENSEMBLE)
409
+ # ============================================================================
410
+
411
+ class TransparencyScorer:
412
+ """
413
+ Ensemble confidence scorer combining multiple signals.
414
+
415
+ Weights based on empirical findings from QE literature:
416
+ - COMETKiwi: 60% (primary, correlates with human judgments)
417
+ - Back-translation: 25% (catches semantic drift)
418
+ - Terminology: 15% (domain-specific quality)
419
+ """
420
+
421
+ # Thresholds for quality levels
422
+ THRESHOLDS = {
423
+ "excellent": 0.85,
424
+ "good": 0.70,
425
+ "acceptable": 0.55,
426
+ "low": 0.40
427
+ # Below 0.40 = "very_low"
428
+ }
429
+
430
+ # Weights for ensemble (sum to 1.0)
431
+ WEIGHTS = {
432
+ "comet_kiwi": 0.60,
433
+ "back_translation": 0.25,
434
+ "terminology": 0.15
435
+ }
436
+
437
+ # Legal context requires higher threshold for human review
438
+ LEGAL_REVIEW_THRESHOLD = 0.75
439
+
440
+ def __init__(
441
+ self,
442
+ translator_func: Optional[Callable] = None,
443
+ glossary: Optional[Dict[str, str]] = None,
444
+ use_comet: bool = True,
445
+ use_back_translation: bool = True,
446
+ use_terminology: bool = True
447
+ ):
448
+ """
449
+ Initialize scorer with configurable components.
450
+
451
+ Args:
452
+ translator_func: Translation function for back-translation
453
+ glossary: Legal glossary for terminology checking
454
+ use_comet: Whether to use COMETKiwi
455
+ use_back_translation: Whether to use back-translation verification
456
+ use_terminology: Whether to check legal terminology
457
+ """
458
+ self.comet_scorer = COMETKiwiScorer() if use_comet else None
459
+
460
+ self.bt_verifier = (
461
+ BackTranslationVerifier(translator_func)
462
+ if use_back_translation and translator_func
463
+ else None
464
+ )
465
+
466
+ self.term_checker = (
467
+ LegalTerminologyChecker(glossary)
468
+ if use_terminology and glossary
469
+ else None
470
+ )
471
+
472
+ self.lightweight_scorer = LightweightScorer()
473
+
474
+ def score(
475
+ self,
476
+ source_text: str,
477
+ translation: str,
478
+ direction: str = "en2km",
479
+ detailed: bool = True
480
+ ) -> ConfidenceResult:
481
+ """
482
+ Compute ensemble confidence score.
483
+
484
+ Args:
485
+ source_text: Original text
486
+ translation: Translated text
487
+ direction: "en2km" or "km2en"
488
+ detailed: Whether to compute all signals (slower but more accurate)
489
+
490
+ Returns:
491
+ ConfidenceResult with overall score and breakdown
492
+ """
493
+ components = {}
494
+ explanations = []
495
+
496
+ # 1. COMETKiwi (PRIMARY)
497
+ if self.comet_scorer and detailed:
498
+ try:
499
+ comet_score = self.comet_scorer.score(source_text, translation)
500
+ components["comet_kiwi"] = comet_score
501
+
502
+ if comet_score < 0.5:
503
+ explanations.append(
504
+ f"Neural QE indicates low quality ({comet_score:.2f})"
505
+ )
506
+ elif comet_score > 0.8:
507
+ explanations.append(
508
+ f"Neural QE indicates high quality ({comet_score:.2f})"
509
+ )
510
+ except Exception as e:
511
+ logger.warning(f"COMETKiwi scoring failed: {e}")
512
+ # Fall back to lightweight
513
+ lw_result = self.lightweight_scorer.score(
514
+ source_text, translation, direction
515
+ )
516
+ components["comet_kiwi"] = lw_result["score"]
517
+ explanations.extend(lw_result["explanations"])
518
+ else:
519
+ # Use lightweight scorer as fallback
520
+ lw_result = self.lightweight_scorer.score(
521
+ source_text, translation, direction
522
+ )
523
+ components["comet_kiwi"] = lw_result["score"]
524
+ explanations.extend(lw_result["explanations"])
525
+
526
+ # 2. Back-translation (SECONDARY)
527
+ if self.bt_verifier and detailed:
528
+ try:
529
+ src_lang = "eng_Latn" if direction == "en2km" else "khm_Khmr"
530
+ tgt_lang = "khm_Khmr" if direction == "en2km" else "eng_Latn"
531
+
532
+ bt_result = self.bt_verifier.verify(
533
+ source_text, translation, src_lang, tgt_lang
534
+ )
535
+ components["back_translation"] = bt_result["similarity"]
536
+
537
+ if bt_result["quality_flag"] in ["concerning", "poor"]:
538
+ explanations.append(
539
+ f"Back-translation diverges ({bt_result['quality_flag']}): "
540
+ f"similarity={bt_result['similarity']:.2f}"
541
+ )
542
+ except Exception as e:
543
+ logger.warning(f"Back-translation verification failed: {e}")
544
+ components["back_translation"] = 0.5
545
+ else:
546
+ components["back_translation"] = 0.5 # Neutral
547
+
548
+ # 3. Terminology (DOMAIN)
549
+ if self.term_checker:
550
+ try:
551
+ term_result = self.term_checker.check(
552
+ source_text, translation, direction
553
+ )
554
+ components["terminology"] = term_result["coverage"]
555
+
556
+ if term_result["missing_terms"]:
557
+ missing_list = [t["term"] for t in term_result["missing_terms"][:3]]
558
+ explanations.append(
559
+ f"Missing legal terms: {', '.join(missing_list)}"
560
+ )
561
+ except Exception as e:
562
+ logger.warning(f"Terminology check failed: {e}")
563
+ components["terminology"] = 0.5
564
+ else:
565
+ components["terminology"] = 0.5 # Neutral
566
+
567
+ # Compute weighted average
568
+ overall_score = sum(
569
+ components.get(k, 0.5) * v
570
+ for k, v in self.WEIGHTS.items()
571
+ )
572
+
573
+ # Determine quality level
574
+ quality_level = "very_low"
575
+ for level, threshold in sorted(
576
+ self.THRESHOLDS.items(),
577
+ key=lambda x: x[1],
578
+ reverse=True
579
+ ):
580
+ if overall_score >= threshold:
581
+ quality_level = level
582
+ break
583
+
584
+ # Legal context: recommend human review below threshold
585
+ human_review = overall_score < self.LEGAL_REVIEW_THRESHOLD
586
+
587
+ if human_review and not any("review" in e.lower() for e in explanations):
588
+ explanations.append(
589
+ "Human review recommended for legal accuracy"
590
+ )
591
+
592
+ return ConfidenceResult(
593
+ overall_score=round(overall_score, 3),
594
+ quality_level=quality_level,
595
+ human_review_recommended=human_review,
596
+ components=components,
597
+ explanations=explanations
598
+ )
599
+
600
+ def score_fast(
601
+ self,
602
+ source_text: str,
603
+ translation: str,
604
+ direction: str = "en2km"
605
+ ) -> ConfidenceResult:
606
+ """
607
+ Fast scoring without COMETKiwi (uses lightweight scorer only).
608
+ ~50ms vs ~3-5s for full scoring.
609
+ """
610
+ return self.score(
611
+ source_text, translation, direction, detailed=False
612
+ )
613
+
614
+
615
+ # ============================================================================
616
+ # Utility Functions
617
+ # ============================================================================
618
+
619
+ def create_default_scorer(
620
+ translator_func: Optional[Callable] = None,
621
+ glossary: Optional[Dict[str, str]] = None
622
+ ) -> TransparencyScorer:
623
+ """
624
+ Create a scorer with sensible defaults for Khmer Legal Bridge.
625
+
626
+ Args:
627
+ translator_func: Translation function for back-translation
628
+ glossary: Legal glossary (optional)
629
+
630
+ Returns:
631
+ Configured TransparencyScorer
632
+ """
633
+ return TransparencyScorer(
634
+ translator_func=translator_func,
635
+ glossary=glossary,
636
+ use_comet=True,
637
+ use_back_translation=True,
638
+ use_terminology=bool(glossary)
639
+ )
640
+
641
+
642
+ # Default legal glossary (subset for demo)
643
+ DEFAULT_LEGAL_GLOSSARY = {
644
+ # Criminal procedure
645
+ "arrest": "αž€αžΆαžšαž…αžΆαž”αŸ‹αžαŸ’αž›αž½αž“",
646
+ "detention": "αž€αžΆαžšαžƒαž»αŸ†αžαŸ’αž›αž½αž“",
647
+ "bail": "αž€αžΆαžšαžŠαžΆαž€αŸ‹αž’αžΆαž“αžΆ",
648
+ "prosecutor": "αžšαžŠαŸ’αž‹αž’αžΆαž‡αŸ’αž‰αžΆ",
649
+ "defendant": "αž‡αž“αž‡αžΆαž”αŸ‹αž…αŸ„αž‘",
650
+ "verdict": "αžŸαžΆαž›αž€αŸ’αžšαž˜",
651
+ "sentence": "αž‘αŸ„αžŸ",
652
+ "appeal": "αž”αžŽαŸ’αžŠαžΉαž„αž§αž‘αŸ’αž’αžšαžŽαŸ",
653
+
654
+ # Juvenile justice
655
+ "minor": "αž’αž“αžΈαžαž·αž‡αž“",
656
+ "juvenile": "αž’αž“αžΈαžαž·αž‡αž“",
657
+ "guardian": "αž’αžΆαžŽαžΆαž–αŸ’αž™αžΆαž”αžΆαž›",
658
+ "rehabilitation": "αž€αžΆαžšαž’αž”αŸ‹αžšαŸ†αž€αŸ‚αž”αŸ’αžšαŸ‚",
659
+ "diversion": "αž€αžΆαžšαž”αž„αŸ’αžœαŸ‚αžšαž€αŸ’αžšαŸ…αž”αŸ’αžšαž–αŸαž“αŸ’αž’αžαž»αž›αžΆαž€αžΆαžš",
660
+
661
+ # Human rights
662
+ "asylum": "αžŸαž·αž‘αŸ’αž’αž·αž‡αŸ’αžšαž€αž€αŸ„αž“",
663
+ "refugee": "αž‡αž“αž—αŸ€αžŸαžαŸ’αž›αž½αž“",
664
+ "persecution": "αž€αžΆαžšαž’αŸ’αžœαžΎαž‘αžΆαžšαž»αžŽαž€αž˜αŸ’αž˜",
665
+ "torture": "αž‘αžΆαžšαž»αžŽαž€αž˜αŸ’αž˜",
666
+ "due process": "αž“αžΈαžαž·αžœαž·αž’αžΈαžαŸ’αžšαžΉαž˜αžαŸ’αžšαžΌαžœ",
667
+
668
+ # General legal
669
+ "court": "αžαž»αž›αžΆαž€αžΆαžš",
670
+ "judge": "αž…αŸ…αž€αŸ’αžšαž˜",
671
+ "lawyer": "αž˜αŸαž’αžΆαžœαžΈ",
672
+ "evidence": "αž—αžŸαŸ’αžαž»αžαžΆαž„",
673
+ "witness": "αžŸαžΆαž€αŸ’αžŸαžΈ",
674
+ "testimony": "αžŸαž€αŸ’αžαžΈαž€αž˜αŸ’αž˜",
675
+ "rights": "αžŸαž·αž‘αŸ’αž’αž·",
676
+ "law": "αž…αŸ’αž”αžΆαž”αŸ‹",
677
+ }