mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
- Add three-tier LLM architecture (Mistral NeMo, TildeOpen, Gemini fallback) - Fix context window handling for mistral-nemo (128K tokens) - Add LiteLLM context override to prevent 1M token bug - Remove Google Analytics tracking from frontend - Add migration and installation documentation - Optimize for CPU-only inference on 32GB RAM servers Performance improvements: - 95% reduction in API costs - Response times: 5-25 seconds (down from 30-120s) - Better Latvian language quality with TildeOpen - Eliminated timeout errors Architecture changes: - Primary: Mistral NeMo 12B (France, local via Ollama) - Grammar: TildeOpen 30B (Latvia, local via Ollama) - Fallback: Gemini 2.0 Flash (Google API, emergency only) Technical fixes: - Fixed LiteLLM reporting incorrect 1M token context (actual: 128K) - Created mistral-nemo:128k model with proper num_ctx parameter - Added context window override in backend utils - Comprehensive security patterns in .gitignore Documentation: - MIGRATION_LOCAL_LLM.md: Complete architecture and history - INSTALLATION_LOCAL_LLM.md: Step-by-step deployment guide - PR_DESCRIPTION.md: Detailed PR description - sync-from-production.sh: Secure deployment script Tested on production at https://ai.kapteinis.lv since November 17, 2025. Co-authored-by: Ojārs Kapteiņš <ojars@kapteinis.lv> Co-authored-by: Claude AI Assistant <code@anthropic.com>
261 lines
9.9 KiB
Python
261 lines
9.9 KiB
Python
"""
|
||
Language detection service for European languages.
|
||
Detects which of the 34 European languages supported by TildeOpen a text is written in.
|
||
"""
|
||
|
||
import re
|
||
from typing import Optional
|
||
|
||
|
||
# Language detection patterns
|
||
# Each language has common words and character patterns
|
||
LANGUAGE_PATTERNS = {
|
||
# Baltic languages
|
||
"lv": { # Latvian
|
||
"name": "Latvian",
|
||
"keywords": ["un", "ir", "var", "kas", "ar", "par", "no", "uz", "vai", "kā", "bet", "jā", "nē", "es", "tu", "viņš", "mēs"],
|
||
"chars": "āčēģīķļņšūž",
|
||
},
|
||
"lt": { # Lithuanian
|
||
"name": "Lithuanian",
|
||
"keywords": ["ir", "kad", "su", "kas", "yra", "bet", "tai", "jo", "ar", "į", "iš", "per", "taip", "ne", "aš", "tu"],
|
||
"chars": "ąčęėįšųūž",
|
||
},
|
||
"et": { # Estonian
|
||
"name": "Estonian",
|
||
"keywords": ["ja", "on", "ei", "see", "kas", "või", "kui", "et", "mis", "kes", "ma", "sa", "ta", "me", "te", "nad"],
|
||
"chars": "äöüõšž",
|
||
},
|
||
# Slavic languages
|
||
"pl": { # Polish
|
||
"name": "Polish",
|
||
"keywords": ["i", "w", "na", "z", "do", "nie", "się", "jest", "to", "o", "że", "ale", "jak", "czy", "tak", "co"],
|
||
"chars": "ąćęłńóśźż",
|
||
},
|
||
"cs": { # Czech
|
||
"name": "Czech",
|
||
"keywords": ["a", "je", "v", "na", "se", "to", "z", "o", "že", "s", "pro", "není", "jak", "ale", "jsem", "být"],
|
||
"chars": "áčďéěíňóřšťúůýž",
|
||
},
|
||
"sk": { # Slovak
|
||
"name": "Slovak",
|
||
"keywords": ["a", "je", "v", "na", "sa", "to", "z", "o", "že", "s", "ako", "nie", "by", "som", "ale", "pre"],
|
||
"chars": "áäčďéíľňóôŕšťúýž",
|
||
},
|
||
"sl": { # Slovenian
|
||
"name": "Slovenian",
|
||
"keywords": ["in", "je", "v", "na", "z", "da", "se", "ne", "s", "o", "ki", "so", "za", "ali", "kot", "pa"],
|
||
"chars": "čšž",
|
||
},
|
||
"hr": { # Croatian
|
||
"name": "Croatian",
|
||
"keywords": ["i", "u", "je", "na", "se", "da", "s", "za", "ne", "o", "su", "kao", "iz", "ili", "biti", "koji"],
|
||
"chars": "čćđšž",
|
||
},
|
||
"sr": { # Serbian
|
||
"name": "Serbian",
|
||
"keywords": ["i", "u", "je", "na", "se", "da", "s", "za", "ne", "o", "su", "kao", "iz", "ili", "biti", "koji"],
|
||
"chars": "čćđšž",
|
||
},
|
||
"bs": { # Bosnian
|
||
"name": "Bosnian",
|
||
"keywords": ["i", "u", "je", "na", "se", "da", "s", "za", "ne", "o", "su", "kao", "iz", "ili", "biti", "koji"],
|
||
"chars": "čćđšž",
|
||
},
|
||
"bg": { # Bulgarian
|
||
"name": "Bulgarian",
|
||
"keywords": ["и", "в", "на", "е", "не", "за", "да", "с", "от", "се", "че", "като", "то", "са", "по", "но"],
|
||
"chars": "абвгдежзийклмнопрстуфхцчшщъьюя",
|
||
},
|
||
"mk": { # Macedonian
|
||
"name": "Macedonian",
|
||
"keywords": ["и", "во", "на", "е", "не", "за", "да", "со", "од", "се", "што", "како", "тоа", "се", "по", "но"],
|
||
"chars": "абвгдѓежзѕијклљмнњопрстќуфхцчџш",
|
||
},
|
||
"ru": { # Russian
|
||
"name": "Russian",
|
||
"keywords": ["и", "в", "не", "на", "я", "что", "он", "с", "а", "как", "это", "то", "все", "она", "так", "его"],
|
||
"chars": "абвгдеёжзийклмнопрстуфхцчшщъыьэюя",
|
||
},
|
||
"uk": { # Ukrainian
|
||
"name": "Ukrainian",
|
||
"keywords": ["і", "в", "не", "на", "що", "з", "у", "та", "він", "це", "як", "до", "за", "я", "по", "але"],
|
||
"chars": "абвгґдеєжзиіїйклмнопрстуфхцчшщьюя",
|
||
},
|
||
# Romance languages
|
||
"ro": { # Romanian
|
||
"name": "Romanian",
|
||
"keywords": ["și", "în", "de", "la", "cu", "a", "pentru", "ce", "este", "că", "din", "pe", "nu", "sau", "dar", "mai"],
|
||
"chars": "ăâîșț",
|
||
},
|
||
"it": { # Italian
|
||
"name": "Italian",
|
||
"keywords": ["e", "di", "il", "la", "che", "a", "è", "per", "in", "un", "una", "non", "con", "le", "si", "da"],
|
||
"chars": "àèéìòù",
|
||
},
|
||
"es": { # Spanish
|
||
"name": "Spanish",
|
||
"keywords": ["y", "de", "el", "la", "que", "a", "en", "es", "un", "por", "con", "no", "una", "para", "los", "se"],
|
||
"chars": "áéíñóú",
|
||
},
|
||
"pt": { # Portuguese
|
||
"name": "Portuguese",
|
||
"keywords": ["e", "de", "o", "a", "que", "é", "do", "da", "em", "um", "para", "com", "não", "uma", "os", "no"],
|
||
"chars": "ãáàâçéêíóôõú",
|
||
},
|
||
"fr": { # French
|
||
"name": "French",
|
||
"keywords": ["et", "de", "le", "la", "à", "un", "une", "est", "en", "que", "pour", "dans", "ce", "il", "qui", "ne"],
|
||
"chars": "àâçéèêëîïôùûü",
|
||
},
|
||
# Germanic languages
|
||
"de": { # German
|
||
"name": "German",
|
||
"keywords": ["und", "der", "die", "das", "in", "ist", "zu", "den", "mit", "von", "ein", "eine", "nicht", "sich", "auf", "für"],
|
||
"chars": "äöüß",
|
||
},
|
||
"nl": { # Dutch
|
||
"name": "Dutch",
|
||
"keywords": ["de", "het", "een", "van", "en", "in", "is", "dat", "op", "te", "voor", "met", "niet", "zijn", "aan", "er"],
|
||
"chars": "ëïé",
|
||
},
|
||
"sv": { # Swedish
|
||
"name": "Swedish",
|
||
"keywords": ["och", "i", "att", "det", "som", "är", "en", "på", "för", "av", "med", "till", "den", "ett", "har", "om"],
|
||
"chars": "åäö",
|
||
},
|
||
"da": { # Danish
|
||
"name": "Danish",
|
||
"keywords": ["og", "i", "at", "det", "er", "en", "til", "på", "som", "af", "for", "med", "ikke", "har", "den", "de"],
|
||
"chars": "æøå",
|
||
},
|
||
"no": { # Norwegian
|
||
"name": "Norwegian",
|
||
"keywords": ["og", "i", "det", "er", "til", "en", "på", "som", "at", "av", "for", "med", "ikke", "har", "den", "de"],
|
||
"chars": "æøå",
|
||
},
|
||
"is": { # Icelandic
|
||
"name": "Icelandic",
|
||
"keywords": ["og", "í", "að", "er", "sem", "á", "til", "um", "með", "fyrir", "ekki", "en", "það", "við", "af", "var"],
|
||
"chars": "áðéíóúýþæö",
|
||
},
|
||
# Finno-Ugric languages
|
||
"fi": { # Finnish
|
||
"name": "Finnish",
|
||
"keywords": ["ja", "on", "ei", "se", "että", "oli", "olla", "joka", "mutta", "tai", "kun", "vain", "niin", "kuin", "jos", "hän"],
|
||
"chars": "äö",
|
||
},
|
||
"hu": { # Hungarian
|
||
"name": "Hungarian",
|
||
"keywords": ["a", "az", "és", "is", "volt", "van", "hogy", "nem", "egy", "mint", "azt", "már", "csak", "még", "ami", "volt"],
|
||
"chars": "áéíóöőúüű",
|
||
},
|
||
# Greek
|
||
"el": { # Greek
|
||
"name": "Greek",
|
||
"keywords": ["και", "το", "της", "την", "του", "στο", "με", "για", "από", "που", "είναι", "στη", "στην", "ότι", "δεν", "τα"],
|
||
"chars": "αβγδεζηθικλμνξοπρστυφχψω",
|
||
},
|
||
# Maltese
|
||
"mt": { # Maltese
|
||
"name": "Maltese",
|
||
"keywords": ["u", "li", "ta", "fl", "għal", "ma", "il", "tal", "f", "b", "minn", "jew", "kif", "bħal", "meta", "għax"],
|
||
"chars": "ċġħż",
|
||
},
|
||
# Turkish
|
||
"tr": { # Turkish
|
||
"name": "Turkish",
|
||
"keywords": ["ve", "bir", "bu", "da", "de", "ile", "için", "mi", "ne", "var", "daha", "olarak", "çok", "gibi", "ancak", "ya"],
|
||
"chars": "çğıöşü",
|
||
},
|
||
# Albanian
|
||
"sq": { # Albanian
|
||
"name": "Albanian",
|
||
"keywords": ["dhe", "i", "të", "e", "në", "për", "me", "që", "një", "nga", "është", "si", "por", "ka", "u", "nga"],
|
||
"chars": "çë",
|
||
},
|
||
# English (for completeness, but won't trigger grammar check)
|
||
"en": { # English
|
||
"name": "English",
|
||
"keywords": ["the", "and", "is", "to", "of", "a", "in", "that", "it", "was", "for", "on", "are", "with", "as", "be"],
|
||
"chars": "",
|
||
},
|
||
}
|
||
|
||
|
||
def detect_language(text: str) -> Optional[str]:
|
||
"""
|
||
Detect which European language a text is written in.
|
||
|
||
Args:
|
||
text: The text to analyze
|
||
|
||
Returns:
|
||
ISO 639-1 language code (e.g., 'lv', 'et', 'pl') or None if no language detected
|
||
"""
|
||
if not text or len(text) < 10:
|
||
return None
|
||
|
||
# Require at least 3 words
|
||
words = re.findall(r'\w+', text.lower())
|
||
if len(words) < 3:
|
||
return None
|
||
|
||
text_lower = text.lower()
|
||
|
||
# Score each language
|
||
scores = {}
|
||
|
||
for lang_code, lang_data in LANGUAGE_PATTERNS.items():
|
||
score = 0
|
||
|
||
# Check for special characters
|
||
if lang_data["chars"]:
|
||
for char in lang_data["chars"]:
|
||
if char in text_lower:
|
||
score += 3 # Special chars are strong indicators
|
||
|
||
# Check for common keywords
|
||
keyword_matches = 0
|
||
for keyword in lang_data["keywords"]:
|
||
# Count occurrences as whole words
|
||
pattern = r'\b' + re.escape(keyword) + r'\b'
|
||
matches = len(re.findall(pattern, text_lower))
|
||
if matches > 0:
|
||
keyword_matches += 1
|
||
score += matches * 2
|
||
|
||
# Require at least 2 keyword matches for short texts or 3 for longer
|
||
min_keyword_matches = 2 if len(words) < 20 else 3
|
||
if keyword_matches < min_keyword_matches:
|
||
score = 0
|
||
|
||
scores[lang_code] = score
|
||
|
||
# Find the highest scoring language
|
||
if not scores:
|
||
return None
|
||
|
||
max_score = max(scores.values())
|
||
if max_score == 0:
|
||
return None
|
||
|
||
# Get the language with highest score
|
||
detected_lang = max(scores.items(), key=lambda x: x[1])[0]
|
||
|
||
return detected_lang
|
||
|
||
|
||
def get_language_name(lang_code: str) -> str:
|
||
"""
|
||
Get the full name of a language from its code.
|
||
|
||
Args:
|
||
lang_code: ISO 639-1 language code
|
||
|
||
Returns:
|
||
Full language name or the code if not found
|
||
"""
|
||
if lang_code in LANGUAGE_PATTERNS:
|
||
return LANGUAGE_PATTERNS[lang_code]["name"]
|
||
return lang_code
|