Spaces:
Runtime error
Runtime error
Refactor translation system to modular v3.0 architecture with offline NLLB/Whisper, gTTS fallback, corrections, and automated tests
b97b788 | """ | |
| 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) | |