MIC-Off / tests /test_all.py
VethaNarayananG's picture
Refactor translation system to modular v3.0 architecture with offline NLLB/Whisper, gTTS fallback, corrections, and automated tests
b97b788
Raw
History Blame Contribute Delete
17.7 kB
"""
tests/test_all.py — Automated tests for MIC Translator
Tests: Speech Recognition, Translation, Language Detection,
Text-to-Speech, History, Dataset Loading, Correction Engine
"""
import sys
import os
import json
import time
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import NLLB_LANGS, LANGUAGE_NAMES, NLLB_TO_ISO
from language_detector import LanguageDetector
from correction_engine import CorrectionEngine
from dataset_loader import DatasetLoader
from history import TranslationHistory
# ═══════════════════════════════════════════════════════════════════════════════
# TEST UTILITIES
# ═══════════════════════════════════════════════════════════════════════════════
PASS = 0
FAIL = 0
SKIP = 0
def test(name, condition, detail=""):
global PASS, FAIL
if condition:
PASS += 1
print(f" [PASS] {name}")
else:
FAIL += 1
print(f" [FAIL] {name} {f'- {detail}' if detail else ''}")
def skip(name, reason=""):
global SKIP
SKIP += 1
print(f" [SKIP] {name} (skipped: {reason})")
def section(title):
print(f"\n{'-'*50}")
print(f" {title}")
print(f"{'-'*50}")
# ═══════════════════════════════════════════════════════════════════════════════
# TEST 1: Configuration
# ═══════════════════════════════════════════════════════════════════════════════
def test_config():
section("1. Configuration & Language Map")
test("NLLB_LANGS has 100+ languages", len(NLLB_LANGS) >= 100,
f"got {len(NLLB_LANGS)}")
test("LANGUAGE_NAMES matches NLLB_LANGS count",
len(LANGUAGE_NAMES) == len(NLLB_LANGS),
f"names={len(LANGUAGE_NAMES)}, codes={len(NLLB_LANGS)}")
test("NLLB_TO_ISO reverse lookup works",
NLLB_TO_ISO.get("eng_Latn") == "en")
# Key languages must be present
key_langs = ["en", "fr", "de", "es", "hi", "ta", "zh", "ja",
"ko", "ar", "ru", "pt", "it", "bn", "ur"]
for lang in key_langs:
test(f"Language '{lang}' ({LANGUAGE_NAMES.get(lang, '?')}) is supported",
lang in NLLB_LANGS)
test("All NLLB codes have underscore format",
all("_" in v for v in NLLB_LANGS.values()))
test("No duplicate NLLB codes",
len(set(NLLB_LANGS.values())) == len(NLLB_LANGS))
# ═══════════════════════════════════════════════════════════════════════════════
# TEST 2: Language Detection
# ═══════════════════════════════════════════════════════════════════════════════
def test_language_detection():
section("2. Language Detection")
det = LanguageDetector()
# English (Latin)
r = det.detect_from_text("Hello, how are you today?")
test("Detects English from Latin text", r["language"] == "en", f"got {r['language']}")
# Hindi (Devanagari)
r = det.detect_from_text("नमस्ते, आप कैसे हैं?")
test("Detects Hindi from Devanagari", r["language"] == "hi", f"got {r['language']}")
# Tamil
r = det.detect_from_text("வணக்கம், நீங்கள் எப்படி இருக்கிறீர்கள்?")
test("Detects Tamil from Tamil script", r["language"] == "ta", f"got {r['language']}")
# Arabic
r = det.detect_from_text("مرحبا كيف حالك")
test("Detects Arabic from Arabic script", r["language"] == "ar", f"got {r['language']}")
# Japanese
r = det.detect_from_text("こんにちは、元気ですか?")
test("Detects Japanese from Hiragana", r["language"] == "ja", f"got {r['language']}")
# Korean
r = det.detect_from_text("안녕하세요, 어떻게 지내세요?")
test("Detects Korean from Hangul", r["language"] == "ko", f"got {r['language']}")
# Chinese
r = det.detect_from_text("你好世界")
test("Detects Chinese from CJK", r["language"] == "zh", f"got {r['language']}")
# Bengali
r = det.detect_from_text("হ্যালো, কেমন আছেন?")
test("Detects Bengali from Bengali script", r["language"] == "bn", f"got {r['language']}")
# Telugu
r = det.detect_from_text("హలో, మీరు ఎలా ఉన్నారు?")
test("Detects Telugu from Telugu script", r["language"] == "te", f"got {r['language']}")
# Empty text
r = det.detect_from_text("")
test("Empty text defaults to English", r["language"] == "en")
# NLLB code lookup
test("get_nllb_code('en') -> eng_Latn", det.get_nllb_code("en") == "eng_Latn")
test("get_nllb_code('ta') -> tam_Taml", det.get_nllb_code("ta") == "tam_Taml")
test("get_nllb_code('ja') -> jpn_Jpan", det.get_nllb_code("ja") == "jpn_Jpan")
test("is_supported('en') -> True", det.is_supported("en"))
test("is_supported('xyz') -> False", not det.is_supported("xyz"))
all_langs = det.get_all_languages()
test("get_all_languages returns 100+", len(all_langs) >= 100)
# ═══════════════════════════════════════════════════════════════════════════════
# TEST 3: Correction Engine
# ═══════════════════════════════════════════════════════════════════════════════
def test_correction_engine():
section("3. Correction Engine")
ce = CorrectionEngine()
ce.load()
test("Corrections loaded", ce.loaded)
test("Slang entries > 30", len(ce.slang_map) > 30, f"got {len(ce.slang_map)}")
test("Short forms > 20", len(ce.short_forms) > 20, f"got {len(ce.short_forms)}")
test("Confused words > 10", len(ce.confused_words) > 10)
# Pre-translation corrections
r = ce.pre_translate("I gonna go to the store", "en")
test("Slang: 'gonna' -> 'going to'", "going to" in r["text"], r["text"])
r = ce.pre_translate("I wanna eat something", "en")
test("Slang: 'wanna' -> 'want to'", "want to" in r["text"], r["text"])
r = ce.pre_translate("idk what to do tbh", "en")
test("Slang: 'idk' expanded", "don't know" in r["text"].lower(), r["text"])
r = ce.pre_translate("hellooo how are you", "en")
test("Pre-correction: 'hellooo' -> 'hello'", "hello" in r["text"].lower(), r["text"])
r = ce.pre_translate("Normal sentence here.", "en")
test("No corrections on clean text", not r["corrected"])
# Stats
stats = ce.get_stats()
test("Stats returns total > 50", stats["total"] > 50, f"got {stats['total']}")
# Add custom correction
ok = ce.add_correction("slang", "yolo", "you only live once")
test("Can add custom correction", ok)
r = ce.pre_translate("yolo my friend", "en")
test("Custom correction applied", "you only live once" in r["text"], r["text"])
# ═══════════════════════════════════════════════════════════════════════════════
# TEST 4: Dataset Loader
# ═══════════════════════════════════════════════════════════════════════════════
def test_dataset_loader():
section("4. Dataset Loader")
dl = DatasetLoader()
dl.load_all()
stats = dl.get_stats()
test("Dataset loader initializes", stats is not None)
test("Datasets directory created",
os.path.exists(os.path.join(os.path.dirname(os.path.dirname(__file__)), "datasets")))
# Test with a sample custom dataset
custom_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)),
"datasets", "custom")
sample_file = os.path.join(custom_dir, "test_sample.json")
sample_data = [
{"source_lang": "en", "target_lang": "fr",
"source_text": "Hello", "target_text": "Bonjour"},
{"source_lang": "en", "target_lang": "fr",
"source_text": "Good morning", "target_text": "Bonjour"},
]
os.makedirs(custom_dir, exist_ok=True)
with open(sample_file, "w", encoding="utf-8") as f:
json.dump(sample_data, f)
# Reload
dl2 = DatasetLoader()
dl2.load_all()
ref = dl2.get_reference("Hello", "en", "fr")
test("Reference lookup works", ref == "Bonjour", f"got {ref}")
val = dl2.validate_translation("Hello", "Bonjour", "en", "fr")
test("Validation: exact match", val["match"] is True)
test("Validation: quality_score = 1.0", val["quality_score"] == 1.0)
val2 = dl2.validate_translation("Hello", "Salut", "en", "fr")
test("Validation: non-match detected", val2["match"] is False)
# Cleanup
try:
os.unlink(sample_file)
except OSError:
pass
# ═══════════════════════════════════════════════════════════════════════════════
# TEST 5: History
# ═══════════════════════════════════════════════════════════════════════════════
def test_history():
section("5. Translation History")
h = TranslationHistory()
h.entries = []
h.favorites = []
h.recent_langs = []
# Add entries
h.add({
"original": "Hello",
"translated": "Bonjour",
"source_lang": "en",
"target_lang": "fr",
"time_ms": 150
})
test("Add entry works", len(h.entries) == 1)
test("Entry has timestamp", "timestamp" in h.entries[0])
# Recent languages
test("Recent langs updated", "fr" in h.recent_langs)
# Favorites
h.add_favorite("ta")
test("Add favorite works", "ta" in h.get_favorites())
h.remove_favorite("ta")
test("Remove favorite works", "ta" not in h.get_favorites())
# Search
h.add({
"original": "Good morning",
"translated": "Buenos días",
"source_lang": "en",
"target_lang": "es",
"time_ms": 200
})
results = h.search("morning")
test("Search works", len(results) == 1)
results = h.search("xyz_not_found")
test("Search returns empty for no match", len(results) == 0)
# Export
csv_str = h.export_csv()
test("CSV export works", "Hello" in csv_str and "Bonjour" in csv_str)
json_str = h.export_json()
test("JSON export works", "Hello" in json_str)
# Stats
stats = h.get_stats()
test("Stats: total = 2", stats["total_translations"] == 2)
test("Stats: avg_time > 0", stats["avg_time_ms"] > 0)
# Clear
h.clear()
test("Clear works", len(h.entries) == 0)
# ═══════════════════════════════════════════════════════════════════════════════
# TEST 6: Speech Recognition (requires Whisper model)
# ═══════════════════════════════════════════════════════════════════════════════
def test_speech():
section("6. Speech Recognition (Whisper)")
try:
from speech import SpeechRecognizer
sr = SpeechRecognizer()
sr.load()
test("Whisper model loads", sr.ready)
# Test with existing test.wav if available
test_wav = os.path.join(os.path.dirname(os.path.dirname(__file__)), "test.wav")
if os.path.exists(test_wav) and sr.ready:
result = sr.transcribe(test_wav)
test("Transcription produces text", len(result.get("text", "")) > 0,
f"text='{result.get('text', '')}'")
test("Language detected", result.get("language") is not None,
f"lang={result.get('language')}")
else:
skip("Transcription test", "no test.wav found or model not ready")
except Exception as e:
skip("Whisper tests", str(e))
# ═══════════════════════════════════════════════════════════════════════════════
# TEST 7: Translation (requires NLLB model)
# ═══════════════════════════════════════════════════════════════════════════════
def test_translation():
section("7. Translation (NLLB-200)")
try:
from translator import Translator
t = Translator()
t.load()
test("NLLB model loads", t.ready)
if t.ready:
# English -> French
r = t.translate("Hello, how are you?", "en", "fr")
test("EN->FR translation works", len(r["translated"]) > 0 and r["error"] is None,
r.get("translated", ""))
test("Translation time recorded", r["time_ms"] > 0)
# English -> Hindi
r = t.translate("Good morning", "en", "hi")
test("EN->HI translation works", len(r["translated"]) > 0 and r["error"] is None,
r.get("translated", ""))
# English -> Tamil
r = t.translate("Thank you", "en", "ta")
test("EN->TA translation works", len(r["translated"]) > 0 and r["error"] is None,
r.get("translated", ""))
# Same language (no-op)
r = t.translate("Test", "en", "en")
test("Same-lang returns original", r["translated"] == "Test")
# Unsupported language
r = t.translate("Test", "xyz", "en")
test("Unsupported lang returns error", r["error"] is not None)
# Language count
test("200+ languages supported", t.get_language_count() >= 100)
else:
skip("Translation tests", "NLLB model not loaded")
except Exception as e:
skip("Translation tests", str(e))
# ═══════════════════════════════════════════════════════════════════════════════
# TEST 8: Text-to-Speech
# ═══════════════════════════════════════════════════════════════════════════════
def test_tts():
section("8. Text-to-Speech (Piper)")
try:
from tts import TextToSpeech
t = TextToSpeech()
t.load()
test("TTS initializes", t.ready)
voices = t.get_available_voices()
test("Voices discovered", len(voices) >= 0, f"voices={list(voices.keys())}")
stats = t.get_stats()
test("Stats returns data", stats is not None)
if t.piper_available and voices:
r = t.synthesize("Hello world", "en")
test("EN synthesis works", r.get("success") is True, r.get("error", ""))
else:
skip("TTS synthesis test", "Piper not installed or no voice models")
except Exception as e:
skip("TTS tests", str(e))
# ═══════════════════════════════════════════════════════════════════════════════
# RUN ALL TESTS
# ═══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
print("\n" + "=" * 50)
print(" MIC Translator - Automated Test Suite")
print("=" * 50)
start = time.time()
test_config()
test_language_detection()
test_correction_engine()
test_dataset_loader()
test_history()
test_speech()
test_translation()
test_tts()
elapsed = round(time.time() - start, 1)
print(f"\n{'='*50}")
print(f" RESULTS: {PASS} passed | {FAIL} failed | {SKIP} skipped")
print(f" Time: {elapsed}s")
print(f"{'='*50}\n")
sys.exit(1 if FAIL > 0 else 0)