From b823b724a5d5693f21a4d427659fcf1f07748e6c Mon Sep 17 00:00:00 2001 From: Menny Even Danan Date: Sat, 17 Oct 2026 03:25:45 +0000 Subject: [PATCH] [LLM] Generate and package seed database asset with dictionary and compaction --- app/BUILD.bazel | 9 + app/src/main/assets/databases/seed.db | Bin 0 -> 114688 bytes core/data/BUILD.bazel | 23 + .../calque/data/seed/PackagedSeedAssetTest.kt | 72 + data/seeds/BUILD.bazel | 2 + data/seeds/dictionary_seed.json | 1568 +++++++++++++++++ tools/preprocessor/BUILD.bazel | 1 + .../calque/preprocessor/DatabaseOptimizer.kt | 9 + .../preprocessor/DictionarySeedProcessor.kt | 58 + .../calque/preprocessor/Main.kt | 64 +- .../preprocessor/PreprocessorPipeline.kt | 14 +- .../calque/preprocessor/SeedDatabase.kt | 2 +- .../calque/preprocessor/PreprocessorTest.kt | 63 +- 13 files changed, 1855 insertions(+), 30 deletions(-) create mode 100644 app/src/main/assets/databases/seed.db create mode 100644 core/data/src/test/kotlin/com/anysoftkeyboard/calque/data/seed/PackagedSeedAssetTest.kt create mode 100644 data/seeds/dictionary_seed.json create mode 100644 tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/DatabaseOptimizer.kt create mode 100644 tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/DictionarySeedProcessor.kt diff --git a/app/BUILD.bazel b/app/BUILD.bazel index ffd0fb0..4dd547b 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -1 +1,10 @@ package(default_visibility = ["//visibility:public"]) + +exports_files([ + "src/main/assets/databases/seed.db", +]) + +filegroup( + name = "seed_db_asset", + srcs = ["src/main/assets/databases/seed.db"], +) diff --git a/app/src/main/assets/databases/seed.db b/app/src/main/assets/databases/seed.db new file mode 100644 index 0000000000000000000000000000000000000000..1812cf8e0565db0a346a39a994dc2e7ba149f244 GIT binary patch literal 114688 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 10KB, got ${assetFile.length()}", + assetFile.length() > 10_000 + ) + + val database = SeedDatabase.open("jdbc:sqlite:${assetFile.path}") + database.use { db -> + val dictRepo = SqliteDictionaryRepository(db.connection) + val fcRepo = SqliteFalseCognatesRepository(db.connection) + val lessonRepo = SqliteCuratedLessonsRepository(db.connection) + val rulesRepo = SqliteCognateRulesRepository(db.connection) + + // 1. Verify dictionary entries + val hotelWord = dictRepo.getBySurfaceForm("hotel") + assertNotNull("Dictionary should contain 'hotel'", hotelWord) + assertEquals("hotel", hotelWord?.lemma) + assertTrue("hotel should be marked as cognate", hotelWord?.isCognate == true) + + val esWord = dictRepo.getBySurfaceForm("es") + assertNotNull("Dictionary should contain 'es'", esWord) + assertEquals("ser", esWord?.lemma) + + // 2. Verify false cognates + val exitoCard = fcRepo.getBySurfaceForm("éxito") + assertNotNull("False cognates should contain 'éxito'", exitoCard) + assertEquals("exit", exitoCard?.apparentEnglish) + assertEquals("salida", exitoCard?.realSpanishForApparent) + + val carpetasPlural = fcRepo.getBySurfaceForm("carpetas") + assertNotNull( + "False cognates should expand inflections like 'carpetas'", + carpetasPlural + ) + assertEquals("carpeta", carpetasPlural?.lemma) + + // 3. Verify curated lessons + val lessons = lessonRepo.getAll() + assertEquals("Should contain 8 curated lessons", 8, lessons.size) + val levels = lessons.map { it.level }.toSet() + assertEquals(setOf(0, 1, 2), levels) + + val firstLesson = lessonRepo.getById("lvl0_00_cognate_unlock") + assertNotNull(firstLesson) + assertEquals("The Cognate Unlock & Vowel Soundboard", firstLesson?.title) + assertEquals( + "El hotel moderno es excelente.", + firstLesson?.parseSentences()?.first()?.sentenceEs, + ) + + // 4. Verify cognate rules + val rules = rulesRepo.getAll() + assertEquals("Should contain 12 cognate rules", 12, rules.size) + val matchingRule = rulesRepo.findRuleForSpanishWord("situación") + assertNotNull(matchingRule) + assertEquals("tion_to_cion", matchingRule?.ruleId) + } + } +} diff --git a/data/seeds/BUILD.bazel b/data/seeds/BUILD.bazel index aaf04a5..565689d 100644 --- a/data/seeds/BUILD.bazel +++ b/data/seeds/BUILD.bazel @@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"]) exports_files([ "cognate_rules_seed.json", "curated_lessons_seed.json", + "dictionary_seed.json", "false_cognates_seed.json", ]) @@ -11,6 +12,7 @@ filegroup( srcs = [ "cognate_rules_seed.json", "curated_lessons_seed.json", + "dictionary_seed.json", "false_cognates_seed.json", ], ) diff --git a/data/seeds/dictionary_seed.json b/data/seeds/dictionary_seed.json new file mode 100644 index 0000000..b204852 --- /dev/null +++ b/data/seeds/dictionary_seed.json @@ -0,0 +1,1568 @@ +[ + { + "surface_form": "el", + "lemma": "el", + "pos": "article", + "grammar_info": "masc. sing.", + "english_gloss": "the", + "is_cognate": false, + "frequency_rank": 1 + }, + { + "surface_form": "la", + "lemma": "el", + "pos": "article", + "grammar_info": "fem. sing.", + "english_gloss": "the", + "is_cognate": false, + "frequency_rank": 2 + }, + { + "surface_form": "de", + "lemma": "de", + "pos": "prep", + "grammar_info": null, + "english_gloss": "of / from", + "is_cognate": false, + "frequency_rank": 3 + }, + { + "surface_form": "que", + "lemma": "que", + "pos": "conj", + "grammar_info": null, + "english_gloss": "that / which", + "is_cognate": false, + "frequency_rank": 4 + }, + { + "surface_form": "y", + "lemma": "y", + "pos": "conj", + "grammar_info": null, + "english_gloss": "and", + "is_cognate": false, + "frequency_rank": 5 + }, + { + "surface_form": "a", + "lemma": "a", + "pos": "prep", + "grammar_info": null, + "english_gloss": "to / at", + "is_cognate": false, + "frequency_rank": 6 + }, + { + "surface_form": "en", + "lemma": "en", + "pos": "prep", + "grammar_info": null, + "english_gloss": "in / on / at", + "is_cognate": false, + "frequency_rank": 7 + }, + { + "surface_form": "un", + "lemma": "uno", + "pos": "article", + "grammar_info": "masc. sing.", + "english_gloss": "a / an", + "is_cognate": false, + "frequency_rank": 8 + }, + { + "surface_form": "una", + "lemma": "uno", + "pos": "article", + "grammar_info": "fem. sing.", + "english_gloss": "a / an", + "is_cognate": false, + "frequency_rank": 9 + }, + { + "surface_form": "ser", + "lemma": "ser", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to be", + "is_cognate": false, + "frequency_rank": 10 + }, + { + "surface_form": "es", + "lemma": "ser", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "is", + "is_cognate": false, + "frequency_rank": 11 + }, + { + "surface_form": "son", + "lemma": "ser", + "pos": "verb", + "grammar_info": "3rd pl. present", + "english_gloss": "are", + "is_cognate": false, + "frequency_rank": 12 + }, + { + "surface_form": "era", + "lemma": "ser", + "pos": "verb", + "grammar_info": "1st/3rd sing. imperfect", + "english_gloss": "was", + "is_cognate": false, + "frequency_rank": 13 + }, + { + "surface_form": "fue", + "lemma": "ser", + "pos": "verb", + "grammar_info": "3rd sing. preterite", + "english_gloss": "was", + "is_cognate": false, + "frequency_rank": 14 + }, + { + "surface_form": "se", + "lemma": "se", + "pos": "pron", + "grammar_info": "reflexive / indirect", + "english_gloss": "himself / herself / to him / to her", + "is_cognate": false, + "frequency_rank": 15 + }, + { + "surface_form": "no", + "lemma": "no", + "pos": "adv", + "grammar_info": null, + "english_gloss": "no / not", + "is_cognate": true, + "frequency_rank": 16 + }, + { + "surface_form": "haber", + "lemma": "haber", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to have (auxiliary) / to exist", + "is_cognate": false, + "frequency_rank": 17 + }, + { + "surface_form": "hay", + "lemma": "haber", + "pos": "verb", + "grammar_info": "present impersonal", + "english_gloss": "there is / there are", + "is_cognate": false, + "frequency_rank": 18 + }, + { + "surface_form": "por", + "lemma": "por", + "pos": "prep", + "grammar_info": null, + "english_gloss": "for / by / through", + "is_cognate": false, + "frequency_rank": 19 + }, + { + "surface_form": "con", + "lemma": "con", + "pos": "prep", + "grammar_info": null, + "english_gloss": "with", + "is_cognate": false, + "frequency_rank": 20 + }, + { + "surface_form": "para", + "lemma": "para", + "pos": "prep", + "grammar_info": null, + "english_gloss": "for / in order to", + "is_cognate": false, + "frequency_rank": 21 + }, + { + "surface_form": "como", + "lemma": "como", + "pos": "conj", + "grammar_info": null, + "english_gloss": "like / as", + "is_cognate": false, + "frequency_rank": 22 + }, + { + "surface_form": "estar", + "lemma": "estar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to be (state/location)", + "is_cognate": false, + "frequency_rank": 23 + }, + { + "surface_form": "está", + "lemma": "estar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "is", + "is_cognate": false, + "frequency_rank": 24 + }, + { + "surface_form": "están", + "lemma": "estar", + "pos": "verb", + "grammar_info": "3rd pl. present", + "english_gloss": "are", + "is_cognate": false, + "frequency_rank": 25 + }, + { + "surface_form": "tener", + "lemma": "tener", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to have", + "is_cognate": false, + "frequency_rank": 26 + }, + { + "surface_form": "tiene", + "lemma": "tener", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "has", + "is_cognate": false, + "frequency_rank": 27 + }, + { + "surface_form": "tienen", + "lemma": "tener", + "pos": "verb", + "grammar_info": "3rd pl. present", + "english_gloss": "have", + "is_cognate": false, + "frequency_rank": 28 + }, + { + "surface_form": "le", + "lemma": "él", + "pos": "pron", + "grammar_info": "3rd sing. indirect object", + "english_gloss": "to him / to her", + "is_cognate": false, + "frequency_rank": 29 + }, + { + "surface_form": "lo", + "lemma": "él", + "pos": "pron", + "grammar_info": "3rd masc. sing. direct object", + "english_gloss": "it / him", + "is_cognate": false, + "frequency_rank": 30 + }, + { + "surface_form": "los", + "lemma": "el", + "pos": "article", + "grammar_info": "masc. pl.", + "english_gloss": "the", + "is_cognate": false, + "frequency_rank": 31 + }, + { + "surface_form": "las", + "lemma": "el", + "pos": "article", + "grammar_info": "fem. pl.", + "english_gloss": "the", + "is_cognate": false, + "frequency_rank": 32 + }, + { + "surface_form": "su", + "lemma": "su", + "pos": "adj", + "grammar_info": "possessive sing.", + "english_gloss": "his / her / their", + "is_cognate": false, + "frequency_rank": 33 + }, + { + "surface_form": "sus", + "lemma": "su", + "pos": "adj", + "grammar_info": "possessive pl.", + "english_gloss": "his / her / their", + "is_cognate": false, + "frequency_rank": 34 + }, + { + "surface_form": "pero", + "lemma": "pero", + "pos": "conj", + "grammar_info": null, + "english_gloss": "but", + "is_cognate": false, + "frequency_rank": 35 + }, + { + "surface_form": "más", + "lemma": "más", + "pos": "adv", + "grammar_info": null, + "english_gloss": "more", + "is_cognate": false, + "frequency_rank": 36 + }, + { + "surface_form": "hacer", + "lemma": "hacer", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to do / to make", + "is_cognate": false, + "frequency_rank": 37 + }, + { + "surface_form": "hace", + "lemma": "hacer", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "does / makes", + "is_cognate": false, + "frequency_rank": 38 + }, + { + "surface_form": "o", + "lemma": "o", + "pos": "conj", + "grammar_info": null, + "english_gloss": "or", + "is_cognate": false, + "frequency_rank": 39 + }, + { + "surface_form": "poder", + "lemma": "poder", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to be able to / can", + "is_cognate": false, + "frequency_rank": 40 + }, + { + "surface_form": "puede", + "lemma": "poder", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "can / is able to", + "is_cognate": false, + "frequency_rank": 41 + }, + { + "surface_form": "decir", + "lemma": "decir", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to say / to tell", + "is_cognate": false, + "frequency_rank": 42 + }, + { + "surface_form": "dice", + "lemma": "decir", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "says", + "is_cognate": false, + "frequency_rank": 43 + }, + { + "surface_form": "este", + "lemma": "este", + "pos": "adj", + "grammar_info": "demonstrative masc. sing.", + "english_gloss": "this", + "is_cognate": false, + "frequency_rank": 44 + }, + { + "surface_form": "esta", + "lemma": "este", + "pos": "adj", + "grammar_info": "demonstrative fem. sing.", + "english_gloss": "this", + "is_cognate": false, + "frequency_rank": 45 + }, + { + "surface_form": "ir", + "lemma": "ir", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to go", + "is_cognate": false, + "frequency_rank": 46 + }, + { + "surface_form": "va", + "lemma": "ir", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "goes / is going", + "is_cognate": false, + "frequency_rank": 47 + }, + { + "surface_form": "van", + "lemma": "ir", + "pos": "verb", + "grammar_info": "3rd pl. present", + "english_gloss": "go / are going", + "is_cognate": false, + "frequency_rank": 48 + }, + { + "surface_form": "otro", + "lemma": "otro", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "other / another", + "is_cognate": false, + "frequency_rank": 49 + }, + { + "surface_form": "otra", + "lemma": "otro", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "other / another", + "is_cognate": false, + "frequency_rank": 50 + }, + { + "surface_form": "al", + "lemma": "a el", + "pos": "prep", + "grammar_info": "contraction a + el", + "english_gloss": "to the", + "is_cognate": false, + "frequency_rank": 51 + }, + { + "surface_form": "del", + "lemma": "de el", + "pos": "prep", + "grammar_info": "contraction de + el", + "english_gloss": "of the", + "is_cognate": false, + "frequency_rank": 52 + }, + { + "surface_form": "cuando", + "lemma": "cuando", + "pos": "conj", + "grammar_info": null, + "english_gloss": "when", + "is_cognate": false, + "frequency_rank": 53 + }, + { + "surface_form": "muy", + "lemma": "muy", + "pos": "adv", + "grammar_info": null, + "english_gloss": "very", + "is_cognate": false, + "frequency_rank": 54 + }, + { + "surface_form": "sin", + "lemma": "sin", + "pos": "prep", + "grammar_info": null, + "english_gloss": "without", + "is_cognate": false, + "frequency_rank": 55 + }, + { + "surface_form": "sobre", + "lemma": "sobre", + "pos": "prep", + "grammar_info": null, + "english_gloss": "on / about", + "is_cognate": false, + "frequency_rank": 56 + }, + { + "surface_form": "entre", + "lemma": "entre", + "pos": "prep", + "grammar_info": null, + "english_gloss": "between / among", + "is_cognate": false, + "frequency_rank": 57 + }, + { + "surface_form": "también", + "lemma": "también", + "pos": "adv", + "grammar_info": null, + "english_gloss": "also / too", + "is_cognate": false, + "frequency_rank": 58 + }, + { + "surface_form": "me", + "lemma": "yo", + "pos": "pron", + "grammar_info": "1st sing. object", + "english_gloss": "me / to me", + "is_cognate": true, + "frequency_rank": 59 + }, + { + "surface_form": "te", + "lemma": "tú", + "pos": "pron", + "grammar_info": "2nd sing. object", + "english_gloss": "you / to you", + "is_cognate": false, + "frequency_rank": 60 + }, + { + "surface_form": "nos", + "lemma": "nosotros", + "pos": "pron", + "grammar_info": "1st pl. object", + "english_gloss": "us / to us", + "is_cognate": false, + "frequency_rank": 61 + }, + { + "surface_form": "hotel", + "lemma": "hotel", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "hotel", + "is_cognate": true, + "frequency_rank": 450 + }, + { + "surface_form": "moderno", + "lemma": "moderno", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "modern", + "is_cognate": true, + "frequency_rank": 600 + }, + { + "surface_form": "moderna", + "lemma": "moderno", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "modern", + "is_cognate": true, + "frequency_rank": 610 + }, + { + "surface_form": "excelente", + "lemma": "excelente", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "excellent", + "is_cognate": true, + "frequency_rank": 700 + }, + { + "surface_form": "música", + "lemma": "música", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "music", + "is_cognate": true, + "frequency_rank": 250 + }, + { + "surface_form": "clásica", + "lemma": "clásico", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "classical", + "is_cognate": true, + "frequency_rank": 800 + }, + { + "surface_form": "clásico", + "lemma": "clásico", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "classic", + "is_cognate": true, + "frequency_rank": 790 + }, + { + "surface_form": "fantástica", + "lemma": "fantástico", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "fantastic", + "is_cognate": true, + "frequency_rank": 950 + }, + { + "surface_form": "fantástico", + "lemma": "fantástico", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "fantastic", + "is_cognate": true, + "frequency_rank": 940 + }, + { + "surface_form": "profesor", + "lemma": "profesor", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "professor / teacher", + "is_cognate": true, + "frequency_rank": 400 + }, + { + "surface_form": "profesora", + "lemma": "profesor", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "professor / teacher", + "is_cognate": true, + "frequency_rank": 420 + }, + { + "surface_form": "explicar", + "lemma": "explicar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to explain", + "is_cognate": true, + "frequency_rank": 320 + }, + { + "surface_form": "explica", + "lemma": "explicar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "explains", + "is_cognate": true, + "frequency_rank": 325 + }, + { + "surface_form": "situación", + "lemma": "situación", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "situation", + "is_cognate": true, + "frequency_rank": 180 + }, + { + "surface_form": "universidad", + "lemma": "universidad", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "university", + "is_cognate": true, + "frequency_rank": 350 + }, + { + "surface_form": "organizar", + "lemma": "organizar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to organize", + "is_cognate": true, + "frequency_rank": 650 + }, + { + "surface_form": "organiza", + "lemma": "organizar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "organizes", + "is_cognate": true, + "frequency_rank": 655 + }, + { + "surface_form": "conferencia", + "lemma": "conferencia", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "conference", + "is_cognate": true, + "frequency_rank": 820 + }, + { + "surface_form": "internacional", + "lemma": "internacional", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "international", + "is_cognate": true, + "frequency_rank": 410 + }, + { + "surface_form": "director", + "lemma": "director", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "director", + "is_cognate": true, + "frequency_rank": 380 + }, + { + "surface_form": "directora", + "lemma": "director", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "director", + "is_cognate": true, + "frequency_rank": 390 + }, + { + "surface_form": "presentar", + "lemma": "presentar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to present", + "is_cognate": true, + "frequency_rank": 340 + }, + { + "surface_form": "presenta", + "lemma": "presentar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "presents", + "is_cognate": true, + "frequency_rank": 345 + }, + { + "surface_form": "proyecto", + "lemma": "proyecto", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "project", + "is_cognate": true, + "frequency_rank": 310 + }, + { + "surface_form": "importante", + "lemma": "importante", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "important", + "is_cognate": true, + "frequency_rank": 160 + }, + { + "surface_form": "importantes", + "lemma": "importante", + "pos": "adj", + "grammar_info": "plural", + "english_gloss": "important", + "is_cognate": true, + "frequency_rank": 165 + }, + { + "surface_form": "solución", + "lemma": "solución", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "solution", + "is_cognate": true, + "frequency_rank": 520 + }, + { + "surface_form": "soluciones", + "lemma": "solución", + "pos": "noun", + "grammar_info": "fem. pl.", + "english_gloss": "solutions", + "is_cognate": true, + "frequency_rank": 525 + }, + { + "surface_form": "posible", + "lemma": "posible", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "possible", + "is_cognate": true, + "frequency_rank": 210 + }, + { + "surface_form": "posibles", + "lemma": "posible", + "pos": "adj", + "grammar_info": "plural", + "english_gloss": "possible", + "is_cognate": true, + "frequency_rank": 215 + }, + { + "surface_form": "requerir", + "lemma": "requerir", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to require", + "is_cognate": true, + "frequency_rank": 730 + }, + { + "surface_form": "requieren", + "lemma": "requerir", + "pos": "verb", + "grammar_info": "3rd pl. present", + "english_gloss": "require", + "is_cognate": true, + "frequency_rank": 735 + }, + { + "surface_form": "paciencia", + "lemma": "paciencia", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "patience", + "is_cognate": true, + "frequency_rank": 680 + }, + { + "surface_form": "sistema", + "lemma": "sistema", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "system", + "is_cognate": true, + "frequency_rank": 270 + }, + { + "surface_form": "funcionar", + "lemma": "funcionar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to function / to work", + "is_cognate": true, + "frequency_rank": 490 + }, + { + "surface_form": "funciona", + "lemma": "funcionar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "functions / works", + "is_cognate": true, + "frequency_rank": 495 + }, + { + "surface_form": "correctamente", + "lemma": "correcto", + "pos": "adv", + "grammar_info": null, + "english_gloss": "correctly", + "is_cognate": true, + "frequency_rank": 780 + }, + { + "surface_form": "necesitar", + "lemma": "necesitar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to need", + "is_cognate": false, + "frequency_rank": 190 + }, + { + "surface_form": "necesita", + "lemma": "necesitar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "needs", + "is_cognate": false, + "frequency_rank": 195 + }, + { + "surface_form": "equipo", + "lemma": "equipo", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "team / equipment", + "is_cognate": false, + "frequency_rank": 330 + }, + { + "surface_form": "información", + "lemma": "información", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "information", + "is_cognate": true, + "frequency_rank": 220 + }, + { + "surface_form": "buscar", + "lemma": "buscar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to look for / to search", + "is_cognate": false, + "frequency_rank": 240 + }, + { + "surface_form": "buscamos", + "lemma": "buscar", + "pos": "verb", + "grammar_info": "1st pl. present", + "english_gloss": "we look for", + "is_cognate": false, + "frequency_rank": 245 + }, + { + "surface_form": "buscaba", + "lemma": "buscar", + "pos": "verb", + "grammar_info": "1st/3rd sing. imperfect", + "english_gloss": "was looking for", + "is_cognate": false, + "frequency_rank": 248 + }, + { + "surface_form": "oportunidad", + "lemma": "oportunidad", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "opportunity", + "is_cognate": true, + "frequency_rank": 360 + }, + { + "surface_form": "diferente", + "lemma": "diferente", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "different", + "is_cognate": true, + "frequency_rank": 280 + }, + { + "surface_form": "preferir", + "lemma": "preferir", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to prefer", + "is_cognate": true, + "frequency_rank": 580 + }, + { + "surface_form": "prefieren", + "lemma": "preferir", + "pos": "verb", + "grammar_info": "3rd pl. present", + "english_gloss": "they prefer", + "is_cognate": true, + "frequency_rank": 585 + }, + { + "surface_form": "estudiar", + "lemma": "estudiar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to study", + "is_cognate": true, + "frequency_rank": 470 + }, + { + "surface_form": "biblioteca", + "lemma": "biblioteca", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "library", + "is_cognate": false, + "frequency_rank": 850 + }, + { + "surface_form": "central", + "lemma": "central", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "central", + "is_cognate": true, + "frequency_rank": 510 + }, + { + "surface_form": "médico", + "lemma": "médico", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "doctor / physician", + "is_cognate": false, + "frequency_rank": 440 + }, + { + "surface_form": "recomendar", + "lemma": "recomendar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to recommend", + "is_cognate": true, + "frequency_rank": 720 + }, + { + "surface_form": "recomienda", + "lemma": "recomendar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "recommends", + "is_cognate": true, + "frequency_rank": 725 + }, + { + "surface_form": "totalmente", + "lemma": "total", + "pos": "adv", + "grammar_info": null, + "english_gloss": "totally", + "is_cognate": true, + "frequency_rank": 640 + }, + { + "surface_form": "querer", + "lemma": "querer", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to want", + "is_cognate": false, + "frequency_rank": 80 + }, + { + "surface_form": "queremos", + "lemma": "querer", + "pos": "verb", + "grammar_info": "1st pl. present", + "english_gloss": "we want", + "is_cognate": false, + "frequency_rank": 85 + }, + { + "surface_form": "calma", + "lemma": "calma", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "calm", + "is_cognate": true, + "frequency_rank": 760 + }, + { + "surface_form": "trabajar", + "lemma": "trabajar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to work", + "is_cognate": false, + "frequency_rank": 260 + }, + { + "surface_form": "trabaja", + "lemma": "trabajar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "works", + "is_cognate": false, + "frequency_rank": 265 + }, + { + "surface_form": "oficina", + "lemma": "oficina", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "office", + "is_cognate": false, + "frequency_rank": 430 + }, + { + "surface_form": "activo", + "lemma": "activo", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "active", + "is_cognate": true, + "frequency_rank": 870 + }, + { + "surface_form": "activa", + "lemma": "activo", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "active", + "is_cognate": true, + "frequency_rank": 875 + }, + { + "surface_form": "guardar", + "lemma": "guardar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to keep / to save", + "is_cognate": false, + "frequency_rank": 540 + }, + { + "surface_form": "guarda", + "lemma": "guardar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "keeps / saves", + "is_cognate": false, + "frequency_rank": 545 + }, + { + "surface_form": "todos", + "lemma": "todo", + "pos": "adj", + "grammar_info": "masc. pl.", + "english_gloss": "all", + "is_cognate": false, + "frequency_rank": 70 + }, + { + "surface_form": "contrato", + "lemma": "contrato", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "contract", + "is_cognate": true, + "frequency_rank": 630 + }, + { + "surface_form": "contratos", + "lemma": "contrato", + "pos": "noun", + "grammar_info": "masc. pl.", + "english_gloss": "contracts", + "is_cognate": true, + "frequency_rank": 635 + }, + { + "surface_form": "azul", + "lemma": "azul", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "blue", + "is_cognate": false, + "frequency_rank": 810 + }, + { + "surface_form": "ruido", + "lemma": "ruido", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "noise", + "is_cognate": false, + "frequency_rank": 740 + }, + { + "surface_form": "decidir", + "lemma": "decidir", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to decide", + "is_cognate": true, + "frequency_rank": 370 + }, + { + "surface_form": "decide", + "lemma": "decidir", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "decides", + "is_cognate": true, + "frequency_rank": 375 + }, + { + "surface_form": "llover", + "lemma": "llover", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to rain", + "is_cognate": false, + "frequency_rank": 920 + }, + { + "surface_form": "llovía", + "lemma": "llover", + "pos": "verb", + "grammar_info": "3rd sing. imperfect", + "english_gloss": "it was raining", + "is_cognate": false, + "frequency_rank": 925 + }, + { + "surface_form": "intensamente", + "lemma": "intenso", + "pos": "adv", + "grammar_info": null, + "english_gloss": "intensely", + "is_cognate": true, + "frequency_rank": 960 + }, + { + "surface_form": "entrar", + "lemma": "entrar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to enter", + "is_cognate": true, + "frequency_rank": 290 + }, + { + "surface_form": "entró", + "lemma": "entrar", + "pos": "verb", + "grammar_info": "3rd sing. preterite", + "english_gloss": "entered", + "is_cognate": true, + "frequency_rank": 295 + }, + { + "surface_form": "pequeño", + "lemma": "pequeño", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "small", + "is_cognate": false, + "frequency_rank": 200 + }, + { + "surface_form": "pequeña", + "lemma": "pequeño", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "small", + "is_cognate": false, + "frequency_rank": 205 + }, + { + "surface_form": "barrio", + "lemma": "barrio", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "neighborhood", + "is_cognate": false, + "frequency_rank": 660 + }, + { + "surface_form": "novela", + "lemma": "novela", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "novel", + "is_cognate": true, + "frequency_rank": 770 + }, + { + "surface_form": "histórico", + "lemma": "histórico", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "historical", + "is_cognate": true, + "frequency_rank": 690 + }, + { + "surface_form": "histórica", + "lemma": "histórico", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "historical", + "is_cognate": true, + "frequency_rank": 695 + }, + { + "surface_form": "encontrar", + "lemma": "encontrar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to find", + "is_cognate": false, + "frequency_rank": 140 + }, + { + "surface_form": "encontró", + "lemma": "encontrar", + "pos": "verb", + "grammar_info": "3rd sing. preterite", + "english_gloss": "found", + "is_cognate": false, + "frequency_rank": 145 + }, + { + "surface_form": "carta", + "lemma": "carta", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "letter / card", + "is_cognate": false, + "frequency_rank": 480 + }, + { + "surface_form": "antiguo", + "lemma": "antiguo", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "ancient / antique", + "is_cognate": false, + "frequency_rank": 590 + }, + { + "surface_form": "antigua", + "lemma": "antiguo", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "ancient / antique", + "is_cognate": false, + "frequency_rank": 595 + }, + { + "surface_form": "página", + "lemma": "página", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "page", + "is_cognate": true, + "frequency_rank": 710 + }, + { + "surface_form": "páginas", + "lemma": "página", + "pos": "noun", + "grammar_info": "fem. pl.", + "english_gloss": "pages", + "is_cognate": true, + "frequency_rank": 715 + }, + { + "surface_form": "lectura", + "lemma": "lectura", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "reading", + "is_cognate": false, + "frequency_rank": 890 + }, + { + "surface_form": "documento", + "lemma": "documento", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "document", + "is_cognate": true, + "frequency_rank": 530 + }, + { + "surface_form": "revelar", + "lemma": "revelar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to reveal", + "is_cognate": true, + "frequency_rank": 840 + }, + { + "surface_form": "reveló", + "lemma": "revelar", + "pos": "verb", + "grammar_info": "3rd sing. preterite", + "english_gloss": "revealed", + "is_cognate": true, + "frequency_rank": 845 + }, + { + "surface_form": "secreto", + "lemma": "secreto", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "secret", + "is_cognate": true, + "frequency_rank": 620 + }, + { + "surface_form": "familiar", + "lemma": "familiar", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "family / familiar", + "is_cognate": true, + "frequency_rank": 570 + }, + { + "surface_form": "extraordinario", + "lemma": "extraordinario", + "pos": "adj", + "grammar_info": "masc. sing.", + "english_gloss": "extraordinary", + "is_cognate": true, + "frequency_rank": 980 + }, + { + "surface_form": "éxito", + "lemma": "éxito", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "success", + "is_cognate": false, + "frequency_rank": 460 + }, + { + "surface_form": "éxitos", + "lemma": "éxito", + "pos": "noun", + "grammar_info": "masc. pl.", + "english_gloss": "successes", + "is_cognate": false, + "frequency_rank": 465 + }, + { + "surface_form": "carpeta", + "lemma": "carpeta", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "folder", + "is_cognate": false, + "frequency_rank": 910 + }, + { + "surface_form": "carpetas", + "lemma": "carpeta", + "pos": "noun", + "grammar_info": "fem. pl.", + "english_gloss": "folders", + "is_cognate": false, + "frequency_rank": 915 + }, + { + "surface_form": "soportar", + "lemma": "soportar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to tolerate / to endure", + "is_cognate": false, + "frequency_rank": 830 + }, + { + "surface_form": "soporta", + "lemma": "soportar", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "tolerates / stands", + "is_cognate": false, + "frequency_rank": 835 + }, + { + "surface_form": "embarazada", + "lemma": "embarazada", + "pos": "adj", + "grammar_info": "fem. sing.", + "english_gloss": "pregnant", + "is_cognate": false, + "frequency_rank": 930 + }, + { + "surface_form": "actualmente", + "lemma": "actual", + "pos": "adv", + "grammar_info": null, + "english_gloss": "currently", + "is_cognate": false, + "frequency_rank": 505 + }, + { + "surface_form": "realizar", + "lemma": "realizar", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to carry out / to fulfill", + "is_cognate": false, + "frequency_rank": 300 + }, + { + "surface_form": "realizan", + "lemma": "realizar", + "pos": "verb", + "grammar_info": "3rd pl. present", + "english_gloss": "carry out / fulfill", + "is_cognate": false, + "frequency_rank": 305 + }, + { + "surface_form": "sensible", + "lemma": "sensible", + "pos": "adj", + "grammar_info": "sing.", + "english_gloss": "sensitive", + "is_cognate": false, + "frequency_rank": 860 + }, + { + "surface_form": "asistir", + "lemma": "asistir", + "pos": "verb", + "grammar_info": "infinitive", + "english_gloss": "to attend", + "is_cognate": false, + "frequency_rank": 670 + }, + { + "surface_form": "asiste", + "lemma": "asistir", + "pos": "verb", + "grammar_info": "3rd sing. present", + "english_gloss": "attends", + "is_cognate": false, + "frequency_rank": 675 + }, + { + "surface_form": "librería", + "lemma": "librería", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "bookstore", + "is_cognate": false, + "frequency_rank": 970 + }, + { + "surface_form": "noticia", + "lemma": "noticia", + "pos": "noun", + "grammar_info": "fem. sing.", + "english_gloss": "news item", + "is_cognate": false, + "frequency_rank": 550 + }, + { + "surface_form": "noticias", + "lemma": "noticia", + "pos": "noun", + "grammar_info": "fem. pl.", + "english_gloss": "news", + "is_cognate": false, + "frequency_rank": 555 + } +] diff --git a/tools/preprocessor/BUILD.bazel b/tools/preprocessor/BUILD.bazel index c3c2d73..3423ed1 100644 --- a/tools/preprocessor/BUILD.bazel +++ b/tools/preprocessor/BUILD.bazel @@ -29,6 +29,7 @@ kt_jvm_test( data = [ "//data/seeds:cognate_rules_seed.json", "//data/seeds:curated_lessons_seed.json", + "//data/seeds:dictionary_seed.json", "//data/seeds:false_cognates_seed.json", ], test_class = "com.anysoftkeyboard.calque.preprocessor.PreprocessorTest", diff --git a/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/DatabaseOptimizer.kt b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/DatabaseOptimizer.kt new file mode 100644 index 0000000..84a2316 --- /dev/null +++ b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/DatabaseOptimizer.kt @@ -0,0 +1,9 @@ +package com.anysoftkeyboard.calque.preprocessor + +/** Performs SQLite database optimization and compaction via ANALYZE and VACUUM. */ +fun SeedDatabase.optimize() { + connection.createStatement().use { stmt -> + stmt.execute("ANALYZE") + stmt.execute("VACUUM") + } +} diff --git a/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/DictionarySeedProcessor.kt b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/DictionarySeedProcessor.kt new file mode 100644 index 0000000..a5c6632 --- /dev/null +++ b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/DictionarySeedProcessor.kt @@ -0,0 +1,58 @@ +package com.anysoftkeyboard.calque.preprocessor + +import java.io.InputStream +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets +import java.util.Locale +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +/** Seeds the dictionary table from a structured JSON definition. */ +object DictionarySeedProcessor { + + /** + * Reads dictionary entries from [inputStream] and inserts them into [database]. + * + * @return Count of dictionary entries inserted. + */ + fun processDictionarySeed(inputStream: InputStream, database: SeedDatabase): Int { + val rows = parseDictionary(inputStream) + database.insertDictionaryBatch(rows) + return rows.size + } + + /** Parses the dictionary seed JSON stream into a list of [DictionaryRow] objects. */ + fun parseDictionary(inputStream: InputStream): List { + val jsonText = InputStreamReader(inputStream, StandardCharsets.UTF_8).use { it.readText() } + val array = Json.parseToJsonElement(jsonText).jsonArray + return array.mapNotNull { parseDictionaryElement(it.jsonObject) } + } + + private fun parseDictionaryElement(obj: JsonObject): DictionaryRow? { + val surfaceForm = + obj["surface_form"]?.jsonPrimitive?.contentOrNull?.lowercase(Locale.ROOT) ?: return null + val lemma = + obj["lemma"]?.jsonPrimitive?.contentOrNull?.lowercase(Locale.ROOT) ?: surfaceForm + val pos = obj["pos"]?.jsonPrimitive?.contentOrNull ?: "noun" + val grammarInfo = obj["grammar_info"]?.jsonPrimitive?.contentOrNull + val englishGloss = obj["english_gloss"]?.jsonPrimitive?.contentOrNull.orEmpty() + val isCognate = obj["is_cognate"]?.jsonPrimitive?.booleanOrNull ?: false + val frequencyRank = obj["frequency_rank"]?.jsonPrimitive?.intOrNull + + return DictionaryRow( + surfaceForm = surfaceForm, + lemma = lemma, + pos = pos, + grammarInfo = grammarInfo, + englishGloss = englishGloss, + isCognate = isCognate, + frequencyRank = frequencyRank, + ) + } +} diff --git a/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/Main.kt b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/Main.kt index 586fccb..7fcd465 100644 --- a/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/Main.kt +++ b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/Main.kt @@ -7,34 +7,23 @@ import kotlin.system.exitProcess fun main(args: Array) { val options = parseCommandLineArgs(args) - val dbPath = options["--db"] - if (dbPath == null) { - System.err.println( - """ - Usage: preprocessor --db [options] - - Options: - --kaikki Path to Kaikki Spanish JSONL dump - --sublex Path to SUBTLEX-ESP frequency file - --false-cognates Path to false_cognates_seed.json - --curated-lessons Path to curated_lessons_seed.json - --tatoeba Path to Tatoeba Spanish-English TSV file - --cognate-rules Path to cognate_rules_seed.json - """ - .trimIndent(), - ) - exitProcess(1) + val rawDbPath = options["--db"] + if (rawDbPath == null) { + printUsageAndExit() + return } + val dbFile = resolveFile(rawDbPath) ?: File(rawDbPath) val config = PipelineConfig( - dbFile = File(dbPath), - kaikkiFile = options["--kaikki"]?.let { File(it) }, - sublexFile = options["--sublex"]?.let { File(it) }, - falseCognatesFile = options["--false-cognates"]?.let { File(it) }, - curatedLessonsFile = options["--curated-lessons"]?.let { File(it) }, - tatoebaFile = options["--tatoeba"]?.let { File(it) }, - cognateRulesFile = options["--cognate-rules"]?.let { File(it) }, + dbFile = dbFile, + kaikkiFile = resolveFile(options["--kaikki"]), + sublexFile = resolveFile(options["--sublex"]), + dictionarySeedFile = resolveFile(options["--dictionary-seed"]), + falseCognatesFile = resolveFile(options["--false-cognates"]), + curatedLessonsFile = resolveFile(options["--curated-lessons"]), + tatoebaFile = resolveFile(options["--tatoeba"]), + cognateRulesFile = resolveFile(options["--cognate-rules"]), ) println("Starting Calque Data Ingestion Pipeline...") @@ -52,6 +41,33 @@ fun main(args: Array) { println(" - Cognate transformer rules seeded: ${summary.cognateRuleRows}") } +private fun printUsageAndExit() { + System.err.println( + """ + Usage: preprocessor --db [options] + + Options: + --kaikki Path to Kaikki Spanish JSONL dump + --sublex Path to SUBTLEX-ESP frequency file + --dictionary-seed Path to dictionary_seed.json + --false-cognates Path to false_cognates_seed.json + --curated-lessons Path to curated_lessons_seed.json + --tatoeba Path to Tatoeba Spanish-English TSV file + --cognate-rules Path to cognate_rules_seed.json + """ + .trimIndent(), + ) + exitProcess(1) +} + +private fun resolveFile(path: String?): File? { + if (path == null) return null + val file = File(path) + if (file.isAbsolute) return file + val workingDir = System.getenv("BUILD_WORKING_DIRECTORY") + return if (workingDir != null) File(workingDir, path) else file +} + private fun parseCommandLineArgs(args: Array): Map { val options = mutableMapOf() var i = 0 diff --git a/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/PreprocessorPipeline.kt b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/PreprocessorPipeline.kt index f57d3f2..209c87d 100644 --- a/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/PreprocessorPipeline.kt +++ b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/PreprocessorPipeline.kt @@ -8,6 +8,7 @@ data class PipelineConfig( val dbFile: File, val kaikkiFile: File? = null, val sublexFile: File? = null, + val dictionarySeedFile: File? = null, val falseCognatesFile: File? = null, val curatedLessonsFile: File? = null, val tatoebaFile: File? = null, @@ -35,10 +36,13 @@ class PreprocessorPipeline(private val config: PipelineConfig) { /** Executes the pipeline stages on an already opened [SeedDatabase]. */ fun executeWithDatabase(database: SeedDatabase): PipelineSummary { val sublexFrequencies = loadSublexFrequencies() - val dictCount = processKaikki(database, sublexFrequencies) + val dictSeedCount = processDictionarySeed(database) + val kaikkiCount = processKaikki(database, sublexFrequencies) + val dictCount = dictSeedCount + kaikkiCount val falseCognatesCount = processFalseCognates(database) val lessonsCount = processCuratedLessons(database) + processTatoeba(database) val rulesCount = processRules(database) + database.optimize() return PipelineSummary( dictionaryRows = dictCount, @@ -54,6 +58,14 @@ class PreprocessorPipeline(private val config: PipelineConfig) { return FileInputStream(file).use { SublexReader.readFrequencies(it) } } + private fun processDictionarySeed(database: SeedDatabase): Long { + val file = config.dictionarySeedFile + if (file == null || !file.exists()) return 0L + return FileInputStream(file).use { + DictionarySeedProcessor.processDictionarySeed(it, database).toLong() + } + } + private fun processKaikki(database: SeedDatabase, sublexFrequencies: Map): Long { val file = config.kaikkiFile if (file == null || !file.exists()) return 0L diff --git a/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/SeedDatabase.kt b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/SeedDatabase.kt index 4b9b142..e12d60a 100644 --- a/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/SeedDatabase.kt +++ b/tools/preprocessor/src/main/kotlin/com/anysoftkeyboard/calque/preprocessor/SeedDatabase.kt @@ -13,7 +13,7 @@ import java.sql.Types * * Implements the table schemas defined in docs/DATA.md. */ -class SeedDatabase(private val connection: Connection) : AutoCloseable { +class SeedDatabase(internal val connection: Connection) : AutoCloseable { init { DatabaseBinders.createTables(connection) diff --git a/tools/preprocessor/src/test/kotlin/com/anysoftkeyboard/calque/preprocessor/PreprocessorTest.kt b/tools/preprocessor/src/test/kotlin/com/anysoftkeyboard/calque/preprocessor/PreprocessorTest.kt index 0088fa4..c14152e 100644 --- a/tools/preprocessor/src/test/kotlin/com/anysoftkeyboard/calque/preprocessor/PreprocessorTest.kt +++ b/tools/preprocessor/src/test/kotlin/com/anysoftkeyboard/calque/preprocessor/PreprocessorTest.kt @@ -200,7 +200,7 @@ class PreprocessorTest { "base", "fantastic", true, - 300 + 300, ), ), ) @@ -352,6 +352,59 @@ class PreprocessorTest { } } + @Test + fun dictionarySeedProcessor_parsesAndInsertsEntries() { + val inMemoryDb = SeedDatabase.open() + inMemoryDb.use { db -> + val sampleJson = + """ + [ + { + "surface_form": "hotel", + "lemma": "hotel", + "pos": "noun", + "grammar_info": "masc. sing.", + "english_gloss": "hotel", + "is_cognate": true, + "frequency_rank": 500 + } + ] + """ + .trimIndent() + + val count = + DictionarySeedProcessor.processDictionarySeed(sampleJson.byteInputStream(), db) + assertEquals(1, count) + + val hotel = db.lookupWord("hotel") + assertNotNull(hotel) + assertTrue(hotel?.isCognate == true) + assertEquals(500, hotel?.frequencyRank) + } + } + + @Test + fun curatedDictionarySeed_isValidAndPopulatesDatabase() { + val seedFile = File("data/seeds/dictionary_seed.json") + assertTrue("Expected seed file at ${seedFile.path}", seedFile.exists()) + + SeedDatabase.open().use { db -> + val insertedCount = + FileInputStream(seedFile).use { stream -> + DictionarySeedProcessor.processDictionarySeed(stream, db) + } + assertTrue("Expected >= 100 entries, found $insertedCount", insertedCount >= 100) + + val hotelInfo = db.lookupWord("hotel") + assertNotNull(hotelInfo) + assertTrue(hotelInfo?.isCognate == true) + + val esInfo = db.lookupWord("es") + assertNotNull(esInfo) + assertEquals("3rd sing. present", esInfo?.grammar) + } + } + @Test fun pipeline_executesEndToEndSuccessfully() { val tempDir = createTempDirectory("calque_pipeline_test").toFile() @@ -366,6 +419,7 @@ class PreprocessorTest { val kaikkiFile = File(tempDir, "kaikki.jsonl") kaikkiFile.writeText("$sampleKaikki\n", StandardCharsets.UTF_8) + val dictionarySeedFile = File("data/seeds/dictionary_seed.json") val falseCognatesSeedFile = File("data/seeds/false_cognates_seed.json") val curatedLessonsSeedFile = File("data/seeds/curated_lessons_seed.json") val cognateRulesSeedFile = File("data/seeds/cognate_rules_seed.json") @@ -375,6 +429,7 @@ class PreprocessorTest { dbFile = dbFile, kaikkiFile = kaikkiFile, sublexFile = sublexFile, + dictionarySeedFile = dictionarySeedFile, falseCognatesFile = falseCognatesSeedFile, curatedLessonsFile = curatedLessonsSeedFile, cognateRulesFile = cognateRulesSeedFile, @@ -384,9 +439,9 @@ class PreprocessorTest { val summary = pipeline.execute() assertTrue(dbFile.exists()) - assertTrue(summary.dictionaryRows >= 2) - // 25 base lemmas + "carpetas" plural from Kaikki = 26 rows - assertEquals(26, summary.falseCognateRows) + assertTrue(summary.dictionaryRows >= 100) + // 25 base lemmas + "carpetas" plural from Kaikki/dictionary = at least 26 rows + assertTrue(summary.falseCognateRows >= 26) assertEquals(8, summary.curatedLessonRows) assertEquals(12, summary.cognateRuleRows)