Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Ψ£ΩΩ ΩΨ± Ψ¨ΩΩΨͺΩΩΩ ΨͺΩΨ΅ΩΨ±ΩΩΩ β BI MORPHOLOGICAL ENGINE | |
| Phase 0 of the BI track. Mirror of amr_tasrif.py (AA track). | |
| BI ΨͺΩΨ΅ΩΨ±ΩΩΩ works by AGGLUTINATION β suffixes stack on a root: | |
| kor (see) + -gan (participle) + -lar (plural) = qorganlar | |
| Three layers of BI ΨͺΩΨ΅ΩΨ±ΩΩΩ: | |
| Layer 1 β VERB PATTERNS: 8 suffix types (bitig_verb_tasrif) | |
| Layer 2 β NOUN PATTERNS: 6 suffix types (bitig_noun_tasrif) | |
| Layer 3 β CASE: 6 case suffixes (bitig_case_tasrif) | |
| Layer 4 β GRAMMAR: 10 patterns β possessive, voice, negation, question, | |
| vowel harmony (bitig_grammar_tasrif) | |
| Layer 5 β COMPOUND: 8 compound formation rules (bitig_compound_rules) | |
| Every suffix checked against Kashgari (1072 CE) headword frequency. | |
| Vowel harmony (BG10) is the structural law β violation = contamination signal. | |
| Usage: | |
| python3 amr_bitig_tasrif.py status # coverage stats | |
| python3 amr_bitig_tasrif.py analyze "qorganlar" # decompose word | |
| python3 amr_bitig_tasrif.py root "kor" # all forms of root | |
| python3 amr_bitig_tasrif.py pattern BV01 # explain pattern | |
| python3 amr_bitig_tasrif.py harmony "qorganlar" # check vowel harmony | |
| python3 amr_bitig_tasrif.py compound "qara tas" # analyze compound | |
| """ | |
| import sys | |
| import os | |
| import sqlite3 | |
| import re | |
| try: | |
| from uslap_db_connect import DB_PATH | |
| except ImportError: | |
| DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uslap_database_v3.db") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # BI PHONOLOGY CONSTANTS β sourced from amr_bitig_alphabet if available | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| from amr_bitig_alphabet import HARMONY_PAIRS, HARMONY_NEUTRAL | |
| _HAS_BITIG_ALPHABET = True | |
| except ImportError: | |
| _HAS_BITIG_ALPHABET = False | |
| BACK_VOWELS = set('aΔ±ou') | |
| FRONT_VOWELS = set('eiΓΆΓΌ') | |
| ALL_VOWELS = BACK_VOWELS | FRONT_VOWELS | |
| # Vowel harmony pairing: back β front | |
| HARMONY_MAP = { | |
| 'a': 'e', 'e': 'a', | |
| 'Δ±': 'i', 'i': 'Δ±', | |
| 'o': 'ΓΆ', 'ΓΆ': 'o', | |
| 'u': 'ΓΌ', 'ΓΌ': 'u', | |
| } | |
| def _connect(db_path=None): | |
| path = db_path or DB_PATH | |
| conn = sqlite3.connect(path) | |
| conn.row_factory = sqlite3.Row | |
| return conn | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LAYER 0: VOWEL HARMONY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_root_harmony(word): | |
| """Determine vowel harmony class of a word from its vowels. | |
| Returns 'BACK', 'FRONT', 'MIXED' (contamination signal), or 'NONE'. | |
| """ | |
| word_lower = word.lower() | |
| has_back = any(v in word_lower for v in BACK_VOWELS) | |
| has_front = any(v in word_lower for v in FRONT_VOWELS) | |
| if has_back and has_front: | |
| return 'MIXED' # violation β contamination signal | |
| elif has_back: | |
| return 'BACK' | |
| elif has_front: | |
| return 'FRONT' | |
| return 'NONE' | |
| def check_harmony(word): | |
| """Full vowel harmony check. | |
| Returns (harmony_class, is_valid, violations). | |
| """ | |
| harmony = get_root_harmony(word) | |
| violations = [] | |
| if harmony == 'MIXED': | |
| # Find which vowels conflict | |
| word_lower = word.lower() | |
| backs = [v for v in word_lower if v in BACK_VOWELS] | |
| fronts = [v for v in word_lower if v in FRONT_VOWELS] | |
| violations.append( | |
| f'MIXED HARMONY: back vowels {backs} and front vowels {fronts} ' | |
| f'in same word. BI words obey vowel harmony (BG10). ' | |
| f'Violation = contamination signal.' | |
| ) | |
| return (harmony, len(violations) == 0, violations) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LAYER 1-4: SUFFIX DECOMPOSITION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _load_all_suffixes(conn): | |
| """Load all suffixes from all 4 tasrif tables. | |
| Returns list of (suffix, code, table, category, function) sorted by length desc. | |
| """ | |
| suffixes = [] | |
| tables = [ | |
| ('bitig_verb_tasrif', 'code', 'suffix_or_affix', 'function', 'VERB'), | |
| ('bitig_noun_tasrif', 'code', 'suffix_or_affix', 'function', 'NOUN'), | |
| ('bitig_case_tasrif', 'code', 'suffix_or_affix', 'case_name', 'CASE'), | |
| ('bitig_grammar_tasrif', 'code', 'suffix_or_affix', 'category', 'GRAMMAR'), | |
| ] | |
| for table, code_col, suffix_col, func_col, category in tables: | |
| try: | |
| rows = conn.execute( | |
| f'SELECT {code_col}, {suffix_col}, {func_col} FROM {table}' | |
| ).fetchall() | |
| for row in rows: | |
| raw = row[1] or '' | |
| func = row[2] or '' | |
| if 'VOWEL_HARMONY' in raw: | |
| continue # BG10 is a rule, not a suffix | |
| # Split on / to get all variants | |
| for variant in raw.replace(',', '/').split('/'): | |
| v = variant.strip().lstrip('-') | |
| if v and len(v) >= 2: | |
| suffixes.append((v, row[0], table, category, func)) | |
| except sqlite3.OperationalError: | |
| pass | |
| # Sort by suffix length descending (greedy matching) | |
| suffixes.sort(key=lambda x: len(x[0]), reverse=True) | |
| return suffixes | |
| def decompose_suffixes(word, conn=None): | |
| """Decompose a word by stripping BI suffixes layer by layer. | |
| Returns list of layers: | |
| [(remaining_stem, stripped_suffix, code, table, category, function), ...] | |
| """ | |
| close_conn = False | |
| if conn is None: | |
| conn = _connect() | |
| close_conn = True | |
| all_suffixes = _load_all_suffixes(conn) | |
| word_lower = word.lower() | |
| layers = [] | |
| current = word_lower | |
| for _ in range(5): # max 5 suffix layers (BI can stack deep) | |
| found = False | |
| for suffix, code, table, category, func in all_suffixes: | |
| if current.endswith(suffix) and len(current) > len(suffix) + 1: | |
| stem = current[:-len(suffix)] | |
| layers.append((stem, suffix, code, table, category, func)) | |
| current = stem | |
| found = True | |
| break | |
| if not found: | |
| break | |
| if close_conn: | |
| conn.close() | |
| return layers | |
| def analyze_word(word): | |
| """Full morphological analysis of a BI word. | |
| Returns dict with harmony, suffix layers, root candidates, bitig matches. | |
| """ | |
| conn = _connect() | |
| # Vowel harmony | |
| harmony, harmony_valid, harmony_violations = check_harmony(word) | |
| # Suffix decomposition | |
| layers = decompose_suffixes(word, conn) | |
| # Get all candidate stems (original + each stripped layer) | |
| stems = [word.lower()] | |
| for stem, *_ in layers: | |
| if stem not in stems: | |
| stems.append(stem) | |
| # Search bitig_a1 for each candidate stem | |
| bitig_matches = [] | |
| for stem in stems: | |
| row = conn.execute( | |
| 'SELECT entry_id, orig2_term, root_letters, kashgari_attestation, ' | |
| 'semantic_field FROM bitig_a1_entries ' | |
| 'WHERE LOWER(orig2_term) = ? LIMIT 1', | |
| (stem,) | |
| ).fetchone() | |
| if row: | |
| bitig_matches.append({ | |
| 'entry_id': row['entry_id'], | |
| 'orig2_term': row['orig2_term'], | |
| 'root_letters': row['root_letters'], | |
| 'kashgari': row['kashgari_attestation'], | |
| 'semantic_field': row['semantic_field'], | |
| 'matched_stem': stem, | |
| }) | |
| conn.close() | |
| return { | |
| 'input': word, | |
| 'harmony': harmony, | |
| 'harmony_valid': harmony_valid, | |
| 'harmony_violations': harmony_violations, | |
| 'suffix_layers': [ | |
| { | |
| 'stem': stem, | |
| 'suffix': f'-{suffix}', | |
| 'code': code, | |
| 'category': cat, | |
| 'function': func, | |
| } | |
| for stem, suffix, code, table, cat, func in layers | |
| ], | |
| 'root_stem': layers[-1][0] if layers else word.lower(), | |
| 'bitig_matches': bitig_matches, | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LAYER 5: COMPOUND ANALYSIS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze_compound(compound_text): | |
| """Analyze a compound word (space-separated or + separated). | |
| Checks each component against bitig_a1 and compound_rules. | |
| """ | |
| conn = _connect() | |
| # Split on space or + | |
| parts = re.split(r'[\s+]+', compound_text.strip()) | |
| components = [] | |
| for part in parts: | |
| part_lower = part.lower() | |
| # Search bitig_a1 | |
| row = conn.execute( | |
| 'SELECT entry_id, orig2_term, root_letters, semantic_field ' | |
| 'FROM bitig_a1_entries WHERE LOWER(orig2_term) = ? LIMIT 1', | |
| (part_lower,) | |
| ).fetchone() | |
| # Also decompose | |
| layers = decompose_suffixes(part, conn) | |
| components.append({ | |
| 'form': part, | |
| 'bitig_match': dict(row) if row else None, | |
| 'suffix_layers': [ | |
| {'stem': s, 'suffix': f'-{sx}', 'code': c, 'category': cat} | |
| for s, sx, c, _, cat, _ in layers | |
| ], | |
| }) | |
| # Match against compound rules | |
| rules = conn.execute('SELECT code, pattern, function, description FROM bitig_compound_rules').fetchall() | |
| matched_rules = [] | |
| for rule in rules: | |
| matched_rules.append({ | |
| 'code': rule['code'], | |
| 'pattern': rule['pattern'], | |
| 'function': rule['function'], | |
| }) | |
| conn.close() | |
| return { | |
| 'input': compound_text, | |
| 'components': components, | |
| 'available_rules': matched_rules, | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ROOT EXPANSION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_root_forms(root_letters): | |
| """Get all bitig_a1 entries with matching root_letters.""" | |
| conn = _connect() | |
| rows = conn.execute( | |
| 'SELECT entry_id, orig2_term, root_letters, kashgari_attestation, ' | |
| 'semantic_field, status FROM bitig_a1_entries ' | |
| 'WHERE root_letters = ? OR root_letters LIKE ?', | |
| (root_letters, f'%{root_letters}%') | |
| ).fetchall() | |
| conn.close() | |
| return [dict(r) for r in rows] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PATTERN INFO | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_pattern_info(code): | |
| """Get full info for a tasrif pattern code (BV01, BN01, BC01, BG01, COMP01).""" | |
| conn = _connect() | |
| tables = { | |
| 'BV': 'bitig_verb_tasrif', | |
| 'BN': 'bitig_noun_tasrif', | |
| 'BC': 'bitig_case_tasrif', | |
| 'BG': 'bitig_grammar_tasrif', | |
| 'CO': 'bitig_compound_rules', | |
| } | |
| prefix = code[:2] | |
| table = tables.get(prefix) | |
| if not table: | |
| conn.close() | |
| return None | |
| row = conn.execute(f'SELECT * FROM {table} WHERE code = ?', (code,)).fetchone() | |
| conn.close() | |
| return dict(row) if row else None | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # STATUS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_status(): | |
| """Coverage stats across all BI tasrif tables.""" | |
| conn = _connect() | |
| stats = {} | |
| tables = [ | |
| ('bitig_verb_tasrif', 'Verb patterns'), | |
| ('bitig_noun_tasrif', 'Noun patterns'), | |
| ('bitig_case_tasrif', 'Case suffixes'), | |
| ('bitig_grammar_tasrif', 'Grammar patterns'), | |
| ('bitig_compound_rules', 'Compound rules'), | |
| ('bitig_phonology', 'Phonemes'), | |
| ] | |
| for table, label in tables: | |
| try: | |
| count = conn.execute(f'SELECT COUNT(*) FROM {table}').fetchone()[0] | |
| stats[label] = count | |
| except sqlite3.OperationalError: | |
| stats[label] = 0 | |
| # Bitig entries total | |
| stats['bitig_a1 entries'] = conn.execute( | |
| 'SELECT COUNT(*) FROM bitig_a1_entries' | |
| ).fetchone()[0] | |
| conn.close() | |
| return stats | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CLI | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print("Usage:") | |
| print(" amr_bitig_tasrif.py status") | |
| print(" amr_bitig_tasrif.py analyze <word>") | |
| print(" amr_bitig_tasrif.py root <root_letters>") | |
| print(" amr_bitig_tasrif.py pattern <code>") | |
| print(" amr_bitig_tasrif.py harmony <word>") | |
| print(" amr_bitig_tasrif.py compound <compound>") | |
| sys.exit(1) | |
| cmd = sys.argv[1] | |
| if cmd == 'status': | |
| stats = get_status() | |
| print("ββββββββββββββββββββββββββββββββββββββββββββ") | |
| print("β BI TASRIF β Ψ¨ΩΩΨͺΩΩΩ ΨͺΩΨ΅ΩΨ±ΩΩΩ β") | |
| print("β βββββββββββββββββββββββββββββββββββββββββββ£") | |
| for label, count in stats.items(): | |
| print(f"β {label:25} {count:>6} β") | |
| total = sum(v for k, v in stats.items() if k != 'bitig_a1 entries') | |
| print(f"β βββββββββββββββββββββββββββββββββββββββββββ£") | |
| print(f"β TOTAL PATTERNS {total:>6} β") | |
| print(f"β BITIG ENTRIES {stats.get('bitig_a1 entries', 0):>6} β") | |
| print(f"ββββββββββββββββββββββββββββββββββββββββββββ") | |
| elif cmd == 'analyze' and len(sys.argv) > 2: | |
| word = sys.argv[2] | |
| result = analyze_word(word) | |
| print(f"Input: {result['input']}") | |
| print(f"Harmony: {result['harmony']} ({'VALID' if result['harmony_valid'] else 'VIOLATION'})") | |
| if result['harmony_violations']: | |
| for v in result['harmony_violations']: | |
| print(f" β {v}") | |
| print(f"Root stem: {result['root_stem']}") | |
| if result['suffix_layers']: | |
| print(f"Suffix layers ({len(result['suffix_layers'])}):") | |
| for layer in result['suffix_layers']: | |
| print(f" {layer['suffix']:8} {layer['code']:6} {layer['category']:8} {layer['function'][:50]}") | |
| if result['bitig_matches']: | |
| print(f"Bitig matches:") | |
| for m in result['bitig_matches']: | |
| print(f" {m['entry_id']}|{m['orig2_term']}|{m['root_letters']}|{m['semantic_field']}") | |
| else: | |
| print("Bitig matches: NONE") | |
| elif cmd == 'root' and len(sys.argv) > 2: | |
| root = sys.argv[2] | |
| forms = get_root_forms(root) | |
| print(f"Root: {root} β {len(forms)} entries") | |
| for f in forms[:20]: | |
| print(f" {f['entry_id']}|{f['orig2_term']}|{f['root_letters']}|{f.get('semantic_field', '')}") | |
| elif cmd == 'pattern' and len(sys.argv) > 2: | |
| code = sys.argv[2] | |
| info = get_pattern_info(code) | |
| if info: | |
| for k, v in info.items(): | |
| if v is not None: | |
| print(f" {k}: {v}") | |
| else: | |
| print(f"Pattern {code} not found.") | |
| elif cmd == 'harmony' and len(sys.argv) > 2: | |
| word = sys.argv[2] | |
| harmony, valid, violations = check_harmony(word) | |
| print(f"Word: {word}") | |
| print(f"Harmony: {harmony} ({'VALID' if valid else 'VIOLATION'})") | |
| for v in violations: | |
| print(f" β {v}") | |
| elif cmd == 'compound' and len(sys.argv) > 2: | |
| compound = ' '.join(sys.argv[2:]) | |
| result = analyze_compound(compound) | |
| print(f"Compound: {result['input']}") | |
| for comp in result['components']: | |
| match_str = f"bitig {comp['bitig_match']['entry_id']}" if comp['bitig_match'] else 'NOT IN BITIG' | |
| print(f" {comp['form']}: {match_str}") | |
| for layer in comp['suffix_layers']: | |
| print(f" {layer['suffix']} ({layer['code']}/{layer['category']})") | |
| if __name__ == '__main__': | |
| main() | |