diff --git a/.github/workflows/semanticemojifilter-ci.yml b/.github/workflows/semanticemojifilter-ci.yml
new file mode 100644
index 0000000..b5ab3f1
--- /dev/null
+++ b/.github/workflows/semanticemojifilter-ci.yml
@@ -0,0 +1,71 @@
+name: SemanticEmojiFilter CI
+
+on:
+ pull_request:
+ paths:
+ - "SemanticEmojiFilter/**"
+ - "skainet-ui/**"
+ - ".github/workflows/semanticemojifilter-ci.yml"
+ push:
+ branches:
+ - develop
+ paths:
+ - "SemanticEmojiFilter/**"
+ - "skainet-ui/**"
+ - ".github/workflows/semanticemojifilter-ci.yml"
+
+concurrency:
+ group: semanticemojifilter-ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "21"
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Make gradlew executable
+ run: chmod +x SemanticEmojiFilter/gradlew
+
+ # composeApp's Kotlin compile tasks depend on the bundled LEAF checkpoint (fetchModel โ see
+ # composeApp/build.gradle.kts) even for a plain compile check, since Compose resource
+ # processing needs the files present. Cache them so only the first run per revision pays
+ # the ~92 MB download.
+ - name: Cache bundled LEAF checkpoint
+ uses: actions/cache@v4
+ with:
+ path: SemanticEmojiFilter/composeApp/src/wasmJsMain/composeResources/files/leaf
+ key: semanticemojifilter-leaf-4262131b
+
+ # Unit tests on JVM + Android (no device), the Android debug APK, and a wasm compile check;
+ # then the opt-in Laya verifier module (desktop + Android source sets, its own tests).
+ - name: Build and test
+ working-directory: SemanticEmojiFilter
+ run: |
+ ./gradlew \
+ :shared:jvmTest \
+ :shared:testAndroidHostTest \
+ :cli:compileKotlin \
+ :androidApp:assembleDebug \
+ :shared:compileKotlinWasmJs \
+ :composeApp:compileKotlinWasmJs \
+ --console=plain
+ - name: Build and test the Laya verifier
+ working-directory: SemanticEmojiFilter
+ run: |
+ ./gradlew -PlayaVerifier=true \
+ :verifier:jvmTest \
+ :cli:compileKotlin \
+ :composeApp:compileKotlinJvm \
+ --console=plain
diff --git a/README.md b/README.md
index 792bcd8..f5ebe21 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,21 @@ falls back to without NEON hardware.
---
+### ๐ Semantic Emoji Filter
+
+Type what you mean, get the emoji. Every keystroke is embedded on-device with the MongoDB LEAF
+retrieval encoder (23M parameters, BERT in the SKaiNET NN DSL) and ranked against a bilingual
+catalog of about 200 emoji; items above a calibrated threshold move up with a confidence badge.
+Desktop and Android add a second stage: the **Laya decision model** answers a typed yes/no
+question per candidate through `sk.ainet.decisions`, so ๐ drops for "healthy things to eat"
+while ๐ฅ stays, and an unsure model says `?` instead of guessing.
+
+
+
+๐ [`SemanticEmojiFilter/`](SemanticEmojiFilter/) ยท runs on Android ยท Desktop ยท Wasm
+
+---
+
### ๐ฌ Kllama Demo
A Qwen3 LLM **playground in the browser** โ chat, completion, translation, tool
diff --git a/SemanticEmojiFilter/.gitignore b/SemanticEmojiFilter/.gitignore
new file mode 100644
index 0000000..e7752f8
--- /dev/null
+++ b/SemanticEmojiFilter/.gitignore
@@ -0,0 +1,6 @@
+.gradle/
+build/
+local.properties
+kotlin-js-store/
+# LEAF checkpoint for the web bundle โ fetched by scripts/fetch-model.sh, never committed
+composeApp/src/wasmJsMain/composeResources/files/leaf/
diff --git a/SemanticEmojiFilter/.java-version b/SemanticEmojiFilter/.java-version
new file mode 100644
index 0000000..5f39e91
--- /dev/null
+++ b/SemanticEmojiFilter/.java-version
@@ -0,0 +1 @@
+21.0
diff --git a/SemanticEmojiFilter/README.md b/SemanticEmojiFilter/README.md
new file mode 100644
index 0000000..07cdab9
--- /dev/null
+++ b/SemanticEmojiFilter/README.md
@@ -0,0 +1,179 @@
+# Semantic Emoji Filter
+
+Type what you mean, get the emoji. A Compose Multiplatform app (Android, Desktop, Web) that ranks
+a bilingual catalog of about 200 emoji by the meaning of a free-text query, on-device, with
+SKaiNET. Desktop and Android add a second stage in which a typed decision model re-checks the top
+hits, the part embeddings alone get wrong.
+
+## What it demonstrates
+
+**System 1: embedding filter, on every keystroke.** The query is encoded with the MongoDB LEAF
+retrieval encoder (`mdbr-leaf-ir`, 23M parameters, Apache-2.0) through the SKaiNET-transformers
+BERT runtime: WordPiece tokenizer from the engine, 6 encoder layers, mean pooling, the model's
+`2_Dense` projection to 768 dimensions, L2 normalisation. The catalog embeddings are computed once
+(or shipped precomputed for the web build), so a sweep is a dot product over 200 rows. Items whose
+calibrated confidence passes the threshold move to the top tier.
+
+**System 2: typed decisions, when the query settles.** Cosine similarity cannot do qualifiers:
+"healthy things to eat" ranks ๐ near ๐ฅ because *food* dominates the vector. After the query has
+been stable for half a second, the top candidates go to the **Laya** decision model through the
+`sk.ainet.decisions` contract, one boolean question per item ("is this emoji a good answer to the
+search?"). The badge then shows both numbers, the vector confidence and Laya's probability; a
+candidate the model is unsure about shows `?` instead of a guess (policy: reject below 0.2,
+accept from 0.6). This stage is opt-in
+(`-PlayaVerifier=true`), Desktop and Android only, and visibly asynchronous: Laya is a 322M-parameter
+encoder run on the eager FP32 CPU path, several seconds per question on a laptop.
+
+**Honest numbers.** The diagnostics row shows the kernel tier, model load time and the live query
+latency; the CLI below produced the numbers in this README on the machine named there.
+
+## Run
+
+The LEAF checkpoint (92 MB) is downloaded on first run into `~/.skainet-examples/semantic-emoji-filter/leaf/`
+(desktop, CLI) or the app's files directory (Android); the web build bundles it. Set
+`LEAF_MODEL_DIR` to reuse an existing copy.
+
+### Desktop
+
+```bash
+./gradlew :composeApp:run
+# with the Laya verifier (SKaiNET-transformers 0.60.0 decisions + laya modules, from Maven Central)
+./gradlew -PlayaVerifier=true :composeApp:run
+```
+
+### Android
+
+```bash
+./gradlew :androidApp:installDebug
+./gradlew -PlayaVerifier=true :androidApp:installDebug # verifier: needs a device that grants a 2 GB heap
+```
+
+### Web (Wasm)
+
+```bash
+./gradlew :composeApp:wasmJsBrowserDevelopmentRun # fetches the checkpoint into composeResources first
+./gradlew :composeApp:wasmJsBrowserDistribution # what the examples site deploys
+```
+
+### CLI (measure, calibrate, regenerate)
+
+```bash
+./gradlew :cli:run --args="bench" # load, catalog, query latency, RSS
+./gradlew :cli:run --args="query healthy things to eat"
+./gradlew :cli:run --args="embed-catalog" # writes catalog-embeddings.bin + digest
+./gradlew :cli:run --args="calibrate" # fits calibration.json on calibration-queries.json
+./gradlew :cli:run --args="golden" # records golden-queries.json for the ranking test
+```
+
+### The Laya verifier
+
+The typed-decision contract (`skainet-transformers-decisions-api`, `-decisions-runtime`) and the
+Laya adapter (`skainet-transformers-inference-laya`) ship with SKaiNET-transformers 0.60.0, which
+the version catalog pins; `-PlayaVerifier=true` adds the `:verifier` module and the Laya source
+directories. The checkpoint (644 MB, `convaiinnovations/laya` `multilingual/`) downloads on the
+first verification into `~/.skainet-examples/semantic-emoji-filter/laya-multilingual/`
+(`LAYA_MODEL_DIR` overrides).
+
+## Measured
+
+Laptop, 12 cores, JDK 21 with the Vector API (`Panama Vector (SIMD)` tier), SKaiNET 0.58.0,
+2026-10-13, `./gradlew :cli:run --args="bench"`:
+
+| | DIRECT (default) | OPTIMIZED (traced graph) |
+|---|---|---|
+| LEAF load (SafeTensors, 92 MB) | 345 ms | 352 ms |
+| catalog embedding, 208 items, once | 23.4 s (112 ms/item) | 45.2 s (217 ms/item) |
+| query embedding, p50 / p95 (20 queries ร 5) | 81 ms / 98 ms | 106 ms / 123 ms |
+| cosine sweep over 208 ร 768 | 0.28 ms | 0.26 ms |
+| RSS after the run | 512 MB | 960 MB |
+
+The query carries LEAF's retrieval prompt (`Represent this sentence for searching relevant passages: `),
+which triples the token count of a short query and costs about 40 ms per keystroke; without the
+prompt the ranking is useless (every cosine lands between 0.70 and 0.79 and ๐ beats ๐ค for
+"feeling sick"). OPTIMIZED traces one graph per sequence length and buys nothing here, so DIRECT is
+the default. The catalog embedding is skipped when the shipped `catalog-embeddings.bin` matches the
+catalog digest; the web build always uses it.
+
+Calibration on 28 labelled English queries (151 positive pairs of 5 824): logistic fit
+`k = 22.1`, `x0 = 0.279`, F1-optimal threshold 0.8 (cosine 0.341), top tier capped at 12 items;
+training precision 0.60, recall 0.60. LEAF cosines are compressed (matches 0.30โ0.40, the rest
+0.10โ0.30), which is exactly why a second stage earns its place: for "healthy things to eat" the
+twelve items above the threshold include ๐ฅฌ ๐ฅ ๐ ๐ฅ ๐ฅ ๐
and also ๐ฅ ๐ฅ ๐ฉ ๐ฃ.
+
+LEAF is an English retrieval encoder; German queries work through the German names in the catalog
+text but rank visibly worse, and were left out of the calibration. The Laya checkpoint is
+multilingual.
+
+**Laya verifier** (`./gradlew -PlayaVerifier=true :cli:run --args="verify ..."`, same laptop, eager
+FP32 CPU path, exact GELU): checkpoint load 9.0 s, 1.8โ3.6 s per decision (first one slowest),
+RSS 4.8 GB.
+
+| query | candidate | P(match) | verdict |
+|---|---|---|---|
+| feeling sick | ๐คข ๐คฎ ๐ค ๐คง | 0.94 ยท 0.95 ยท 0.95 ยท 0.85 | yes, yes, yes, yes |
+| healthy things to eat | ๐ฅ green salad | 0.34 | ? |
+| | ๐ฅฌ leafy green | 0.17 | no |
+| | ๐ red apple ยท ๐ฅ bacon | 0.07 ยท 0.07 | no |
+| | ๐ฅ peanuts ยท ๐ฉ doughnut | 0.03 ยท 0.03 | no |
+
+The ordering is right (salad above leafy green above bacon above doughnut, under every wording
+tried), the absolute probabilities are conservative: the base checkpoint is a starting point its
+publisher expects to be specialised. The acceptance policy is therefore asymmetric, reject below
+0.2, accept from 0.6, so the salad shows as "unsure" and the doughnut as a clear no, and the apple
+is a wrong confident rejection the demo does not hide. Four wordings of the question were
+compared from the CLI (`verify --statement ... --no ... --yes ...`); the default has the widest
+spread between the right and the wrong answer.
+
+## Architecture
+
+```
+shared/ commonMain domain/ Catalog, CatalogItem.embeddingText(), Calibration (logistic fit), RankedItem/Tier
+ engine/ LeafEmbeddingEngine (BERT runtime + engine tokenizer), CatalogIndex (sweep, .bin),
+ ByteArrayRandomAccessSource (web), LeafFiles (pinned HF revision)
+ model/ LeafProvider + LeafResolver (pure plan); jvmAndroidMain: streamed download
+ verify/ Verifier API (Verdict, VerifierState, NoopVerifier) โ no Laya dependency
+ vm/ EmojiSearchViewModel: debounce 120 ms, mapLatest, stable-query trigger 500 ms
+ jvmMain / androidMain / wasmJsMain platform actuals, checkpoint providers
+verifier/ jvm + android only LayaVerifier (one request per item, cancellable between items, verdict cache),
+ EmojiMatchSchema (decisionSchema { probability(...); policy { boolean { abstainBetween(0.4, 0.6) } } }),
+ DecisionAdapterSource (the seam a decision cartridge replaces later), LayaFileSource
+composeApp/ Compose UI; src/{jvm,android}{Verifier,NoVerifier}/ pick the verifier at build time
+androidApp/ MainActivity, EmojiApp
+cli/ bench | embed-catalog | calibrate | golden | query | tokens
+scripts/ fetch-model.sh (LEAF, pinned revision), generate-catalog.py (CLDR -> catalog.json), catalog-selection.json
+```
+
+The ranking flow never waits for the verifier: verdicts stream into a separate state and are
+cleared when the query changes. The verifier sends one request per candidate rather than one
+request with many questions, because the Laya adapter runs one encoder pass per question either
+way and a per-item request gives a cancellation point every few seconds instead of one long call.
+
+## Catalog
+
+`catalog.json` is generated from Unicode CLDR 47 annotations (English and German names and
+keywords) for the emoji listed in `scripts/catalog-selection.json`; the `tags` and `group` fields
+are this example's own curation so the showcase queries (healthy food, feeling sick, rock band)
+have a defined answer set. The encoder never sees the tags: the text it embeds is
+`". . "` (`CatalogItem.embeddingText()`), and the calibration is
+fitted on `calibration-queries.json`, 28 hand-labelled English queries. Regenerate with
+
+```bash
+python3 -I scripts/generate-catalog.py --cldr \
+ --selection scripts/catalog-selection.json --out composeApp/src/commonMain/composeResources/files/catalog/catalog.json
+./gradlew :cli:run --args="embed-catalog" && ./gradlew :cli:run --args="calibrate" && ./gradlew :cli:run --args="golden"
+```
+
+## Out of scope
+
+* **iOS**: the bert, laya and decisions modules of SKaiNET-transformers publish no Apple targets yet.
+* **A trained classifier head** over the embeddings (the PRD's "Mode B"); a follow-up example.
+* **Laya in the browser**: 644 MB of weights against a web page budget. The seam is there if a
+ smaller decision checkpoint appears.
+* **Cartridges**: the verifier loads the checkpoint through `DecisionAdapterSource`, one file;
+ a SKaiNET decision cartridge (task profile `decision/v1`, blueprint) replaces that source without
+ touching the UI or the view model once it exists.
+
+## Licenses
+
+MIT for this example. Third-party content and licenses: `THIRD_PARTY_LICENSES/NOTICE` (LEAF and
+Laya under Apache-2.0, CLDR under the Unicode License v3).
diff --git a/SemanticEmojiFilter/THIRD_PARTY_LICENSES/Apache-2.0.txt b/SemanticEmojiFilter/THIRD_PARTY_LICENSES/Apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/SemanticEmojiFilter/THIRD_PARTY_LICENSES/Apache-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/SemanticEmojiFilter/THIRD_PARTY_LICENSES/MIT-coi-serviceworker.txt b/SemanticEmojiFilter/THIRD_PARTY_LICENSES/MIT-coi-serviceworker.txt
new file mode 100644
index 0000000..4932a51
--- /dev/null
+++ b/SemanticEmojiFilter/THIRD_PARTY_LICENSES/MIT-coi-serviceworker.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Guido Zuidhof
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/SemanticEmojiFilter/THIRD_PARTY_LICENSES/NOTICE b/SemanticEmojiFilter/THIRD_PARTY_LICENSES/NOTICE
new file mode 100644
index 0000000..5a07f4e
--- /dev/null
+++ b/SemanticEmojiFilter/THIRD_PARTY_LICENSES/NOTICE
@@ -0,0 +1,21 @@
+Third-party content used by the Semantic Emoji Filter example
+=============================================================
+
+MongoDB LEAF retrieval encoder (mdbr-leaf-ir)
+ https://huggingface.co/MongoDB/mdbr-leaf-ir (revision 4262131b32c3182bd06e67e92ae69d7bd66e0c5c)
+ Copyright MongoDB, Inc. Licensed under the Apache License, Version 2.0 (Apache-2.0.txt).
+ Downloaded at build time (web) or first run (desktop, Android); not redistributed in this repository.
+
+Laya decision model, multilingual checkpoint
+ https://huggingface.co/convaiinnovations/laya (revision 7b928d828b7b0e022f929d9bd2e44165aa270148)
+ Copyright Convai Innovations. Licensed under the Apache License, Version 2.0 (Apache-2.0.txt).
+ Downloaded on first use of the optional verifier (-PlayaVerifier=true); not redistributed.
+
+Unicode CLDR emoji annotations (release 47)
+ https://github.com/unicode-org/cldr common/annotations/{en,de}.xml, common/annotationsDerived/{en,de}.xml
+ Copyright Unicode, Inc. Licensed under the Unicode License v3 (Unicode-3.0.txt).
+ The English and German emoji names and keywords in catalog/catalog.json are derived from these files
+ by scripts/generate-catalog.py; the tags and groups are this example's own curation.
+
+coi-serviceworker (web build)
+ https://github.com/gzuidhof/coi-serviceworker MIT License, see the header of composeApp/src/wasmJsMain/resources/coi-serviceworker.js.
diff --git a/SemanticEmojiFilter/THIRD_PARTY_LICENSES/Unicode-3.0.txt b/SemanticEmojiFilter/THIRD_PARTY_LICENSES/Unicode-3.0.txt
new file mode 100644
index 0000000..56da589
--- /dev/null
+++ b/SemanticEmojiFilter/THIRD_PARTY_LICENSES/Unicode-3.0.txt
@@ -0,0 +1,39 @@
+UNICODE LICENSE V3
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright ยฉ 1991-2026 Unicode, Inc.
+
+NOTICE TO USER: Carefully read the following legal agreement. BY
+DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
+SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
+TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
+DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of data files and any associated documentation (the "Data Files") or
+software and any associated documentation (the "Software") to deal in the
+Data Files or Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, and/or sell
+copies of the Data Files or Software, and to permit persons to whom the
+Data Files or Software are furnished to do so, provided that either (a)
+this copyright and permission notice appear with all copies of the Data
+Files or Software, or (b) this copyright and permission notice appear in
+associated Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+THIRD PARTY RIGHTS.
+
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
+BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
+FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder shall
+not be used in advertising or otherwise to promote the sale, use or other
+dealings in these Data Files or Software without prior written
+authorization of the copyright holder.
diff --git a/SemanticEmojiFilter/androidApp/build.gradle.kts b/SemanticEmojiFilter/androidApp/build.gradle.kts
new file mode 100644
index 0000000..a9630e2
--- /dev/null
+++ b/SemanticEmojiFilter/androidApp/build.gradle.kts
@@ -0,0 +1,67 @@
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.composeMultiplatform)
+ alias(libs.plugins.composeCompiler)
+}
+
+val layaVerifier = providers.gradleProperty("layaVerifier").orNull == "true"
+
+kotlin {
+ dependencies {
+ implementation(projects.composeApp)
+ implementation(projects.shared)
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.ui)
+ implementation(libs.compose.uiTooling)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.kotlinx.coroutines)
+ // Hand-written ARM NEON kernels โ Android JNI only.
+ implementation(libs.skainet.backend.jni.cpu)
+ }
+
+ target {
+ compilerOptions {
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
+ }
+ }
+}
+
+android {
+ namespace = "sk.ainet.samples.emoji"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
+ defaultConfig {
+ applicationId = "sk.ainet.samples.emoji"
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ targetSdk = libs.versions.android.targetSdk.get().toInt()
+ versionCode = 1
+ versionName = "1.0"
+ // ARM only โ the JNI NEON kernels are what makes the encoder fast on a phone.
+ ndk { abiFilters += "arm64-v8a" }
+
+ // Shown in the diagnostics row โ always matches the dependency in the catalog.
+ buildConfigField("String", "SKAINET_VERSION", "\"${libs.versions.skainet.asProvider().get()}\"")
+ buildConfigField("boolean", "VERIFIER_ENABLED", layaVerifier.toString())
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
diff --git a/SemanticEmojiFilter/androidApp/src/main/AndroidManifest.xml b/SemanticEmojiFilter/androidApp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b837ef2
--- /dev/null
+++ b/SemanticEmojiFilter/androidApp/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SemanticEmojiFilter/androidApp/src/main/kotlin/sk/ainet/samples/emoji/EmojiApp.kt b/SemanticEmojiFilter/androidApp/src/main/kotlin/sk/ainet/samples/emoji/EmojiApp.kt
new file mode 100644
index 0000000..56143ea
--- /dev/null
+++ b/SemanticEmojiFilter/androidApp/src/main/kotlin/sk/ainet/samples/emoji/EmojiApp.kt
@@ -0,0 +1,17 @@
+package sk.ainet.samples.emoji
+
+import android.app.Application
+import sk.ainet.samples.emoji.engine.LeafEmbeddingEngine
+import sk.ainet.samples.emoji.model.AndroidLeafProvider
+import sk.ainet.samples.emoji.verify.Verifier
+
+/** Application-scoped factories: the checkpoint provider and the verifier live for the app lifetime. */
+class EmojiApp : Application() {
+ val leafProvider by lazy { AndroidLeafProvider(this) }
+ val verifier: Verifier by lazy { createVerifier(this) }
+
+ suspend fun loadEngine(onProgress: (String) -> Unit): LeafEmbeddingEngine {
+ val source = leafProvider.resolve(onProgress)
+ return LeafEmbeddingEngine.load(source, onProgress = onProgress)
+ }
+}
diff --git a/SemanticEmojiFilter/androidApp/src/main/kotlin/sk/ainet/samples/emoji/MainActivity.kt b/SemanticEmojiFilter/androidApp/src/main/kotlin/sk/ainet/samples/emoji/MainActivity.kt
new file mode 100644
index 0000000..d7a2d96
--- /dev/null
+++ b/SemanticEmojiFilter/androidApp/src/main/kotlin/sk/ainet/samples/emoji/MainActivity.kt
@@ -0,0 +1,34 @@
+package sk.ainet.samples.emoji
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import kotlinx.coroutines.runBlocking
+import sk.ainet.samples.emoji.vm.EmojiSearchViewModel
+
+class MainActivity : ComponentActivity() {
+
+ private val app get() = applicationContext as EmojiApp
+
+ private val viewModel: EmojiSearchViewModel by viewModels {
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T = EmojiSearchViewModel(
+ loadEngine = { onProgress -> app.loadEngine(onProgress) },
+ loadIndex = { engine, onProgress -> CatalogResources.index(CatalogResources.catalog(), engine, onProgress = onProgress) },
+ calibration = runBlocking { CatalogResources.calibration() },
+ verifier = app.verifier,
+ ) as T
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ App(viewModel = viewModel, skainetVersion = BuildConfig.SKAINET_VERSION)
+ }
+ }
+}
diff --git a/SemanticEmojiFilter/build.gradle.kts b/SemanticEmojiFilter/build.gradle.kts
new file mode 100644
index 0000000..25e833d
--- /dev/null
+++ b/SemanticEmojiFilter/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.androidMultiplatformLibrary) apply false
+ alias(libs.plugins.kotlinMultiplatform) apply false
+ alias(libs.plugins.kotlinJvm) apply false
+ alias(libs.plugins.kotlinSerialization) apply false
+ alias(libs.plugins.composeMultiplatform) apply false
+ alias(libs.plugins.composeCompiler) apply false
+}
diff --git a/SemanticEmojiFilter/cli/build.gradle.kts b/SemanticEmojiFilter/cli/build.gradle.kts
new file mode 100644
index 0000000..9f22b49
--- /dev/null
+++ b/SemanticEmojiFilter/cli/build.gradle.kts
@@ -0,0 +1,40 @@
+plugins {
+ alias(libs.plugins.kotlinJvm)
+ alias(libs.plugins.kotlinSerialization)
+ application
+}
+
+val layaVerifier = providers.gradleProperty("layaVerifier").orNull == "true"
+
+kotlin {
+ jvmToolchain(21)
+}
+
+dependencies {
+ implementation(projects.shared)
+ implementation(libs.kotlinx.coroutines)
+ implementation(libs.kotlinx.serialization.json)
+ // Panama FFM kernels for the JVM.
+ implementation(libs.skainet.backend.api)
+ if (layaVerifier) implementation(project(":verifier"))
+
+ testImplementation(kotlin("test"))
+}
+
+// The CLI reads the same catalog files the apps bundle, so one copy stays the source of truth.
+// The `verify` command exists only when the verifier module is part of the build.
+sourceSets.main {
+ resources.srcDir("../composeApp/src/commonMain/composeResources/files")
+ kotlin.srcDir(if (layaVerifier) "src/verifier/kotlin" else "src/noVerifier/kotlin")
+}
+
+application {
+ mainClass.set("sk.ainet.samples.emoji.cli.MainKt")
+}
+
+tasks.withType().configureEach {
+ jvmArgs("--enable-preview", "--add-modules", "jdk.incubator.vector", "-Dfile.encoding=UTF-8", "-Dstdout.encoding=UTF-8", "-Dstderr.encoding=UTF-8")
+ systemProperty("skainet.cpu.vector.enabled", "true")
+ maxHeapSize = if (layaVerifier) "6g" else "2g"
+ standardInput = System.`in`
+}
diff --git a/SemanticEmojiFilter/cli/src/main/kotlin/sk/ainet/samples/emoji/cli/Main.kt b/SemanticEmojiFilter/cli/src/main/kotlin/sk/ainet/samples/emoji/cli/Main.kt
new file mode 100644
index 0000000..d15ce29
--- /dev/null
+++ b/SemanticEmojiFilter/cli/src/main/kotlin/sk/ainet/samples/emoji/cli/Main.kt
@@ -0,0 +1,256 @@
+package sk.ainet.samples.emoji.cli
+
+import java.io.File
+import java.security.MessageDigest
+import kotlin.math.roundToLong
+import kotlin.time.TimeSource
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import sk.ainet.models.bert.BertExecutionMode
+import sk.ainet.samples.emoji.domain.Calibration
+import sk.ainet.samples.emoji.domain.Catalog
+import sk.ainet.samples.emoji.domain.Tier
+import sk.ainet.samples.emoji.engine.CatalogIndex
+import sk.ainet.samples.emoji.engine.LeafSource
+import sk.ainet.samples.emoji.engine.LeafEmbeddingEngine
+import sk.ainet.samples.emoji.model.DesktopLeafProvider
+import sk.ainet.samples.emoji.model.defaultLeafCacheDir
+import sk.ainet.samples.emoji.platform.kernelTierLabel
+
+private const val USAGE = """
+Semantic Emoji Filter โ JVM spike and tooling over the shared engine.
+
+ bench [--model DIR] [--mode direct|optimized] [--repeats N] load, catalog, query latency, RSS
+ embed-catalog [--model DIR] write catalog-embeddings.bin + sha256
+ calibrate [--model DIR] [--labels FILE] fit calibration.json on labelled queries
+ golden [--model DIR] write golden-queries.json for the ranking test
+ query [--model DIR] "" print the top 10
+ verify [--model DIR] [--top N] "" Laya verdicts for the top N (needs -PlayaVerifier=true)
+
+Options: --catalog-dir DIR (default: ../composeApp/src/commonMain/composeResources/files/catalog)
+ --model DIR (default: ${'$'}LEAF_MODEL_DIR or ~/.skainet-examples/semantic-emoji-filter/leaf;
+ missing files are downloaded)
+"""
+
+private val BENCH_QUERIES = listOf(
+ "healthy things to eat", "junk food", "feeling sick", "rock band", "cute pets", "rainy weather",
+ "going on holiday", "team sports", "very happy", "working late at the office",
+ "gesundes Essen", "mir ist schlecht", "Rockkonzert", "Haustiere", "Urlaub am Meer",
+ "Geburtstagsfeier", "winter sports", "breakfast", "something sweet", "fruit",
+)
+
+fun main(args: Array) {
+ if (args.isEmpty()) {
+ println(USAGE.trimIndent())
+ return
+ }
+ val opts = Options.parse(args.drop(1))
+ when (args[0]) {
+ "bench" -> bench(opts)
+ "embed-catalog" -> embedCatalog(opts)
+ "calibrate" -> calibrate(opts)
+ "golden" -> golden(opts)
+ "query" -> query(opts)
+ "tokens" -> tokens(opts)
+ "verify" -> verify(opts)
+ else -> {
+ System.err.println("unknown command '${args[0]}'")
+ println(USAGE.trimIndent())
+ System.exit(2)
+ }
+ }
+}
+
+private class Options(val flags: Map, val positional: List) {
+ val modelDir: String get() = flags["model"] ?: defaultLeafCacheDir()
+ val catalogDir: File get() = File(flags["catalog-dir"] ?: "../composeApp/src/commonMain/composeResources/files/catalog").canonicalFile
+ val mode: BertExecutionMode get() = if (flags["mode"] == "optimized") BertExecutionMode.OPTIMIZED else BertExecutionMode.DIRECT
+ val repeats: Int get() = flags["repeats"]?.toInt() ?: 5
+
+ companion object {
+ fun parse(args: List): Options {
+ val flags = LinkedHashMap()
+ val positional = ArrayList()
+ var i = 0
+ while (i < args.size) {
+ val a = args[i]
+ if (a.startsWith("--") && i + 1 < args.size) {
+ flags[a.removePrefix("--")] = args[i + 1]
+ i += 2
+ } else {
+ positional += a
+ i++
+ }
+ }
+ return Options(flags, positional)
+ }
+ }
+}
+
+private fun loadCatalog(opts: Options): Catalog = Catalog.parse(File(opts.catalogDir, "catalog.json").readText())
+
+private fun loadEngine(opts: Options): LeafEmbeddingEngine = runBlocking {
+ val source = DesktopLeafProvider(opts.modelDir).resolve { System.err.println("[MODEL] $it") }
+ val engine = LeafEmbeddingEngine.load(source, mode = opts.mode) { System.err.println("[MODEL] $it") }
+ System.err.println("[MODEL] loaded ${(source as LeafSource.Directory).path} in ${engine.loadMillis} ms, ${engine.dimensions} dims, kernels: ${kernelTierLabel()}")
+ engine
+}
+
+private fun buildIndex(catalog: Catalog, engine: LeafEmbeddingEngine, log: Boolean = true): Pair {
+ val started = TimeSource.Monotonic.markNow()
+ val index = CatalogIndex.build(catalog.items, engine)
+ val ms = started.elapsedNow().inWholeMilliseconds
+ if (log) System.err.println("[INDEX] ${catalog.items.size} items embedded in $ms ms (${"%.1f".format(ms.toDouble() / catalog.items.size)} ms/item)")
+ return index to ms
+}
+
+private fun rssMb(): Long? = runCatching {
+ File("/proc/self/status").readLines().firstOrNull { it.startsWith("VmRSS:") }
+ ?.split(Regex("\\s+"))?.get(1)?.toLong()?.div(1024)
+}.getOrNull()
+
+private fun percentile(sorted: List, p: Double): Long = sorted[((sorted.size - 1) * p).roundToLong().toInt()]
+
+private fun bench(opts: Options) {
+ val catalog = loadCatalog(opts)
+ val engine = loadEngine(opts)
+ val (index, indexMs) = buildIndex(catalog, engine)
+ val calibration = loadCalibration(opts)
+
+ // Warm-up, then timed repeats per query.
+ BENCH_QUERIES.take(3).forEach { engine.embedQuery(it) }
+ val embedTimes = ArrayList()
+ val sweepTimes = ArrayList()
+ repeat(opts.repeats) {
+ for (q in BENCH_QUERIES) {
+ val t0 = System.nanoTime()
+ val v = engine.embedQuery(q)
+ val t1 = System.nanoTime()
+ index.rank(v, calibration)
+ val t2 = System.nanoTime()
+ embedTimes += (t1 - t0) / 1_000_000
+ sweepTimes += (t2 - t1) / 1_000
+ }
+ }
+ val e = embedTimes.sorted()
+ val s = sweepTimes.sorted()
+ println("mode=${opts.mode} kernels=${kernelTierLabel()} cores=${Runtime.getRuntime().availableProcessors()}")
+ println("model load: ${engine.loadMillis} ms")
+ println("catalog embed: $indexMs ms for ${catalog.items.size} items (${"%.1f".format(indexMs.toDouble() / catalog.items.size)} ms/item)")
+ println("query embed: p50 ${percentile(e, 0.5)} ms, p95 ${percentile(e, 0.95)} ms, max ${e.last()} ms (${e.size} runs, ${BENCH_QUERIES.size} queries x ${opts.repeats})")
+ println("cosine sweep: p50 ${percentile(s, 0.5)} us, p95 ${percentile(s, 0.95)} us")
+ println("RSS: ${rssMb()?.let { "$it MB" } ?: "n/a"}")
+ println()
+ for (q in listOf("healthy things to eat", "feeling sick", "rock band")) {
+ val top = index.rank(engine.embedQuery(q), calibration).take(8)
+ println("$q -> " + top.joinToString(" ") { "${it.item.emoji}${"%.2f".format(it.cosine)}" })
+ }
+}
+
+private fun embedCatalog(opts: Options) {
+ val catalog = loadCatalog(opts)
+ val engine = loadEngine(opts)
+ val (index, _) = buildIndex(catalog, engine)
+ val bytes = index.toBytes()
+ val out = File(opts.catalogDir, "catalog-embeddings.bin")
+ out.writeBytes(bytes)
+ val sha = MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { "%02x".format(it) }
+ val updated = catalog.copy(embeddingsSha256 = sha)
+ File(opts.catalogDir, "catalog.json").writeText(Catalog.encode(updated) + "\n")
+ println("wrote ${out.path} (${bytes.size} bytes, ${index.dimensions} dims), sha256 $sha recorded in catalog.json")
+}
+
+@Serializable
+private data class LabelledQuery(val query: String, val matches: List)
+
+private fun loadCalibration(opts: Options): Calibration =
+ File(opts.catalogDir, "calibration.json").takeIf { it.isFile }?.let { Calibration.parse(it.readText()) } ?: Calibration.DEFAULT
+
+private fun calibrate(opts: Options) {
+ val catalog = loadCatalog(opts)
+ val labelsFile = File(opts.flags["labels"] ?: File(opts.catalogDir, "calibration-queries.json").path)
+ val labels = Json { ignoreUnknownKeys = true }.decodeFromString>(labelsFile.readText())
+ val engine = loadEngine(opts)
+ val (index, _) = buildIndex(catalog, engine)
+ val samples = ArrayList>()
+ val unknown = labels.flatMap { it.matches }.filter { catalog.item(it) == null }
+ require(unknown.isEmpty()) { "labels name unknown ids: $unknown" }
+ for (label in labels) {
+ val cos = index.cosines(engine.embedQuery(label.query))
+ catalog.items.forEachIndexed { i, item -> samples += cos[i].toDouble() to (item.id in label.matches) }
+ }
+ val fitted = Calibration.fit(samples).copy(queries = labels.size)
+ var tp = 0; var fp = 0; var fn = 0
+ for ((x, y) in samples) {
+ val predicted = fitted.isTop(x)
+ if (predicted && y) tp++ else if (predicted && !y) fp++ else if (!predicted && y) fn++
+ }
+ val precision = if (tp + fp == 0) 0.0 else tp.toDouble() / (tp + fp)
+ val recall = if (tp + fn == 0) 0.0 else tp.toDouble() / (tp + fn)
+ File(opts.catalogDir, "calibration.json").writeText(Calibration.encode(fitted) + "\n")
+ println("k=${"%.3f".format(fitted.k)} x0=${"%.4f".format(fitted.x0)} threshold=${fitted.threshold} (cosine ${"%.3f".format(fitted.cosineAtThreshold)}), top tier capped at ${fitted.maxTop}")
+ val f1 = if (precision + recall == 0.0) 0.0 else 2 * precision * recall / (precision + recall)
+ println("${labels.size} queries, ${samples.count { it.second }} positives of ${samples.size}; accuracy ${"%.3f".format(fitted.accuracy)}, precision ${"%.2f".format(precision)}, recall ${"%.2f".format(recall)}, F1 ${"%.2f".format(f1)}")
+}
+
+@Serializable
+private data class GoldenQuery(val query: String, val vector: List, val expectedTop: List)
+
+private fun golden(opts: Options) {
+ val catalog = loadCatalog(opts)
+ val engine = loadEngine(opts)
+ val (index, _) = buildIndex(catalog, engine)
+ val calibration = loadCalibration(opts)
+ val goldens = listOf("healthy things to eat", "feeling sick", "rock band", "cute pets", "beach holiday").map { q ->
+ val v = engine.embedQuery(q)
+ GoldenQuery(q, v.toList(), index.rank(v, calibration).take(3).map { it.item.id })
+ }
+ val out = File(opts.catalogDir, "golden-queries.json")
+ out.writeText(Json.encodeToString(goldens) + "\n")
+ println("wrote ${out.path}: " + goldens.joinToString("; ") { g -> "${g.query} -> ${g.expectedTop.joinToString(",") { catalog.item(it)!!.emoji }}" })
+}
+
+/** System 2 on the CLI: embed, rank, then ask Laya about the top items. */
+private fun verify(opts: Options) {
+ val text = opts.positional.joinToString(" ").ifBlank { error("query text missing") }
+ val catalog = loadCatalog(opts)
+ val engine = loadEngine(opts)
+ val calibration = loadCalibration(opts)
+ val embeddings = File(opts.catalogDir, "catalog-embeddings.bin")
+ val index = if (embeddings.isFile) CatalogIndex.fromBytes(catalog.items, embeddings.readBytes(), engine.dimensions) else buildIndex(catalog, engine).first
+ val n = opts.flags["top"]?.toInt() ?: 6
+ val ranked = index.rank(engine.embedQuery(text), calibration)
+ val candidates = ranked.filter { it.tier == Tier.TOP }.take(n).ifEmpty { ranked.take(3) }
+ println("'$text' -> asking Laya about " + candidates.joinToString(" ") { "${it.item.emoji}(${"%.2f".format(it.cosine)})" })
+ verifyCommand(text, candidates.map { it.item }, opts.flags)
+}
+
+/** Debug: token ids of a text through tokenizer.json and vocab.txt, to catch normalisation differences. */
+private fun tokens(opts: Options) {
+ val text = opts.positional.joinToString(" ").ifBlank { error("text missing") }
+ val dir = File(opts.modelDir)
+ val fromJson = sk.ainet.frontend.tokenizer.TextFrontEnds.fromTokenizerJson(File(dir, "tokenizer.json").readText(), File(dir, "tokenizer_config.json").takeIf { it.isFile }?.readText()) as sk.ainet.frontend.text.BatchTokenizer
+ val fromVocab = sk.ainet.frontend.tokenizer.TextFrontEnds.fromVocabTxt(File(dir, "vocab.txt").readText())
+ val a = fromJson.encodeWithMetadata(text)
+ val b = fromVocab.encodeWithMetadata(text)
+ println("tokenizer.json: ${a.ids.toList()}")
+ println("vocab.txt: ${b.ids.toList()}")
+ println("decoded(json): ${fromJson.decode(a.ids)}")
+}
+
+private fun query(opts: Options) {
+ val text = opts.positional.joinToString(" ").ifBlank { error("query text missing") }
+ val catalog = loadCatalog(opts)
+ val engine = loadEngine(opts)
+ val calibration = loadCalibration(opts)
+ val embeddings = File(opts.catalogDir, "catalog-embeddings.bin")
+ val index = if (embeddings.isFile) CatalogIndex.fromBytes(catalog.items, embeddings.readBytes(), engine.dimensions) else buildIndex(catalog, engine).first
+ val t0 = System.nanoTime()
+ val v = engine.embedQuery(text)
+ val ranked = index.rank(v, calibration)
+ println("'$text' embedded in ${(System.nanoTime() - t0) / 1_000_000} ms; ${ranked.count { it.tier == Tier.TOP }} above threshold")
+ for (r in ranked.take(10)) {
+ println(" ${r.item.emoji} cos ${"%.3f".format(r.cosine)} conf ${"%.2f".format(r.confidence)} ${if (r.tier == Tier.TOP) "TOP " else " "} ${r.item.nameEn} / ${r.item.nameDe}")
+ }
+}
diff --git a/SemanticEmojiFilter/cli/src/noVerifier/kotlin/sk/ainet/samples/emoji/cli/Verify.kt b/SemanticEmojiFilter/cli/src/noVerifier/kotlin/sk/ainet/samples/emoji/cli/Verify.kt
new file mode 100644
index 0000000..38cd96f
--- /dev/null
+++ b/SemanticEmojiFilter/cli/src/noVerifier/kotlin/sk/ainet/samples/emoji/cli/Verify.kt
@@ -0,0 +1,9 @@
+package sk.ainet.samples.emoji.cli
+
+/** Built without `-PlayaVerifier=true`. */
+internal fun verifyAvailable(): Boolean = false
+
+internal fun verifyCommand(query: String, items: List, flags: Map = emptyMap()) {
+ System.err.println("verify: this CLI was built without -PlayaVerifier=true")
+ System.exit(2)
+}
diff --git a/SemanticEmojiFilter/cli/src/verifier/kotlin/sk/ainet/samples/emoji/cli/Verify.kt b/SemanticEmojiFilter/cli/src/verifier/kotlin/sk/ainet/samples/emoji/cli/Verify.kt
new file mode 100644
index 0000000..7d49f62
--- /dev/null
+++ b/SemanticEmojiFilter/cli/src/verifier/kotlin/sk/ainet/samples/emoji/cli/Verify.kt
@@ -0,0 +1,34 @@
+package sk.ainet.samples.emoji.cli
+
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.runBlocking
+import sk.ainet.samples.emoji.domain.CatalogItem
+import sk.ainet.samples.emoji.verifier.LayaVerifier
+import sk.ainet.samples.emoji.verifier.layaDesktopSource
+
+internal fun verifyAvailable(): Boolean = true
+
+/** Runs the Laya verifier over [items] for [query] and prints every verdict with its timing. */
+internal fun verifyCommand(query: String, items: List, flags: Map = emptyMap()) {
+ val schema = sk.ainet.samples.emoji.verifier.EmojiMatchSchema.schema(
+ statement = flags["statement"] ?: sk.ainet.samples.emoji.verifier.EmojiMatchSchema.STATEMENT,
+ whenFalse = flags["no"] ?: sk.ainet.samples.emoji.verifier.EmojiMatchSchema.WHEN_FALSE,
+ whenTrue = flags["yes"] ?: sk.ainet.samples.emoji.verifier.EmojiMatchSchema.WHEN_TRUE,
+ )
+ val verifier = LayaVerifier(layaDesktopSource(), schema)
+ val started = System.nanoTime()
+ var loadMs = -1L
+ runBlocking {
+ verifier.verify(query, items).collect { v ->
+ if (loadMs < 0) loadMs = (System.nanoTime() - started) / 1_000_000 - v.millis
+ val item = items.first { it.id == v.itemId }
+ val p = v.probabilityTrue?.let { "%.2f".format(it) } ?: "fail"
+ val verdict = when (v.accepted) { true -> "YES"; false -> "no "; null -> "? " }
+ println(" ${item.emoji} $verdict P(match)=$p ${v.millis} ms ${item.nameEn}")
+ }
+ }
+ println("Laya load ${loadMs} ms, ${items.size} decisions in ${(System.nanoTime() - started) / 1_000_000 - loadMs} ms")
+ val rss = java.io.File("/proc/self/status").takeIf { it.isFile }?.readLines()?.firstOrNull { it.startsWith("VmRSS:") }?.split(Regex("\\s+"))?.get(1)?.toLong()?.div(1024)
+ if (rss != null) println("RSS: $rss MB")
+ verifier.close()
+}
diff --git a/SemanticEmojiFilter/composeApp/build.gradle.kts b/SemanticEmojiFilter/composeApp/build.gradle.kts
new file mode 100644
index 0000000..46b95be
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/build.gradle.kts
@@ -0,0 +1,135 @@
+import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.androidMultiplatformLibrary)
+ alias(libs.plugins.composeMultiplatform)
+ alias(libs.plugins.composeCompiler)
+}
+
+val layaVerifier = providers.gradleProperty("layaVerifier").orNull == "true"
+
+kotlin {
+ jvmToolchain(21)
+
+ // The Android app entry point (MainActivity, manifest) lives in :androidApp โ AGP 9 no
+ // longer allows 'com.android.application' in a KMP module. This is a plain KMP library on
+ // the Android axis; :androidApp depends on it for App().
+ android {
+ namespace = "sk.ainet.samples.emoji.library"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ minSdk = libs.versions.android.minSdk.get().toInt()
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+ }
+
+ jvm()
+
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ browser()
+ binaries.executable()
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.shared)
+ implementation("sk.ainet.ui:skainet-ui")
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.ui)
+ implementation(compose.components.resources)
+ implementation(compose.components.uiToolingPreview)
+ implementation(libs.androidx.lifecycle.viewmodelCompose)
+ implementation(libs.androidx.lifecycle.runtimeCompose)
+ implementation(libs.kotlinx.coroutines)
+ }
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ }
+ jvmMain.dependencies {
+ implementation(compose.desktop.currentOs)
+ implementation(libs.kotlinx.coroutinesSwing)
+ }
+ wasmJsMain.dependencies {
+ implementation(libs.kotlinx.coroutines)
+ }
+ // The Laya verifier is a build-time switch: with -PlayaVerifier=true the :verifier module
+ // is on the Desktop and Android classpath and the *Verifier source dirs provide
+ // createVerifier(); without it the *NoVerifier dirs return the no-op verifier.
+ val verifierDir = if (layaVerifier) "Verifier" else "NoVerifier"
+ jvmMain {
+ kotlin.srcDir("src/jvm$verifierDir/kotlin")
+ if (layaVerifier) dependencies { implementation(project(":verifier")) }
+ }
+ androidMain {
+ kotlin.srcDir("src/android$verifierDir/kotlin")
+ if (layaVerifier) dependencies { implementation(project(":verifier")) }
+ }
+ }
+}
+
+// Single source of truth for the JVM flags SKaiNET needs on desktop: the JDK Vector API
+// (incubator) for SIMD-accelerated CPU ops.
+val skainetSimdJvmArgs = listOf(
+ "--add-modules", "jdk.incubator.vector",
+ "--enable-preview",
+ "-Dskainet.cpu.vector.enabled=true",
+)
+
+tasks.withType().configureEach {
+ jvmArgs(skainetSimdJvmArgs)
+ maxHeapSize = if (layaVerifier) "6g" else "2g"
+}
+
+tasks.withType().configureEach {
+ jvmArgs(skainetSimdJvmArgs)
+}
+
+// The web build bundles the LEAF checkpoint as Compose resources (wasm has no filesystem).
+// Desktop and Android download the same files at runtime instead, so the directory sits under
+// wasmJsMain and never reaches the jar or the APK.
+val leafDir = layout.projectDirectory.dir("src/wasmJsMain/composeResources/files/leaf")
+
+val fetchModel by tasks.registering(Exec::class) {
+ description = "Downloads the MongoDB LEAF (mdbr-leaf-ir) checkpoint into the wasm Compose resources. Skips if present."
+ group = "build setup"
+ val scriptPath = rootProject.layout.projectDirectory.file("scripts/fetch-model.sh")
+ commandLine("bash", scriptPath.asFile.absolutePath, leafDir.asFile.absolutePath)
+ inputs.file(scriptPath)
+ outputs.dir(leafDir)
+ val modelFile = leafDir.file("model.safetensors").asFile
+ onlyIf { !modelFile.exists() || modelFile.length() < 80L * 1024 * 1024 }
+}
+
+// Every Kotlin compile task AND every Compose-resources copy task depends on the model being on
+// disk โ Gradle 9's strict implicit-dependency validator fails otherwise, since the
+// resource-copy task reads from a directory fetchModel writes into.
+tasks.matching {
+ it.name.startsWith("compileKotlin") ||
+ it.name.startsWith("compileJava") ||
+ it.name.startsWith("convertXmlValueResourcesFor") ||
+ it.name.startsWith("copyNonXmlValueResourcesFor") ||
+ it.name.startsWith("prepareComposeResourcesTaskFor") ||
+ it.name.startsWith("generateResourceAccessorsFor")
+}.configureEach { dependsOn(fetchModel) }
+
+compose.desktop {
+ application {
+ mainClass = "sk.ainet.samples.emoji.MainKt"
+
+ jvmArgs += skainetSimdJvmArgs
+ jvmArgs += if (layaVerifier) "-Xmx6g" else "-Xmx2g"
+
+ nativeDistributions {
+ targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
+ packageName = "sk.ainet.samples.emoji"
+ packageVersion = "1.0.0"
+ }
+ }
+}
diff --git a/SemanticEmojiFilter/composeApp/src/androidNoVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt b/SemanticEmojiFilter/composeApp/src/androidNoVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt
new file mode 100644
index 0000000..db0848b
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/androidNoVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt
@@ -0,0 +1,8 @@
+package sk.ainet.samples.emoji
+
+import android.content.Context
+import sk.ainet.samples.emoji.verify.NoopVerifier
+import sk.ainet.samples.emoji.verify.Verifier
+
+/** Built without `-PlayaVerifier=true`: the second stage is off. */
+fun createVerifier(context: Context): Verifier = NoopVerifier("build with -PlayaVerifier=true")
diff --git a/SemanticEmojiFilter/composeApp/src/androidVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt b/SemanticEmojiFilter/composeApp/src/androidVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt
new file mode 100644
index 0000000..0efa5bd
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/androidVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt
@@ -0,0 +1,22 @@
+package sk.ainet.samples.emoji
+
+import android.app.ActivityManager
+import android.content.Context
+import sk.ainet.samples.emoji.verify.NoopVerifier
+import sk.ainet.samples.emoji.verify.Verifier
+import sk.ainet.samples.emoji.verifier.LayaVerifier
+import sk.ainet.samples.emoji.verifier.layaAndroidSource
+
+/**
+ * Built with `-PlayaVerifier=true`. The checkpoint is 644 MB of F16 weights and about 1.3 GB
+ * once up-cast to FP32 on the ART heap, so the verifier only switches on when the device grants
+ * a large heap that can hold it; otherwise the UI says why it is off.
+ */
+fun createVerifier(context: Context): Verifier {
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ val largeHeapMb = am.largeMemoryClass
+ if (am.isLowRamDevice || largeHeapMb < 2048) {
+ return NoopVerifier("needs a ${2048} MB heap, device grants $largeHeapMb MB")
+ }
+ return LayaVerifier(layaAndroidSource(context))
+}
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/calibration-queries.json b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/calibration-queries.json
new file mode 100644
index 0000000..f6f10c8
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/calibration-queries.json
@@ -0,0 +1,293 @@
+[
+ {
+ "query": "healthy things to eat",
+ "matches": [
+ "1F957",
+ "1F34E",
+ "1F966",
+ "1F955",
+ "1F34C",
+ "1F347",
+ "1F353",
+ "1F951",
+ "1F345",
+ "1F952",
+ "1F96C",
+ "1FAD0",
+ "1F95D",
+ "1F352"
+ ]
+ },
+ {
+ "query": "fruit",
+ "matches": [
+ "1F34E",
+ "1F34C",
+ "1F347",
+ "1F353",
+ "1F34A",
+ "1F34B",
+ "1F349",
+ "1FAD0",
+ "1F95D",
+ "1F352",
+ "1F351",
+ "1F350"
+ ]
+ },
+ {
+ "query": "junk food",
+ "matches": [
+ "1F355",
+ "1F354",
+ "1F35F",
+ "1F32D",
+ "1F369",
+ "1F37F",
+ "1F953",
+ "1F32E"
+ ]
+ },
+ {
+ "query": "something sweet",
+ "matches": [
+ "1F369",
+ "1F36A",
+ "1F382",
+ "1F370",
+ "1F9C1",
+ "1F36B",
+ "1F36C",
+ "1F36D",
+ "1F366"
+ ]
+ },
+ {
+ "query": "birthday cake",
+ "matches": [
+ "1F382",
+ "1F370",
+ "1F9C1",
+ "1F381",
+ "1F388"
+ ]
+ },
+ {
+ "query": "breakfast",
+ "matches": [
+ "1F95E",
+ "1F950",
+ "2615",
+ "1F95B",
+ "1F34C"
+ ]
+ },
+ {
+ "query": "feeling sick",
+ "matches": [
+ "1F912",
+ "1F927",
+ "1F92E",
+ "1F922",
+ "1F637",
+ "1F915",
+ "1F48A"
+ ]
+ },
+ {
+ "query": "hospital",
+ "matches": [
+ "1F3E5",
+ "1F691",
+ "1FA7A",
+ "1F489",
+ "1F48A",
+ "1F915"
+ ]
+ },
+ {
+ "query": "rock band",
+ "matches": [
+ "1F3B8",
+ "1F941",
+ "1F3A4",
+ "1F3B9",
+ "1F918",
+ "1F3B6"
+ ]
+ },
+ {
+ "query": "classical music",
+ "matches": [
+ "1F3BB",
+ "1F3B9",
+ "1F3BA",
+ "1F3B7",
+ "1F3BC",
+ "1F3B5"
+ ]
+ },
+ {
+ "query": "cute pets",
+ "matches": [
+ "1F436",
+ "1F431",
+ "1F439",
+ "1F430",
+ "1F42D"
+ ]
+ },
+ {
+ "query": "sea creatures",
+ "matches": [
+ "1F42C",
+ "1F433",
+ "1F988",
+ "1F419",
+ "1F422"
+ ]
+ },
+ {
+ "query": "rainy weather",
+ "matches": [
+ "1F327",
+ "26C8",
+ "2601",
+ "1F4A7"
+ ]
+ },
+ {
+ "query": "hot summer day",
+ "matches": [
+ "2600",
+ "1F525",
+ "1F3D6",
+ "1F349",
+ "1F366"
+ ]
+ },
+ {
+ "query": "winter sports",
+ "matches": [
+ "26F7",
+ "2744",
+ "1F3D4"
+ ]
+ },
+ {
+ "query": "team sports",
+ "matches": [
+ "26BD",
+ "1F3C0",
+ "1F3C8",
+ "26BE"
+ ]
+ },
+ {
+ "query": "going to the gym",
+ "matches": [
+ "1F3CB",
+ "1F3C3",
+ "1F94A",
+ "1F9D8"
+ ]
+ },
+ {
+ "query": "going on holiday",
+ "matches": [
+ "2708",
+ "1F3D6",
+ "1F9F3",
+ "1F3E8",
+ "26F5",
+ "1F6F3"
+ ]
+ },
+ {
+ "query": "camping in the mountains",
+ "matches": [
+ "26FA",
+ "1F3D4",
+ "1F332",
+ "1F525"
+ ]
+ },
+ {
+ "query": "very happy",
+ "matches": [
+ "1F600",
+ "1F60D",
+ "1F973",
+ "1F970",
+ "1F607"
+ ]
+ },
+ {
+ "query": "sad and crying",
+ "matches": [
+ "1F622",
+ "1F62D",
+ "1F494"
+ ]
+ },
+ {
+ "query": "angry",
+ "matches": [
+ "1F621",
+ "1F624"
+ ]
+ },
+ {
+ "query": "tired, need sleep",
+ "matches": [
+ "1F634",
+ "1F6CC",
+ "1F6CF",
+ "1F62C"
+ ]
+ },
+ {
+ "query": "working late at the office",
+ "matches": [
+ "1F4BB",
+ "23F0",
+ "2615",
+ "1F4C5"
+ ]
+ },
+ {
+ "query": "cleaning the house",
+ "matches": [
+ "1F9F9",
+ "1F3E0",
+ "1F5D1"
+ ]
+ },
+ {
+ "query": "video games",
+ "matches": [
+ "1F3AE",
+ "1F3B2",
+ "1F9E9",
+ "1F4BB"
+ ]
+ },
+ {
+ "query": "money and shopping",
+ "matches": [
+ "1F4B0",
+ "1F6D2",
+ "1F381"
+ ]
+ },
+ {
+ "query": "beach holiday",
+ "matches": [
+ "1F3D6",
+ "26F5",
+ "1F6F3",
+ "1F30A",
+ "2708"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/calibration.json b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/calibration.json
new file mode 100644
index 0000000..de57099
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/calibration.json
@@ -0,0 +1,8 @@
+{
+ "k": 22.117331604372698,
+ "x0": 0.2785088608201167,
+ "threshold": 0.8,
+ "queries": 28,
+ "accuracy": 0.9790521978021978,
+ "f1": 0.5960264900662252
+}
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/catalog-embeddings.bin b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/catalog-embeddings.bin
new file mode 100644
index 0000000..4c9ad04
Binary files /dev/null and b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/catalog-embeddings.bin differ
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/catalog.json b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/catalog.json
new file mode 100644
index 0000000..70676b2
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/catalog.json
@@ -0,0 +1,5517 @@
+{
+ "version": 1,
+ "cldr": "47",
+ "embeddingTextVersion": 1,
+ "embeddingsSha256": "21d90c9fcb6b1895dc9b2b4fa6eaa87f228dd0a99182ebc38ab7852c2dba94a3",
+ "items": [
+ {
+ "id": "1F957",
+ "emoji": "๐ฅ",
+ "nameEn": "green salad",
+ "nameDe": "Salat",
+ "keywordsEn": [
+ "food",
+ "green",
+ "salad"
+ ],
+ "keywordsDe": [
+ "Blattgemรผse",
+ "Essen",
+ "gesund",
+ "grรผn",
+ "Salat"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F34E",
+ "emoji": "๐",
+ "nameEn": "red apple",
+ "nameDe": "roter Apfel",
+ "keywordsEn": [
+ "apple",
+ "diet",
+ "food",
+ "fruit",
+ "health",
+ "red",
+ "ripe"
+ ],
+ "keywordsDe": [
+ "Apfel",
+ "Frucht",
+ "Obst",
+ "rot",
+ "roter Apfel"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F966",
+ "emoji": "๐ฅฆ",
+ "nameEn": "broccoli",
+ "nameDe": "Brokkoli",
+ "keywordsEn": [
+ "broccoli",
+ "cabbage",
+ "wild"
+ ],
+ "keywordsDe": [
+ "Broccoli",
+ "Brokkoli",
+ "essen",
+ "Gemรผse",
+ "Gemรผsekohl",
+ "Kohl"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F955",
+ "emoji": "๐ฅ",
+ "nameEn": "carrot",
+ "nameDe": "Karotte",
+ "keywordsEn": [
+ "carrot",
+ "food",
+ "vegetable"
+ ],
+ "keywordsDe": [
+ "Gemรผse",
+ "Karotte",
+ "Mรถhre",
+ "Mohrrรผbe"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F34C",
+ "emoji": "๐",
+ "nameEn": "banana",
+ "nameDe": "Banane",
+ "keywordsEn": [
+ "banana",
+ "fruit",
+ "potassium"
+ ],
+ "keywordsDe": [
+ "Banane",
+ "Frucht",
+ "Obst",
+ "Schale"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F347",
+ "emoji": "๐",
+ "nameEn": "grapes",
+ "nameDe": "Trauben",
+ "keywordsEn": [
+ "Dionysus",
+ "fruit",
+ "grape",
+ "grapes"
+ ],
+ "keywordsDe": [
+ "Frucht",
+ "Obst",
+ "Traube",
+ "Trauben",
+ "Wein"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F353",
+ "emoji": "๐",
+ "nameEn": "strawberry",
+ "nameDe": "Erdbeere",
+ "keywordsEn": [
+ "berry",
+ "fruit",
+ "strawberry"
+ ],
+ "keywordsDe": [
+ "Beere",
+ "Erdbeere",
+ "Essen",
+ "Frucht",
+ "Obst"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F951",
+ "emoji": "๐ฅ",
+ "nameEn": "avocado",
+ "nameDe": "Avocado",
+ "keywordsEn": [
+ "avocado",
+ "food",
+ "fruit"
+ ],
+ "keywordsDe": [
+ "Avocado",
+ "Essen",
+ "Frucht"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F345",
+ "emoji": "๐
",
+ "nameEn": "tomato",
+ "nameDe": "Tomate",
+ "keywordsEn": [
+ "food",
+ "fruit",
+ "tomato",
+ "vegetable"
+ ],
+ "keywordsDe": [
+ "Essen",
+ "Gemรผse",
+ "Obst",
+ "Tomate"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F952",
+ "emoji": "๐ฅ",
+ "nameEn": "cucumber",
+ "nameDe": "Gurke",
+ "keywordsEn": [
+ "cucumber",
+ "food",
+ "pickle",
+ "vegetable"
+ ],
+ "keywordsDe": [
+ "Essen",
+ "Gemรผse",
+ "Gurke",
+ "Scheiben"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F34A",
+ "emoji": "๐",
+ "nameEn": "tangerine",
+ "nameDe": "Mandarine",
+ "keywordsEn": [
+ "c",
+ "citrus",
+ "fruit",
+ "nectarine",
+ "orange",
+ "tangerine",
+ "vitamin"
+ ],
+ "keywordsDe": [
+ "Apfelsine",
+ "Frucht",
+ "Mandarine",
+ "Obst",
+ "Orange",
+ "Vitamin"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F34B",
+ "emoji": "๐",
+ "nameEn": "lemon",
+ "nameDe": "Zitrone",
+ "keywordsEn": [
+ "citrus",
+ "fruit",
+ "lemon",
+ "sour"
+ ],
+ "keywordsDe": [
+ "Frucht",
+ "Obst",
+ "sauer",
+ "Zitrone",
+ "Zitrusfrucht"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F349",
+ "emoji": "๐",
+ "nameEn": "watermelon",
+ "nameDe": "Wassermelone",
+ "keywordsEn": [
+ "fruit",
+ "watermelon"
+ ],
+ "keywordsDe": [
+ "Frucht",
+ "Melone",
+ "Obst",
+ "Wasser",
+ "Wassermelone"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F96C",
+ "emoji": "๐ฅฌ",
+ "nameEn": "leafy green",
+ "nameDe": "Blattgemรผse",
+ "keywordsEn": [
+ "bok",
+ "burgers",
+ "cabbage",
+ "choy",
+ "green",
+ "kale",
+ "leafy",
+ "lettuce",
+ "salad"
+ ],
+ "keywordsDe": [
+ "Blattgemรผse",
+ "blattgrรผn",
+ "Essen",
+ "Gemรผse",
+ "Grรผnkohl",
+ "Grรผnzeug",
+ "Kohl",
+ "Kraut",
+ "Mangold",
+ "Pak Choi",
+ "Pok Choi",
+ "Salat",
+ "Salatblatt",
+ "Spinat"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F360",
+ "emoji": "๐ ",
+ "nameEn": "roasted sweet potato",
+ "nameDe": "gerรถstete Sรผรkartoffel",
+ "keywordsEn": [
+ "food",
+ "potato",
+ "roasted",
+ "sweet"
+ ],
+ "keywordsDe": [
+ "Essen",
+ "Gemรผse",
+ "gerรถstet",
+ "Sรผรkartoffel"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F95C",
+ "emoji": "๐ฅ",
+ "nameEn": "peanuts",
+ "nameDe": "Erdnuss",
+ "keywordsEn": [
+ "food",
+ "nut",
+ "peanut",
+ "peanuts",
+ "vegetable"
+ ],
+ "keywordsDe": [
+ "Erdnuss",
+ "Essen"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F95B",
+ "emoji": "๐ฅ",
+ "nameEn": "glass of milk",
+ "nameDe": "Glas Milch",
+ "keywordsEn": [
+ "drink",
+ "glass",
+ "milk"
+ ],
+ "keywordsDe": [
+ "Getrรคnk",
+ "Glas",
+ "Milch",
+ "trinken"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1FAD0",
+ "emoji": "๐ซ",
+ "nameEn": "blueberries",
+ "nameDe": "Blaubeeren",
+ "keywordsEn": [
+ "berries",
+ "berry",
+ "bilberry",
+ "blue",
+ "blueberries",
+ "blueberry",
+ "food",
+ "fruit"
+ ],
+ "keywordsDe": [
+ "Beere",
+ "blau",
+ "Blaubeere",
+ "Blaubeeren",
+ "Essen",
+ "Frucht",
+ "Heidelbeere",
+ "Heidelbeeren",
+ "Lebensmittel",
+ "Nahrung",
+ "Obst"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F95D",
+ "emoji": "๐ฅ",
+ "nameEn": "kiwi fruit",
+ "nameDe": "Kiwi",
+ "keywordsEn": [
+ "food",
+ "fruit",
+ "kiwi"
+ ],
+ "keywordsDe": [
+ "Frucht",
+ "Kiwi",
+ "Obst"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F352",
+ "emoji": "๐",
+ "nameEn": "cherries",
+ "nameDe": "Kirschen",
+ "keywordsEn": [
+ "berries",
+ "cherries",
+ "cherry",
+ "fruit",
+ "red"
+ ],
+ "keywordsDe": [
+ "Frucht",
+ "Kirsche",
+ "Kirschen",
+ "Obst"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F351",
+ "emoji": "๐",
+ "nameEn": "peach",
+ "nameDe": "Pfirsich",
+ "keywordsEn": [
+ "fruit",
+ "peach"
+ ],
+ "keywordsDe": [
+ "Frucht",
+ "Obst",
+ "Pfirsich"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F350",
+ "emoji": "๐",
+ "nameEn": "pear",
+ "nameDe": "Birne",
+ "keywordsEn": [
+ "fruit",
+ "pear"
+ ],
+ "keywordsDe": [
+ "Birne",
+ "Frucht",
+ "Obst"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F33D",
+ "emoji": "๐ฝ",
+ "nameEn": "ear of corn",
+ "nameDe": "Maiskolben",
+ "keywordsEn": [
+ "corn",
+ "crops",
+ "ear",
+ "farm",
+ "maize",
+ "maze"
+ ],
+ "keywordsDe": [
+ "Gemรผse",
+ "Mais",
+ "Maiskolben"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F954",
+ "emoji": "๐ฅ",
+ "nameEn": "potato",
+ "nameDe": "Kartoffel",
+ "keywordsEn": [
+ "food",
+ "potato",
+ "vegetable"
+ ],
+ "keywordsDe": [
+ "Essen",
+ "Gemรผse",
+ "Kartoffel"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F363",
+ "emoji": "๐ฃ",
+ "nameEn": "sushi",
+ "nameDe": "Sushi",
+ "keywordsEn": [
+ "food",
+ "sushi"
+ ],
+ "keywordsDe": [
+ "japanisches Gericht",
+ "Restaurant",
+ "Sushi"
+ ],
+ "tags": [
+ "food",
+ "healthy"
+ ],
+ "group": "food-healthy"
+ },
+ {
+ "id": "1F355",
+ "emoji": "๐",
+ "nameEn": "pizza",
+ "nameDe": "Pizza",
+ "keywordsEn": [
+ "cheese",
+ "food",
+ "hungry",
+ "pepperoni",
+ "pizza",
+ "slice"
+ ],
+ "keywordsDe": [
+ "Pizza",
+ "Pizzastรผck",
+ "Pizzeria"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F354",
+ "emoji": "๐",
+ "nameEn": "hamburger",
+ "nameDe": "Hamburger",
+ "keywordsEn": [
+ "burger",
+ "eat",
+ "fast",
+ "food",
+ "hamburger",
+ "hungry"
+ ],
+ "keywordsDe": [
+ "Burger",
+ "Fast Food",
+ "Hamburger",
+ "Restaurant"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F35F",
+ "emoji": "๐",
+ "nameEn": "french fries",
+ "nameDe": "Pommes frites",
+ "keywordsEn": [
+ "fast",
+ "food",
+ "french",
+ "fries"
+ ],
+ "keywordsDe": [
+ "Fritten",
+ "Pommes",
+ "Pommes frites",
+ "Pommes Frites"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F32D",
+ "emoji": "๐ญ",
+ "nameEn": "hot dog",
+ "nameDe": "Hotdog",
+ "keywordsEn": [
+ "dog",
+ "frankfurter",
+ "hot",
+ "hotdog",
+ "sausage"
+ ],
+ "keywordsDe": [
+ "Essen",
+ "Frankfurter",
+ "Hot Dog",
+ "Hotdog",
+ "Wurst",
+ "Wรผrstchen"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F369",
+ "emoji": "๐ฉ",
+ "nameEn": "doughnut",
+ "nameDe": "Donut",
+ "keywordsEn": [
+ "breakfast",
+ "dessert",
+ "donut",
+ "doughnut",
+ "food",
+ "sweet"
+ ],
+ "keywordsDe": [
+ "Donut",
+ "Doughnut",
+ "Essen",
+ "Kuchen",
+ "sรผร"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F36A",
+ "emoji": "๐ช",
+ "nameEn": "cookie",
+ "nameDe": "Keks",
+ "keywordsEn": [
+ "chip",
+ "chocolate",
+ "cookie",
+ "dessert",
+ "sweet"
+ ],
+ "keywordsDe": [
+ "Cookie",
+ "Keks",
+ "Plรคtchen"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F382",
+ "emoji": "๐",
+ "nameEn": "birthday cake",
+ "nameDe": "Geburtstagskuchen",
+ "keywordsEn": [
+ "bday",
+ "birthday",
+ "cake",
+ "celebration",
+ "dessert",
+ "happy",
+ "pastry",
+ "sweet"
+ ],
+ "keywordsDe": [
+ "Essen",
+ "feier",
+ "Feier",
+ "Geburtstag",
+ "Geburtstagskuchen",
+ "Geburtstagstorte",
+ "happy birthday",
+ "kerzen",
+ "Kuchen",
+ "Lebensmittel",
+ "Torte"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F370",
+ "emoji": "๐ฐ",
+ "nameEn": "shortcake",
+ "nameDe": "Torte",
+ "keywordsEn": [
+ "cake",
+ "dessert",
+ "pastry",
+ "shortcake",
+ "slice",
+ "sweet"
+ ],
+ "keywordsDe": [
+ "Kuchen",
+ "Kuchenstรผck",
+ "Stรผck Torte",
+ "Torte",
+ "Tortenstรผck"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F9C1",
+ "emoji": "๐ง",
+ "nameEn": "cupcake",
+ "nameDe": "Cupcake",
+ "keywordsEn": [
+ "bakery",
+ "cupcake",
+ "dessert",
+ "sprinkles",
+ "sugar",
+ "sweet",
+ "treat"
+ ],
+ "keywordsDe": [
+ "Bรคckerei",
+ "Cupcake",
+ "Dessert",
+ "Essen",
+ "Gebรคck",
+ "Kalorienbombe",
+ "Konditorei",
+ "Muffin",
+ "Nachspeise",
+ "Streusel",
+ "sรผร",
+ "Zucker"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F36B",
+ "emoji": "๐ซ",
+ "nameEn": "chocolate bar",
+ "nameDe": "Schokoladentafel",
+ "keywordsEn": [
+ "bar",
+ "candy",
+ "chocolate",
+ "dessert",
+ "halloween",
+ "sweet",
+ "tooth"
+ ],
+ "keywordsDe": [
+ "Essen",
+ "Schokolade",
+ "Schokoladentafel",
+ "Sรผรigkeit"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F36C",
+ "emoji": "๐ฌ",
+ "nameEn": "candy",
+ "nameDe": "Bonbon",
+ "keywordsEn": [
+ "candy",
+ "cavities",
+ "dessert",
+ "halloween",
+ "restaurant",
+ "sweet",
+ "tooth",
+ "wrapper"
+ ],
+ "keywordsDe": [
+ "Bonbon",
+ "sรผร",
+ "Sรผรigkeit"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F36D",
+ "emoji": "๐ญ",
+ "nameEn": "lollipop",
+ "nameDe": "Lutscher",
+ "keywordsEn": [
+ "candy",
+ "dessert",
+ "food",
+ "lollipop",
+ "restaurant",
+ "sweet"
+ ],
+ "keywordsDe": [
+ "Lolli",
+ "Lutscher",
+ "Sรผรigkeit"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F366",
+ "emoji": "๐ฆ",
+ "nameEn": "soft ice cream",
+ "nameDe": "Softeis",
+ "keywordsEn": [
+ "cream",
+ "dessert",
+ "food",
+ "ice",
+ "icecream",
+ "restaurant",
+ "serve",
+ "soft",
+ "sweet"
+ ],
+ "keywordsDe": [
+ "Eis",
+ "Softeis"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F37F",
+ "emoji": "๐ฟ",
+ "nameEn": "popcorn",
+ "nameDe": "Popcorn",
+ "keywordsEn": [
+ "corn",
+ "movie",
+ "pop",
+ "popcorn"
+ ],
+ "keywordsDe": [
+ "Kino",
+ "Popcorn",
+ "Snack"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F953",
+ "emoji": "๐ฅ",
+ "nameEn": "bacon",
+ "nameDe": "Bacon",
+ "keywordsEn": [
+ "bacon",
+ "breakfast",
+ "food",
+ "meat"
+ ],
+ "keywordsDe": [
+ "Bacon",
+ "Essen",
+ "Frรผhstรผcksspeck",
+ "Speck",
+ "Speckstreifen"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F9C0",
+ "emoji": "๐ง",
+ "nameEn": "cheese wedge",
+ "nameDe": "Kรคsestรผck",
+ "keywordsEn": [
+ "cheese",
+ "wedge"
+ ],
+ "keywordsDe": [
+ "Kรคse",
+ "Kรคsestรผck"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F32E",
+ "emoji": "๐ฎ",
+ "nameEn": "taco",
+ "nameDe": "Taco",
+ "keywordsEn": [
+ "mexican",
+ "taco"
+ ],
+ "keywordsDe": [
+ "Essen",
+ "mexikanisch",
+ "Taco"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F950",
+ "emoji": "๐ฅ",
+ "nameEn": "croissant",
+ "nameDe": "Croissant",
+ "keywordsEn": [
+ "bread",
+ "breakfast",
+ "crescent",
+ "croissant",
+ "food",
+ "french",
+ "roll"
+ ],
+ "keywordsDe": [
+ "Corissant",
+ "Croissant",
+ "franzรถsisch",
+ "Frรผhstรผck",
+ "Frรผhstรผckshรถrnchen"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F37A",
+ "emoji": "๐บ",
+ "nameEn": "beer mug",
+ "nameDe": "Bierkrug",
+ "keywordsEn": [
+ "alcohol",
+ "ale",
+ "bar",
+ "beer",
+ "booze",
+ "drink",
+ "drinking",
+ "drinks",
+ "mug",
+ "octoberfest",
+ "oktoberfest",
+ "pint",
+ "stein",
+ "summer"
+ ],
+ "keywordsDe": [
+ "Bar",
+ "Bier",
+ "Bierkrug",
+ "Krug",
+ "Oktoberfest",
+ "prost",
+ "Weizenbier"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F377",
+ "emoji": "๐ท",
+ "nameEn": "wine glass",
+ "nameDe": "Weinglas",
+ "keywordsEn": [
+ "alcohol",
+ "bar",
+ "beverage",
+ "booze",
+ "club",
+ "drink",
+ "drinking",
+ "drinks",
+ "glass",
+ "restaurant",
+ "wine"
+ ],
+ "keywordsDe": [
+ "Alkohol",
+ "Bar",
+ "Glas",
+ "Wein",
+ "Weinglas"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "2615",
+ "emoji": "โ",
+ "nameEn": "hot beverage",
+ "nameDe": "Heiรgetrรคnk",
+ "keywordsEn": [
+ "beverage",
+ "cafe",
+ "caffeine",
+ "chai",
+ "coffee",
+ "drink",
+ "hot",
+ "morning",
+ "steaming",
+ "tea"
+ ],
+ "keywordsDe": [
+ "Cafe",
+ "dampfend",
+ "Getrรคnk",
+ "heiร",
+ "Heiรgetrรคnk",
+ "Kaffee",
+ "Tasse",
+ "Tee",
+ "trinken"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F35D",
+ "emoji": "๐",
+ "nameEn": "spaghetti",
+ "nameDe": "Spaghetti",
+ "keywordsEn": [
+ "food",
+ "meatballs",
+ "pasta",
+ "restaurant",
+ "spaghetti"
+ ],
+ "keywordsDe": [
+ "Essse",
+ "Nudeln mit Tomatensoรe",
+ "Pasta",
+ "Restaurant",
+ "Spaghetti"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F35C",
+ "emoji": "๐",
+ "nameEn": "steaming bowl",
+ "nameDe": "Schรผssel und Essstรคbchen",
+ "keywordsEn": [
+ "bowl",
+ "chopsticks",
+ "food",
+ "noodle",
+ "pho",
+ "ramen",
+ "soup",
+ "steaming"
+ ],
+ "keywordsDe": [
+ "chinesisches Essen",
+ "dampfend",
+ "Eiernudeln",
+ "Nudeln",
+ "Schรผssel",
+ "Schรผssel und Essstรคbchen",
+ "Stรคbchen",
+ "Suppe"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F95E",
+ "emoji": "๐ฅ",
+ "nameEn": "pancakes",
+ "nameDe": "Pfannkuchen",
+ "keywordsEn": [
+ "breakfast",
+ "crรชpe",
+ "food",
+ "hotcake",
+ "pancake",
+ "pancakes"
+ ],
+ "keywordsDe": [
+ "Crepe",
+ "Eierpfannkuchen",
+ "Essen",
+ "Pfannkuchen"
+ ],
+ "tags": [
+ "food",
+ "indulgent"
+ ],
+ "group": "food-indulgent"
+ },
+ {
+ "id": "1F912",
+ "emoji": "๐ค",
+ "nameEn": "face with thermometer",
+ "nameDe": "Gesicht mit Fieberthermometer",
+ "keywordsEn": [
+ "face",
+ "ill",
+ "sick",
+ "thermometer"
+ ],
+ "keywordsDe": [
+ "Fieber",
+ "Gesicht",
+ "krank",
+ "messen",
+ "Smiley",
+ "Thermometer"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F927",
+ "emoji": "๐คง",
+ "nameEn": "sneezing face",
+ "nameDe": "niesendes Gesicht",
+ "keywordsEn": [
+ "face",
+ "fever",
+ "flu",
+ "gesundheit",
+ "sick",
+ "sneeze",
+ "sneezing"
+ ],
+ "keywordsDe": [
+ "erkรคltet",
+ "Erkรคltung",
+ "Gesicht",
+ "Gesundheit",
+ "Grippe",
+ "hatschi",
+ "krank",
+ "Nase",
+ "niesen",
+ "putzen",
+ "Smiley",
+ "Taschentuch"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F92E",
+ "emoji": "๐คฎ",
+ "nameEn": "face vomiting",
+ "nameDe": "kotzendes Gesicht",
+ "keywordsEn": [
+ "barf",
+ "ew",
+ "face",
+ "gross",
+ "puke",
+ "sick",
+ "spew",
+ "throw",
+ "up",
+ "vomit",
+ "vomiting"
+ ],
+ "keywordsDe": [
+ "brechen",
+ "erbrechen",
+ "Gesicht",
+ "kotzen",
+ "krank",
+ "schlecht",
+ "Smiley",
+ "speien",
+ "spucken",
+ "รผbel",
+ "รผbergeben"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F922",
+ "emoji": "๐คข",
+ "nameEn": "nauseated face",
+ "nameDe": "wรผrgendes Gesicht",
+ "keywordsEn": [
+ "face",
+ "gross",
+ "nasty",
+ "nauseated",
+ "sick",
+ "vomit"
+ ],
+ "keywordsDe": [
+ "ekel",
+ "eklig",
+ "erbrechen",
+ "Gesicht",
+ "grรผn",
+ "igitt",
+ "Smiley",
+ "รbelkeit",
+ "รผbergeben",
+ "widerlich",
+ "wรผrgen"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F637",
+ "emoji": "๐ท",
+ "nameEn": "face with medical mask",
+ "nameDe": "Gesicht mit Atemschutzmaske",
+ "keywordsEn": [
+ "cold",
+ "dentist",
+ "dermatologist",
+ "doctor",
+ "dr",
+ "face",
+ "germs",
+ "mask",
+ "medical",
+ "medicine",
+ "sick"
+ ],
+ "keywordsDe": [
+ "Arzt",
+ "Atemschutzmaske",
+ "Bazillen",
+ "erkรคltet",
+ "Gesicht",
+ "krank",
+ "Krankheit",
+ "Mund",
+ "Mundschutz",
+ "Schutzmaske",
+ "Smiley",
+ "Viren",
+ "Virus"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F915",
+ "emoji": "๐ค",
+ "nameEn": "face with head-bandage",
+ "nameDe": "Gesicht mit Kopfverband",
+ "keywordsEn": [
+ "bandage",
+ "face",
+ "head-bandage",
+ "hurt",
+ "injury",
+ "ouch"
+ ],
+ "keywordsDe": [
+ "aua",
+ "autsch",
+ "Gesicht",
+ "Kopf",
+ "Schmerzen",
+ "Smiley",
+ "Verband",
+ "Verletzung"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F48A",
+ "emoji": "๐",
+ "nameEn": "pill",
+ "nameDe": "Kapsel",
+ "keywordsEn": [
+ "doctor",
+ "drugs",
+ "medicated",
+ "medicine",
+ "pill",
+ "pills",
+ "sick",
+ "vitamin"
+ ],
+ "keywordsDe": [
+ "Arznei",
+ "Arzt",
+ "Kapsel",
+ "Medizin",
+ "Pille",
+ "Tabletten"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1FA7A",
+ "emoji": "๐ฉบ",
+ "nameEn": "stethoscope",
+ "nameDe": "Stethoskop",
+ "keywordsEn": [
+ "doctor",
+ "heart",
+ "medicine",
+ "stethoscope"
+ ],
+ "keywordsDe": [
+ "abhรถren",
+ "Arzt",
+ "Doktor",
+ "Herz",
+ "Herzschlag",
+ "Medizin",
+ "Stethoskop",
+ "Untersuchung"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F321",
+ "emoji": "๐ก๏ธ",
+ "nameEn": "thermometer",
+ "nameDe": "Thermometer",
+ "keywordsEn": [
+ "thermometer",
+ "weather"
+ ],
+ "keywordsDe": [
+ "Temperatur",
+ "Thermometer",
+ "Wetter"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F3E5",
+ "emoji": "๐ฅ",
+ "nameEn": "hospital",
+ "nameDe": "Krankenhaus",
+ "keywordsEn": [
+ "building",
+ "doctor",
+ "hospital",
+ "medicine"
+ ],
+ "keywordsDe": [
+ "Ambulanz",
+ "Arzt",
+ "Gebรคude",
+ "krank",
+ "Krankenhaus",
+ "Medizin",
+ "Notaufnahme"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F489",
+ "emoji": "๐",
+ "nameEn": "syringe",
+ "nameDe": "Spritze",
+ "keywordsEn": [
+ "doctor",
+ "flu",
+ "medicine",
+ "needle",
+ "shot",
+ "sick",
+ "syringe",
+ "tool",
+ "vaccination"
+ ],
+ "keywordsDe": [
+ "Arzt",
+ "impfen",
+ "Impfung",
+ "Injektion",
+ "Kanรผle",
+ "Nadel",
+ "Spritze"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1FA79",
+ "emoji": "๐ฉน",
+ "nameEn": "adhesive bandage",
+ "nameDe": "Heftpflaster",
+ "keywordsEn": [
+ "adhesive",
+ "bandage"
+ ],
+ "keywordsDe": [
+ "Heftpflaster",
+ "Pflaster",
+ "verletzt",
+ "Wunde"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F6CC",
+ "emoji": "๐",
+ "nameEn": "person in bed",
+ "nameDe": "im Bett liegende Person",
+ "keywordsEn": [
+ "bed",
+ "bedtime",
+ "good",
+ "goodnight",
+ "hotel",
+ "nap",
+ "night",
+ "person",
+ "sleep",
+ "tired",
+ "zzz"
+ ],
+ "keywordsDe": [
+ "Bett",
+ "gute Nacht",
+ "Heia",
+ "im Bett liegende Person",
+ "Im Bett liegende Person",
+ "mรผde",
+ "Nachtruhe",
+ "schlaf schรถn",
+ "schlafen",
+ "zzz"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F691",
+ "emoji": "๐",
+ "nameEn": "ambulance",
+ "nameDe": "Krankenwagen",
+ "keywordsEn": [
+ "ambulance",
+ "emergency",
+ "vehicle"
+ ],
+ "keywordsDe": [
+ "Ambulanz",
+ "Krankenwagen",
+ "Notfall"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F9A0",
+ "emoji": "๐ฆ ",
+ "nameEn": "microbe",
+ "nameDe": "Mikrobe",
+ "keywordsEn": [
+ "amoeba",
+ "bacteria",
+ "microbe",
+ "science",
+ "virus"
+ ],
+ "keywordsDe": [
+ "Amรถbe",
+ "Amรถben",
+ "Bakterie",
+ "Bakterien",
+ "Einzeller",
+ "Medizin",
+ "Mikrobe",
+ "Virus",
+ "Wissenschaft"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F9EC",
+ "emoji": "๐งฌ",
+ "nameEn": "dna",
+ "nameDe": "DNA",
+ "keywordsEn": [
+ "biologist",
+ "dna",
+ "evolution",
+ "gene",
+ "genetics",
+ "life"
+ ],
+ "keywordsDe": [
+ "Biologe",
+ "Biologie",
+ "Biologin",
+ "DNA",
+ "DNS",
+ "Evolution",
+ "Gen",
+ "Genetik",
+ "Genmanipulation",
+ "Leben"
+ ],
+ "tags": [
+ "health",
+ "sick"
+ ],
+ "group": "health-sick"
+ },
+ {
+ "id": "1F3B8",
+ "emoji": "๐ธ",
+ "nameEn": "guitar",
+ "nameDe": "Gitarre",
+ "keywordsEn": [
+ "guitar",
+ "instrument",
+ "music",
+ "strat"
+ ],
+ "keywordsDe": [
+ "Gitarre",
+ "Instrument",
+ "Musik",
+ "Musikinstrument"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F941",
+ "emoji": "๐ฅ",
+ "nameEn": "drum",
+ "nameDe": "Trommel",
+ "keywordsEn": [
+ "drum",
+ "drumsticks",
+ "music"
+ ],
+ "keywordsDe": [
+ "Drumsticks",
+ "Instrument",
+ "Musik",
+ "Schlรคgel",
+ "Sticks",
+ "Trommel",
+ "Trommelstรถcke"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3A4",
+ "emoji": "๐ค",
+ "nameEn": "microphone",
+ "nameDe": "Mikrofon",
+ "keywordsEn": [
+ "karaoke",
+ "mic",
+ "microphone",
+ "music",
+ "sing",
+ "sound"
+ ],
+ "keywordsDe": [
+ "Karaoke",
+ "Mikrofon",
+ "Singen",
+ "Unterhaltung"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3B9",
+ "emoji": "๐น",
+ "nameEn": "musical keyboard",
+ "nameDe": "Klaviatur",
+ "keywordsEn": [
+ "instrument",
+ "keyboard",
+ "music",
+ "musical",
+ "piano"
+ ],
+ "keywordsDe": [
+ "Instrument",
+ "Klaviatur",
+ "Klavier",
+ "Musik",
+ "Musikinstrument",
+ "Piano",
+ "Tastatur",
+ "Tasten"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3BA",
+ "emoji": "๐บ",
+ "nameEn": "trumpet",
+ "nameDe": "Trompete",
+ "keywordsEn": [
+ "instrument",
+ "music",
+ "trumpet"
+ ],
+ "keywordsDe": [
+ "Instrument",
+ "Musik",
+ "Musikinstrument",
+ "Trompete"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3B7",
+ "emoji": "๐ท",
+ "nameEn": "saxophone",
+ "nameDe": "Saxofon",
+ "keywordsEn": [
+ "instrument",
+ "music",
+ "sax",
+ "saxophone"
+ ],
+ "keywordsDe": [
+ "Instrument",
+ "Musik",
+ "Musikinstrument",
+ "Saxofon"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3BB",
+ "emoji": "๐ป",
+ "nameEn": "violin",
+ "nameDe": "Geige",
+ "keywordsEn": [
+ "instrument",
+ "music",
+ "violin"
+ ],
+ "keywordsDe": [
+ "Geige",
+ "Instrument",
+ "Musik",
+ "Musikinstrument",
+ "Violine"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F918",
+ "emoji": "๐ค",
+ "nameEn": "sign of the horns",
+ "nameDe": "Teufelsgruร",
+ "keywordsEn": [
+ "finger",
+ "hand",
+ "horns",
+ "rock-on",
+ "sign"
+ ],
+ "keywordsDe": [
+ "corna",
+ "Finger",
+ "Hand",
+ "Heavy",
+ "Hรถrner",
+ "Mano cornuta",
+ "Metal",
+ "Metalhand",
+ "Pommesgabel",
+ "Rock",
+ "Teufel",
+ "Teufelsgruร",
+ "yeah"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3B6",
+ "emoji": "๐ถ",
+ "nameEn": "musical notes",
+ "nameDe": "Musiknoten",
+ "keywordsEn": [
+ "music",
+ "musical",
+ "note",
+ "notes",
+ "sound"
+ ],
+ "keywordsDe": [
+ "Musik",
+ "Musiknoten",
+ "Noten"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3B5",
+ "emoji": "๐ต",
+ "nameEn": "musical note",
+ "nameDe": "Musiknote",
+ "keywordsEn": [
+ "music",
+ "musical",
+ "note",
+ "sound"
+ ],
+ "keywordsDe": [
+ "Lied",
+ "Melodie",
+ "Musik",
+ "Musiknote",
+ "Note"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3A7",
+ "emoji": "๐ง",
+ "nameEn": "headphone",
+ "nameDe": "Kopfhรถrer",
+ "keywordsEn": [
+ "earbud",
+ "headphone",
+ "sound"
+ ],
+ "keywordsDe": [
+ "Kopfhรถrer",
+ "Musik",
+ "Unterhaltung"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F50A",
+ "emoji": "๐",
+ "nameEn": "speaker high volume",
+ "nameDe": "Lautsprecher mit hoher Lautstรคrke",
+ "keywordsEn": [
+ "high",
+ "loud",
+ "music",
+ "sound",
+ "speaker",
+ "volume"
+ ],
+ "keywordsDe": [
+ "Dezibel",
+ "laut",
+ "Lautsprecher mit drei Schallwellen",
+ "Lautsprecher mit hoher Lautstรคrke",
+ "Musik",
+ "Schallwellen"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F3BC",
+ "emoji": "๐ผ",
+ "nameEn": "musical score",
+ "nameDe": "Notenschlรผssel",
+ "keywordsEn": [
+ "music",
+ "musical",
+ "note",
+ "score"
+ ],
+ "keywordsDe": [
+ "Musik",
+ "Noten",
+ "Notenschlรผssel",
+ "Partitur",
+ "Violinschlรผssel"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F4FB",
+ "emoji": "๐ป",
+ "nameEn": "radio",
+ "nameDe": "Radio",
+ "keywordsEn": [
+ "entertainment",
+ "radio",
+ "tbt",
+ "video"
+ ],
+ "keywordsDe": [
+ "Musik",
+ "Radio"
+ ],
+ "tags": [
+ "music",
+ "rock-band"
+ ],
+ "group": "music-rock"
+ },
+ {
+ "id": "1F436",
+ "emoji": "๐ถ",
+ "nameEn": "dog face",
+ "nameDe": "Hundegesicht",
+ "keywordsEn": [
+ "adorbs",
+ "animal",
+ "dog",
+ "face",
+ "pet",
+ "puppies",
+ "puppy"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Hund",
+ "Hundegesicht",
+ "Tier",
+ "treu",
+ "Wau",
+ "wuff"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F431",
+ "emoji": "๐ฑ",
+ "nameEn": "cat face",
+ "nameDe": "Katzengesicht",
+ "keywordsEn": [
+ "animal",
+ "cat",
+ "face",
+ "kitten",
+ "kitty",
+ "pet"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Katze",
+ "Katzengesicht",
+ "Miau",
+ "Miez",
+ "Miezekatze",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F42D",
+ "emoji": "๐ญ",
+ "nameEn": "mouse face",
+ "nameDe": "Mรคusegesicht",
+ "keywordsEn": [
+ "animal",
+ "face",
+ "mouse"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Maus",
+ "Mรคusegesicht",
+ "Mausgesicht",
+ "Nagetier",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F439",
+ "emoji": "๐น",
+ "nameEn": "hamster",
+ "nameDe": "Hamster",
+ "keywordsEn": [
+ "animal",
+ "face",
+ "hamster",
+ "pet"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Hamster",
+ "Hamstergesicht",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F430",
+ "emoji": "๐ฐ",
+ "nameEn": "rabbit face",
+ "nameDe": "Hasengesicht",
+ "keywordsEn": [
+ "animal",
+ "bunny",
+ "face",
+ "pet",
+ "rabbit"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Hase",
+ "Hasengesicht",
+ "Kaninchen",
+ "Karnickel",
+ "Langohr",
+ "Mรผmmelmann",
+ "Nager",
+ "Nagetier",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F98A",
+ "emoji": "๐ฆ",
+ "nameEn": "fox",
+ "nameDe": "Fuchs",
+ "keywordsEn": [
+ "animal",
+ "face",
+ "fox"
+ ],
+ "keywordsDe": [
+ "Fuchs",
+ "Fuchsgesicht",
+ "Gesicht",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F43B",
+ "emoji": "๐ป",
+ "nameEn": "bear",
+ "nameDe": "Bรคr",
+ "keywordsEn": [
+ "animal",
+ "bear",
+ "face",
+ "grizzly",
+ "growl",
+ "honey"
+ ],
+ "keywordsDe": [
+ "Bรคr",
+ "Bรคrengesicht",
+ "Gesicht",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F43C",
+ "emoji": "๐ผ",
+ "nameEn": "panda",
+ "nameDe": "Panda",
+ "keywordsEn": [
+ "animal",
+ "bamboo",
+ "face",
+ "panda"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Panda",
+ "Pandabรคr",
+ "Pandagesicht",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F428",
+ "emoji": "๐จ",
+ "nameEn": "koala",
+ "nameDe": "Koala",
+ "keywordsEn": [
+ "animal",
+ "australia",
+ "bear",
+ "down",
+ "face",
+ "koala",
+ "marsupial",
+ "under"
+ ],
+ "keywordsDe": [
+ "Koala",
+ "Koalabรคr",
+ "Koalagesicht",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F981",
+ "emoji": "๐ฆ",
+ "nameEn": "lion",
+ "nameDe": "Lรถwe",
+ "keywordsEn": [
+ "alpha",
+ "animal",
+ "face",
+ "Leo",
+ "lion",
+ "mane",
+ "order",
+ "rawr",
+ "roar",
+ "safari",
+ "strong",
+ "zodiac"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Lรถwe",
+ "Lรถwengesicht",
+ "Sternzeichen",
+ "Tier",
+ "Tierkreis"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F42E",
+ "emoji": "๐ฎ",
+ "nameEn": "cow face",
+ "nameDe": "Kuhgesicht",
+ "keywordsEn": [
+ "animal",
+ "cow",
+ "face",
+ "farm",
+ "milk",
+ "moo"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Kuh",
+ "Kuhgesicht",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F437",
+ "emoji": "๐ท",
+ "nameEn": "pig face",
+ "nameDe": "Schweinegesicht",
+ "keywordsEn": [
+ "animal",
+ "bacon",
+ "face",
+ "farm",
+ "pig",
+ "pork"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Schwein",
+ "Schweinegesicht",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F438",
+ "emoji": "๐ธ",
+ "nameEn": "frog",
+ "nameDe": "Frosch",
+ "keywordsEn": [
+ "animal",
+ "face",
+ "frog"
+ ],
+ "keywordsDe": [
+ "Frosch",
+ "Froschgesicht",
+ "Gesicht",
+ "grรผn",
+ "quak",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F435",
+ "emoji": "๐ต",
+ "nameEn": "monkey face",
+ "nameDe": "Affengesicht",
+ "keywordsEn": [
+ "animal",
+ "banana",
+ "face",
+ "monkey"
+ ],
+ "keywordsDe": [
+ "Affe",
+ "Affengesicht",
+ "Banane",
+ "Dschungel",
+ "Gesicht",
+ "Tier",
+ "Zoo"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F414",
+ "emoji": "๐",
+ "nameEn": "chicken",
+ "nameDe": "Huhn",
+ "keywordsEn": [
+ "animal",
+ "bird",
+ "chicken",
+ "ornithology"
+ ],
+ "keywordsDe": [
+ "Geflรผgel",
+ "Henne",
+ "Huhn",
+ "Tier",
+ "Tierkopf"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F427",
+ "emoji": "๐ง",
+ "nameEn": "penguin",
+ "nameDe": "Pinguin",
+ "keywordsEn": [
+ "animal",
+ "antarctica",
+ "bird",
+ "ornithology",
+ "penguin"
+ ],
+ "keywordsDe": [
+ "Pinguin",
+ "Pinguinkopf",
+ "Tier",
+ "Vogel"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F98B",
+ "emoji": "๐ฆ",
+ "nameEn": "butterfly",
+ "nameDe": "Schmetterling",
+ "keywordsEn": [
+ "butterfly",
+ "insect",
+ "pretty"
+ ],
+ "keywordsDe": [
+ "Insekt",
+ "Schmetterling",
+ "schรถn"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F41D",
+ "emoji": "๐",
+ "nameEn": "honeybee",
+ "nameDe": "Biene",
+ "keywordsEn": [
+ "animal",
+ "bee",
+ "bumblebee",
+ "honey",
+ "honeybee",
+ "insect",
+ "nature",
+ "spring"
+ ],
+ "keywordsDe": [
+ "Biene",
+ "Honig",
+ "Honigbiene",
+ "Hummel",
+ "Insekt",
+ "Maja",
+ "Natur",
+ "Summ",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F40D",
+ "emoji": "๐",
+ "nameEn": "snake",
+ "nameDe": "Schlange",
+ "keywordsEn": [
+ "animal",
+ "bearer",
+ "Ophiuchus",
+ "serpent",
+ "snake",
+ "zodiac"
+ ],
+ "keywordsDe": [
+ "Schlange",
+ "Schlangentrรคger",
+ "Sternzeichen",
+ "Tier",
+ "Tierkreis"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F422",
+ "emoji": "๐ข",
+ "nameEn": "turtle",
+ "nameDe": "Schildkrรถte",
+ "keywordsEn": [
+ "animal",
+ "terrapin",
+ "tortoise",
+ "turtle"
+ ],
+ "keywordsDe": [
+ "Schildkrรถte",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F42C",
+ "emoji": "๐ฌ",
+ "nameEn": "dolphin",
+ "nameDe": "Delfin",
+ "keywordsEn": [
+ "animal",
+ "beach",
+ "dolphin",
+ "flipper",
+ "ocean"
+ ],
+ "keywordsDe": [
+ "Delfin",
+ "Meer",
+ "Ozean",
+ "Strand",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F433",
+ "emoji": "๐ณ",
+ "nameEn": "spouting whale",
+ "nameDe": "blasender Wal",
+ "keywordsEn": [
+ "animal",
+ "beach",
+ "face",
+ "ocean",
+ "spouting",
+ "whale"
+ ],
+ "keywordsDe": [
+ "blasender Wal",
+ "Blasender Wal",
+ "Meer",
+ "Ozean",
+ "Tier",
+ "Wal"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F988",
+ "emoji": "๐ฆ",
+ "nameEn": "shark",
+ "nameDe": "Hai",
+ "keywordsEn": [
+ "animal",
+ "fish",
+ "shark"
+ ],
+ "keywordsDe": [
+ "Fisch",
+ "Hai",
+ "Haifisch",
+ "Meer",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F419",
+ "emoji": "๐",
+ "nameEn": "octopus",
+ "nameDe": "Oktopus",
+ "keywordsEn": [
+ "animal",
+ "creature",
+ "ocean",
+ "octopus"
+ ],
+ "keywordsDe": [
+ "Krake",
+ "Meer",
+ "Oktopus",
+ "Tier",
+ "Tintenfisch"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F989",
+ "emoji": "๐ฆ",
+ "nameEn": "owl",
+ "nameDe": "Eule",
+ "keywordsEn": [
+ "animal",
+ "bird",
+ "ornithology",
+ "owl",
+ "wise"
+ ],
+ "keywordsDe": [
+ "Eule",
+ "Raubvogel",
+ "Tier",
+ "Uhu",
+ "Vogel",
+ "weise"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "1F418",
+ "emoji": "๐",
+ "nameEn": "elephant",
+ "nameDe": "Elefant",
+ "keywordsEn": [
+ "animal",
+ "elephant"
+ ],
+ "keywordsDe": [
+ "Elefant",
+ "Tier"
+ ],
+ "tags": [
+ "animal"
+ ],
+ "group": "animals"
+ },
+ {
+ "id": "2600",
+ "emoji": "โ๏ธ",
+ "nameEn": "sun",
+ "nameDe": "Sonne",
+ "keywordsEn": [
+ "bright",
+ "rays",
+ "space",
+ "sun",
+ "sunny",
+ "weather"
+ ],
+ "keywordsDe": [
+ "Sonne",
+ "Sonnenstrahlen",
+ "sonnig",
+ "Strahlen",
+ "Wetter"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F327",
+ "emoji": "๐ง๏ธ",
+ "nameEn": "cloud with rain",
+ "nameDe": "Wolke mit Regen",
+ "keywordsEn": [
+ "cloud",
+ "rain",
+ "weather"
+ ],
+ "keywordsDe": [
+ "Regen",
+ "Regenwolke",
+ "Wetter",
+ "Wolke",
+ "Wolke mit Regen"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "26C8",
+ "emoji": "โ๏ธ",
+ "nameEn": "cloud with lightning and rain",
+ "nameDe": "Wolke mit Blitz und Regen",
+ "keywordsEn": [
+ "cloud",
+ "lightning",
+ "rain",
+ "thunder",
+ "thunderstorm"
+ ],
+ "keywordsDe": [
+ "Blitz",
+ "Gewitter",
+ "Regen",
+ "Wetter",
+ "Wolke",
+ "Wolke mit Blitz und Regen",
+ "wolkig"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "2744",
+ "emoji": "โ๏ธ",
+ "nameEn": "snowflake",
+ "nameDe": "Schneeflocke",
+ "keywordsEn": [
+ "cold",
+ "snow",
+ "snowflake",
+ "weather"
+ ],
+ "keywordsDe": [
+ "Flocke",
+ "kalt",
+ "Schnee",
+ "Schneeflocke",
+ "schneien",
+ "Wetter",
+ "Winter"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F308",
+ "emoji": "๐",
+ "nameEn": "rainbow",
+ "nameDe": "Regenbogen",
+ "keywordsEn": [
+ "gay",
+ "genderqueer",
+ "glbt",
+ "glbtq",
+ "lesbian",
+ "lgbt",
+ "lgbtq",
+ "lgbtqia",
+ "nature",
+ "pride",
+ "queer",
+ "rain",
+ "rainbow",
+ "trans",
+ "transgender",
+ "weather"
+ ],
+ "keywordsDe": [
+ "Regenbogen",
+ "Wetter"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F32A",
+ "emoji": "๐ช๏ธ",
+ "nameEn": "tornado",
+ "nameDe": "Wirbelsturm",
+ "keywordsEn": [
+ "cloud",
+ "tornado",
+ "weather",
+ "whirlwind"
+ ],
+ "keywordsDe": [
+ "Tornado",
+ "Wetter",
+ "Wirbelsturm"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F319",
+ "emoji": "๐",
+ "nameEn": "crescent moon",
+ "nameDe": "Mondsichel",
+ "keywordsEn": [
+ "crescent",
+ "moon",
+ "ramadan",
+ "space"
+ ],
+ "keywordsDe": [
+ "Mond",
+ "Mondsichel",
+ "Nacht"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "2B50",
+ "emoji": "โญ",
+ "nameEn": "star",
+ "nameDe": "weiรer mittelgroรer Stern",
+ "keywordsEn": [
+ "astronomy",
+ "medium",
+ "star",
+ "stars",
+ "white"
+ ],
+ "keywordsDe": [
+ "astronomie",
+ "gelber Stern",
+ "Himmel",
+ "Stern",
+ "weiรer mittelgroรer Stern"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F525",
+ "emoji": "๐ฅ",
+ "nameEn": "fire",
+ "nameDe": "Feuer",
+ "keywordsEn": [
+ "af",
+ "burn",
+ "fire",
+ "flame",
+ "hot",
+ "lit",
+ "litaf",
+ "tool"
+ ],
+ "keywordsDe": [
+ "brennen",
+ "Feuer",
+ "Flamme",
+ "heiร"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F4A7",
+ "emoji": "๐ง",
+ "nameEn": "droplet",
+ "nameDe": "Tropfen",
+ "keywordsEn": [
+ "cold",
+ "comic",
+ "drop",
+ "droplet",
+ "nature",
+ "sad",
+ "sweat",
+ "tear",
+ "water",
+ "weather"
+ ],
+ "keywordsDe": [
+ "Schweiร",
+ "Trรคne",
+ "Tropfen",
+ "Wasser",
+ "Wassertropfen"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F30A",
+ "emoji": "๐",
+ "nameEn": "water wave",
+ "nameDe": "Welle",
+ "keywordsEn": [
+ "nature",
+ "ocean",
+ "surf",
+ "surfer",
+ "surfing",
+ "water",
+ "wave"
+ ],
+ "keywordsDe": [
+ "Meer",
+ "Welle"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F338",
+ "emoji": "๐ธ",
+ "nameEn": "cherry blossom",
+ "nameDe": "Kirschblรผte",
+ "keywordsEn": [
+ "blossom",
+ "cherry",
+ "flower",
+ "plant",
+ "spring",
+ "springtime"
+ ],
+ "keywordsDe": [
+ "Blume",
+ "Blรผte",
+ "Kirschblรผte",
+ "Kirsche",
+ "Pflanze"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F33B",
+ "emoji": "๐ป",
+ "nameEn": "sunflower",
+ "nameDe": "Sonnenblume",
+ "keywordsEn": [
+ "flower",
+ "outdoors",
+ "plant",
+ "sun",
+ "sunflower"
+ ],
+ "keywordsDe": [
+ "Blume",
+ "Blรผte",
+ "Pflanze",
+ "Sonne",
+ "Sonnenblume"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F332",
+ "emoji": "๐ฒ",
+ "nameEn": "evergreen tree",
+ "nameDe": "Nadelbaum",
+ "keywordsEn": [
+ "christmas",
+ "evergreen",
+ "forest",
+ "pine",
+ "tree"
+ ],
+ "keywordsDe": [
+ "Baum",
+ "Nadelbaum",
+ "Pflanze",
+ "Tannenbaum"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F342",
+ "emoji": "๐",
+ "nameEn": "fallen leaf",
+ "nameDe": "Laub",
+ "keywordsEn": [
+ "autumn",
+ "fall",
+ "fallen",
+ "falling",
+ "leaf"
+ ],
+ "keywordsDe": [
+ "Blatt",
+ "Blรคtter",
+ "Fallendes Laub",
+ "Herbst",
+ "Laub",
+ "Pflanze"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "2601",
+ "emoji": "โ๏ธ",
+ "nameEn": "cloud",
+ "nameDe": "Wolke",
+ "keywordsEn": [
+ "cloud",
+ "weather"
+ ],
+ "keywordsDe": [
+ "Wetter",
+ "Wolke",
+ "wolkig"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "1F30B",
+ "emoji": "๐",
+ "nameEn": "volcano",
+ "nameDe": "Vulkan",
+ "keywordsEn": [
+ "eruption",
+ "mountain",
+ "nature",
+ "volcano"
+ ],
+ "keywordsDe": [
+ "Ausbruch",
+ "Berg",
+ "Lava",
+ "Vulkan",
+ "Vulkanausbruch",
+ "Wetter"
+ ],
+ "tags": [
+ "weather",
+ "nature"
+ ],
+ "group": "weather-nature"
+ },
+ {
+ "id": "26BD",
+ "emoji": "โฝ",
+ "nameEn": "soccer ball",
+ "nameDe": "Fuรball",
+ "keywordsEn": [
+ "ball",
+ "football",
+ "futbol",
+ "soccer",
+ "sport"
+ ],
+ "keywordsDe": [
+ "Ball",
+ "Bundesliga",
+ "Fuรball",
+ "Messi"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F3C0",
+ "emoji": "๐",
+ "nameEn": "basketball",
+ "nameDe": "Basketball",
+ "keywordsEn": [
+ "ball",
+ "basketball",
+ "hoop",
+ "sport"
+ ],
+ "keywordsDe": [
+ "Ball",
+ "Basketball",
+ "Korb",
+ "Sport"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F3BE",
+ "emoji": "๐พ",
+ "nameEn": "tennis",
+ "nameDe": "Tennisball",
+ "keywordsEn": [
+ "ball",
+ "racquet",
+ "sport",
+ "tennis"
+ ],
+ "keywordsDe": [
+ "Ball",
+ "Sport",
+ "Tennis",
+ "Tennisball"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F3C8",
+ "emoji": "๐",
+ "nameEn": "american football",
+ "nameDe": "Football",
+ "keywordsEn": [
+ "american",
+ "ball",
+ "bowl",
+ "football",
+ "sport",
+ "super"
+ ],
+ "keywordsDe": [
+ "American Football",
+ "Amerika",
+ "Ball",
+ "Football",
+ "Sport",
+ "Super Bowl"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "26BE",
+ "emoji": "โพ",
+ "nameEn": "baseball",
+ "nameDe": "Baseball",
+ "keywordsEn": [
+ "ball",
+ "baseball",
+ "sport"
+ ],
+ "keywordsDe": [
+ "Ball",
+ "Baseball"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F3CA",
+ "emoji": "๐",
+ "nameEn": "person swimming",
+ "nameDe": "Schwimmer(in)",
+ "keywordsEn": [
+ "freestyle",
+ "person",
+ "sport",
+ "swim",
+ "swimmer",
+ "swimming",
+ "triathlon"
+ ],
+ "keywordsDe": [
+ "Kraulen",
+ "Person",
+ "Pool",
+ "Schwimmbecken",
+ "Schwimmen",
+ "Schwimmer",
+ "Schwimmer(in)",
+ "Schwimmerin",
+ "Sport",
+ "Triathlon",
+ "Wasser"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F6B4",
+ "emoji": "๐ด",
+ "nameEn": "person biking",
+ "nameDe": "Radfahrer(in)",
+ "keywordsEn": [
+ "bicycle",
+ "bicyclist",
+ "bike",
+ "biking",
+ "cycle",
+ "cyclist",
+ "person",
+ "riding",
+ "sport"
+ ],
+ "keywordsDe": [
+ "Fahrrad",
+ "Person",
+ "Rad",
+ "radeln",
+ "Radfahren",
+ "Radfahrer",
+ "Radfahrer(in)",
+ "Radfahrerin",
+ "Sport",
+ "Tour de France"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F3C3",
+ "emoji": "๐",
+ "nameEn": "person running",
+ "nameDe": "laufende Person",
+ "keywordsEn": [
+ "fast",
+ "hurry",
+ "marathon",
+ "move",
+ "person",
+ "quick",
+ "race",
+ "racing",
+ "run",
+ "rush",
+ "speed"
+ ],
+ "keywordsDe": [
+ "eilig",
+ "joggen",
+ "laufen",
+ "Laufen",
+ "laufende Person",
+ "Lรคufer",
+ "Lรคuferin",
+ "Marathon",
+ "Person",
+ "rennen",
+ "schnell",
+ "Sport"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F3CB",
+ "emoji": "๐๏ธ",
+ "nameEn": "person lifting weights",
+ "nameDe": "Gewichtheber(in)",
+ "keywordsEn": [
+ "barbell",
+ "bodybuilder",
+ "deadlift",
+ "lifter",
+ "lifting",
+ "person",
+ "powerlifting",
+ "weight",
+ "weightlifter",
+ "weights",
+ "workout"
+ ],
+ "keywordsDe": [
+ "Gewicht",
+ "Gewichtheber",
+ "Gewichtheber(in)",
+ "Gewichtheberin",
+ "Hantel",
+ "Muskeln",
+ "Person",
+ "schwer",
+ "Sport",
+ "stark",
+ "Training"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "26F7",
+ "emoji": "โท๏ธ",
+ "nameEn": "skier",
+ "nameDe": "Skifahrer(in)",
+ "keywordsEn": [
+ "ski",
+ "skier",
+ "snow"
+ ],
+ "keywordsDe": [
+ "Schnee",
+ "Ski",
+ "Skifahrer",
+ "Skifahrer(in)",
+ "Skifahrerin",
+ "Sport"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F3C4",
+ "emoji": "๐",
+ "nameEn": "person surfing",
+ "nameDe": "Surfer(in)",
+ "keywordsEn": [
+ "beach",
+ "ocean",
+ "person",
+ "sport",
+ "surf",
+ "surfer",
+ "surfing",
+ "swell",
+ "waves"
+ ],
+ "keywordsDe": [
+ "Person",
+ "Sport",
+ "Surfboard",
+ "Surfbrett",
+ "Surfen",
+ "Surfer",
+ "Surfer(in)",
+ "Surferin",
+ "Wassersport",
+ "Wellenreiten",
+ "Wellenreiter",
+ "Wellenreiterin"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F9D7",
+ "emoji": "๐ง",
+ "nameEn": "person climbing",
+ "nameDe": "Bergsteiger(in)",
+ "keywordsEn": [
+ "climb",
+ "climber",
+ "climbing",
+ "mountain",
+ "person",
+ "rock",
+ "scale",
+ "up"
+ ],
+ "keywordsDe": [
+ "Bergsteiger",
+ "Bergsteiger(in)",
+ "Fels",
+ "Kletterer",
+ "klettern",
+ "Person",
+ "Sport"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F94A",
+ "emoji": "๐ฅ",
+ "nameEn": "boxing glove",
+ "nameDe": "Boxhandschuh",
+ "keywordsEn": [
+ "boxing",
+ "glove"
+ ],
+ "keywordsDe": [
+ "Boxen",
+ "Boxhandschuh",
+ "Handschuh",
+ "Sport"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F3C6",
+ "emoji": "๐",
+ "nameEn": "trophy",
+ "nameDe": "Pokal",
+ "keywordsEn": [
+ "champion",
+ "champs",
+ "prize",
+ "slay",
+ "sport",
+ "trophy",
+ "victory",
+ "win",
+ "winning"
+ ],
+ "keywordsDe": [
+ "Bundesliga",
+ "Champion",
+ "gewinnen",
+ "Pokal",
+ "Preis",
+ "Sieger",
+ "Sport",
+ "Trophรคe"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F947",
+ "emoji": "๐ฅ",
+ "nameEn": "1st place medal",
+ "nameDe": "Goldmedaille",
+ "keywordsEn": [
+ "1st",
+ "first",
+ "gold",
+ "medal",
+ "place"
+ ],
+ "keywordsDe": [
+ "Erster",
+ "Gewinn",
+ "Goldmedaille",
+ "Medaille 1. Platz"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "26F3",
+ "emoji": "โณ",
+ "nameEn": "flag in hole",
+ "nameDe": "Golffahne",
+ "keywordsEn": [
+ "flag",
+ "golf",
+ "hole",
+ "sport"
+ ],
+ "keywordsDe": [
+ "Golffahne",
+ "Golfplatz"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "1F9D8",
+ "emoji": "๐ง",
+ "nameEn": "person in lotus position",
+ "nameDe": "Person im Lotossitz",
+ "keywordsEn": [
+ "cross",
+ "legged",
+ "legs",
+ "lotus",
+ "meditation",
+ "peace",
+ "person",
+ "position",
+ "relax",
+ "serenity",
+ "yoga",
+ "yogi",
+ "zen"
+ ],
+ "keywordsDe": [
+ "burmesischer Sitz",
+ "Frau",
+ "Lotussitz",
+ "Meditation",
+ "Person",
+ "Person im Lotossitz",
+ "Person in Lotussitz",
+ "Schneidersitz",
+ "Yoga",
+ "Zen"
+ ],
+ "tags": [
+ "sport"
+ ],
+ "group": "sport"
+ },
+ {
+ "id": "2708",
+ "emoji": "โ๏ธ",
+ "nameEn": "airplane",
+ "nameDe": "Flugzeug",
+ "keywordsEn": [
+ "aeroplane",
+ "airplane",
+ "fly",
+ "flying",
+ "jet",
+ "plane",
+ "travel"
+ ],
+ "keywordsDe": [
+ "Flieger",
+ "Flugzeug"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F697",
+ "emoji": "๐",
+ "nameEn": "automobile",
+ "nameDe": "Auto",
+ "keywordsEn": [
+ "automobile",
+ "car",
+ "driving",
+ "vehicle"
+ ],
+ "keywordsDe": [
+ "Auto",
+ "fahren",
+ "Fahrzeug",
+ "rot",
+ "Verkehr"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F682",
+ "emoji": "๐",
+ "nameEn": "locomotive",
+ "nameDe": "Dampflokomotive",
+ "keywordsEn": [
+ "caboose",
+ "engine",
+ "locomotive",
+ "railway",
+ "steam",
+ "train",
+ "trains",
+ "travel"
+ ],
+ "keywordsDe": [
+ "Bahn",
+ "Dampf",
+ "Dampflok",
+ "Dampflokomotive",
+ "Fahrzeug",
+ "Lokomotive",
+ "Schiene",
+ "Zug"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F6B2",
+ "emoji": "๐ฒ",
+ "nameEn": "bicycle",
+ "nameDe": "Fahrrad",
+ "keywordsEn": [
+ "bicycle",
+ "bike",
+ "class",
+ "cycle",
+ "cycling",
+ "cyclist",
+ "gang",
+ "ride",
+ "spin",
+ "spinning"
+ ],
+ "keywordsDe": [
+ "BMX",
+ "Fahrrad",
+ "Rad",
+ "Radeln",
+ "Radler",
+ "Spinning"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F680",
+ "emoji": "๐",
+ "nameEn": "rocket",
+ "nameDe": "Rakete",
+ "keywordsEn": [
+ "launch",
+ "rocket",
+ "rockets",
+ "space",
+ "travel"
+ ],
+ "keywordsDe": [
+ "All",
+ "Rakete",
+ "Weltraum"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "26F5",
+ "emoji": "โต",
+ "nameEn": "sailboat",
+ "nameDe": "Segelboot",
+ "keywordsEn": [
+ "boat",
+ "resort",
+ "sailboat",
+ "sailing",
+ "sea",
+ "yacht"
+ ],
+ "keywordsDe": [
+ "Boot",
+ "Segelboot"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F3D6",
+ "emoji": "๐๏ธ",
+ "nameEn": "beach with umbrella",
+ "nameDe": "Strand mit Sonnenschirm",
+ "keywordsEn": [
+ "beach",
+ "umbrella"
+ ],
+ "keywordsDe": [
+ "Meer",
+ "Sonnenschirm",
+ "Strand",
+ "Strand mit Sonnenschirm"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F3D4",
+ "emoji": "๐๏ธ",
+ "nameEn": "snow-capped mountain",
+ "nameDe": "schneebedeckter Berg",
+ "keywordsEn": [
+ "cold",
+ "mountain",
+ "snow",
+ "snow-capped"
+ ],
+ "keywordsDe": [
+ "Berg",
+ "Gebirge",
+ "kalt",
+ "Schnee",
+ "schneebedeckter Berg",
+ "Schneebedeckter Berg"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F5FA",
+ "emoji": "๐บ๏ธ",
+ "nameEn": "world map",
+ "nameDe": "Weltkarte",
+ "keywordsEn": [
+ "map",
+ "world"
+ ],
+ "keywordsDe": [
+ "Karte",
+ "Landkarte",
+ "reisen",
+ "Welt",
+ "Weltkarte"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F9F3",
+ "emoji": "๐งณ",
+ "nameEn": "luggage",
+ "nameDe": "Gepรคck",
+ "keywordsEn": [
+ "bag",
+ "luggage",
+ "packing",
+ "roller",
+ "suitcase",
+ "travel"
+ ],
+ "keywordsDe": [
+ "Ballast",
+ "Gepรคck",
+ "Hartschalenkoffer",
+ "Koffer",
+ "Reise",
+ "Trolley-Tasche"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F3E8",
+ "emoji": "๐จ",
+ "nameEn": "hotel",
+ "nameDe": "Hotel",
+ "keywordsEn": [
+ "building",
+ "hotel"
+ ],
+ "keywordsDe": [
+ "Gebรคude",
+ "Hotel",
+ "รผbernachten",
+ "Unterkunft"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F695",
+ "emoji": "๐",
+ "nameEn": "taxi",
+ "nameDe": "Taxi",
+ "keywordsEn": [
+ "cab",
+ "cabbie",
+ "car",
+ "drive",
+ "taxi",
+ "vehicle",
+ "yellow"
+ ],
+ "keywordsDe": [
+ "Auto",
+ "Fahrzeug",
+ "Taxi"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F68C",
+ "emoji": "๐",
+ "nameEn": "bus",
+ "nameDe": "Bus",
+ "keywordsEn": [
+ "bus",
+ "school",
+ "vehicle"
+ ],
+ "keywordsDe": [
+ "Bus",
+ "Fahrzeug",
+ "Schulbus"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F6F3",
+ "emoji": "๐ณ๏ธ",
+ "nameEn": "passenger ship",
+ "nameDe": "Passagierschiff",
+ "keywordsEn": [
+ "passenger",
+ "ship"
+ ],
+ "keywordsDe": [
+ "Aida",
+ "Passagierschiff",
+ "Rundreise",
+ "Schiff",
+ "Seereise",
+ "Traumschiff"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "26FA",
+ "emoji": "โบ",
+ "nameEn": "tent",
+ "nameDe": "Zelt",
+ "keywordsEn": [
+ "camping",
+ "tent"
+ ],
+ "keywordsDe": [
+ "Camping",
+ "Campingurlaub",
+ "Zelt",
+ "Zeltplatz"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F5FD",
+ "emoji": "๐ฝ",
+ "nameEn": "Statue of Liberty",
+ "nameDe": "Freiheitsstatue",
+ "keywordsEn": [
+ "liberty",
+ "Liberty",
+ "new",
+ "ny",
+ "nyc",
+ "statue",
+ "Statue",
+ "york"
+ ],
+ "keywordsDe": [
+ "Amerika",
+ "Freiheit",
+ "Freiheitsstatue"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F9ED",
+ "emoji": "๐งญ",
+ "nameEn": "compass",
+ "nameDe": "Kompass",
+ "keywordsEn": [
+ "compass",
+ "direction",
+ "magnetic",
+ "navigation",
+ "orienteering"
+ ],
+ "keywordsDe": [
+ "Himmelsrichtung",
+ "Kompass",
+ "magnetisch",
+ "Navigation",
+ "Orientierung",
+ "Richtung",
+ "Windrose"
+ ],
+ "tags": [
+ "travel"
+ ],
+ "group": "travel"
+ },
+ {
+ "id": "1F600",
+ "emoji": "๐",
+ "nameEn": "grinning face",
+ "nameDe": "grinsendes Gesicht",
+ "keywordsEn": [
+ "cheerful",
+ "cheery",
+ "face",
+ "grin",
+ "grinning",
+ "happy",
+ "laugh",
+ "nice",
+ "smile",
+ "smiling",
+ "teeth"
+ ],
+ "keywordsDe": [
+ "froh",
+ "frรถhlich",
+ "Gesicht",
+ "grinsen",
+ "haha",
+ "lachen",
+ "lol",
+ "lustig",
+ "Smiley"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F602",
+ "emoji": "๐",
+ "nameEn": "face with tears of joy",
+ "nameDe": "Gesicht mit Freudentrรคnen",
+ "keywordsEn": [
+ "crying",
+ "face",
+ "feels",
+ "funny",
+ "haha",
+ "happy",
+ "hehe",
+ "hilarious",
+ "joy",
+ "laugh",
+ "lmao",
+ "lol",
+ "rofl",
+ "roflmao",
+ "tear"
+ ],
+ "keywordsDe": [
+ "Freude",
+ "Freudentrรคnen",
+ "frรถhlich",
+ "Gesicht",
+ "haha",
+ "happy",
+ "hehe",
+ "hihi",
+ "lachen",
+ "lol",
+ "lustig",
+ "muahaha",
+ "rofl",
+ "Smiley",
+ "Trรคnen",
+ "witzig"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F60D",
+ "emoji": "๐",
+ "nameEn": "smiling face with heart-eyes",
+ "nameDe": "lรคchelndes Gesicht mit herzfรถrmigen Augen",
+ "keywordsEn": [
+ "143",
+ "bae",
+ "eye",
+ "face",
+ "feels",
+ "heart-eyes",
+ "hearts",
+ "ily",
+ "kisses",
+ "love",
+ "romance",
+ "romantic",
+ "smile",
+ "xoxo"
+ ],
+ "keywordsDe": [
+ "Augen",
+ "Gesicht",
+ "glรผcklich",
+ "Herzen",
+ "herzfรถrmig",
+ "lรคcheln",
+ "Liebe",
+ "Smiley",
+ "verliebt"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F622",
+ "emoji": "๐ข",
+ "nameEn": "crying face",
+ "nameDe": "weinendes Gesicht",
+ "keywordsEn": [
+ "awful",
+ "cry",
+ "crying",
+ "face",
+ "feels",
+ "miss",
+ "sad",
+ "tear",
+ "triste",
+ "unhappy"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Smiley",
+ "Trรคne",
+ "traurig",
+ "weinen"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F62D",
+ "emoji": "๐ญ",
+ "nameEn": "loudly crying face",
+ "nameDe": "heulendes Gesicht",
+ "keywordsEn": [
+ "bawling",
+ "cry",
+ "crying",
+ "face",
+ "loudly",
+ "sad",
+ "sob",
+ "tear",
+ "tears",
+ "unhappy"
+ ],
+ "keywordsDe": [
+ "flennen",
+ "Gesicht",
+ "heulen",
+ "schluchzen",
+ "Smiley",
+ "Trรคnen",
+ "Trรคnenfluss",
+ "traurig",
+ "weinen"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F621",
+ "emoji": "๐ก",
+ "nameEn": "enraged face",
+ "nameDe": "schmollendes Gesicht",
+ "keywordsEn": [
+ "anger",
+ "angry",
+ "enraged",
+ "face",
+ "feels",
+ "mad",
+ "maddening",
+ "pouting",
+ "rage",
+ "red",
+ "shade",
+ "unhappy",
+ "upset"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "rot",
+ "Rot angelaufener Smiley",
+ "schmollendes Gesicht",
+ "wรผtend"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F631",
+ "emoji": "๐ฑ",
+ "nameEn": "face screaming in fear",
+ "nameDe": "vor Angst schreiendes Gesicht",
+ "keywordsEn": [
+ "epic",
+ "face",
+ "fear",
+ "fearful",
+ "munch",
+ "scared",
+ "scream",
+ "screamer",
+ "screaming",
+ "shocked",
+ "surprised",
+ "woah"
+ ],
+ "keywordsDe": [
+ "Angst",
+ "รคngstlich",
+ "Gesicht",
+ "Munch",
+ "omg",
+ "Panik",
+ "Schrei",
+ "schreien",
+ "schreiend"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F634",
+ "emoji": "๐ด",
+ "nameEn": "sleeping face",
+ "nameDe": "schlafendes Gesicht",
+ "keywordsEn": [
+ "bed",
+ "bedtime",
+ "face",
+ "good",
+ "goodnight",
+ "nap",
+ "night",
+ "sleep",
+ "sleeping",
+ "tired",
+ "whatever",
+ "yawn",
+ "zzz"
+ ],
+ "keywordsDe": [
+ "dรถsen",
+ "einschlafen",
+ "gรคhnen",
+ "Gesicht",
+ "gute Nacht",
+ "langweilig",
+ "Mittagsschlaf",
+ "mรผde",
+ "Nickerchen",
+ "Schlaf",
+ "schlafen",
+ "Smiley",
+ "zzz"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F914",
+ "emoji": "๐ค",
+ "nameEn": "thinking face",
+ "nameDe": "nachdenkendes Gesicht",
+ "keywordsEn": [
+ "chin",
+ "consider",
+ "face",
+ "hmm",
+ "ponder",
+ "pondering",
+ "thinking",
+ "wondering"
+ ],
+ "keywordsDe": [
+ "fragen",
+ "Gesicht",
+ "grรผbeln",
+ "hm",
+ "komisch",
+ "nachdenken",
+ "nachdenklich",
+ "Smiley",
+ "wundern"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F60E",
+ "emoji": "๐",
+ "nameEn": "smiling face with sunglasses",
+ "nameDe": "lรคchelndes Gesicht mit Sonnenbrille",
+ "keywordsEn": [
+ "awesome",
+ "beach",
+ "bright",
+ "bro",
+ "chilling",
+ "cool",
+ "face",
+ "rad",
+ "relaxed",
+ "shades",
+ "slay",
+ "smile",
+ "style",
+ "sunglasses",
+ "swag",
+ "win"
+ ],
+ "keywordsDe": [
+ "cool",
+ "Gesicht",
+ "lรคcheln",
+ "Smiley",
+ "Sonnenbrille"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F973",
+ "emoji": "๐ฅณ",
+ "nameEn": "partying face",
+ "nameDe": "Partygesicht",
+ "keywordsEn": [
+ "bday",
+ "birthday",
+ "celebrate",
+ "celebration",
+ "excited",
+ "face",
+ "happy",
+ "hat",
+ "hooray",
+ "horn",
+ "party",
+ "partying"
+ ],
+ "keywordsDe": [
+ "aufgeregt",
+ "Feier",
+ "Fest",
+ "frรถhlich",
+ "Geburtstag",
+ "Gesicht",
+ "Glรผckwunsch",
+ "Hurra",
+ "Hut",
+ "Party",
+ "Smiley",
+ "รberraschung"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F62C",
+ "emoji": "๐ฌ",
+ "nameEn": "grimacing face",
+ "nameDe": "Grimassen schneidendes Gesicht",
+ "keywordsEn": [
+ "awk",
+ "awkward",
+ "dentist",
+ "face",
+ "grimace",
+ "grimacing",
+ "grinning",
+ "smile",
+ "smiling"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "Grimasse",
+ "peinlich",
+ "schneiden",
+ "Smiley",
+ "verziehen",
+ "Zรคhne"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F644",
+ "emoji": "๐",
+ "nameEn": "face with rolling eyes",
+ "nameDe": "Augen verdrehendes Gesicht",
+ "keywordsEn": [
+ "eyeroll",
+ "eyes",
+ "face",
+ "rolling",
+ "shade",
+ "ugh",
+ "whatever"
+ ],
+ "keywordsDe": [
+ "Augen",
+ "genervt",
+ "Gesicht",
+ "meinetwegen",
+ "nerv",
+ "ohje",
+ "rollen",
+ "Smiley",
+ "verdrehen",
+ "verrollen"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F607",
+ "emoji": "๐",
+ "nameEn": "smiling face with halo",
+ "nameDe": "lรคchelndes Gesicht mit Heiligenschein",
+ "keywordsEn": [
+ "angel",
+ "angelic",
+ "angels",
+ "blessed",
+ "face",
+ "fairy",
+ "fairytale",
+ "fantasy",
+ "halo",
+ "happy",
+ "innocent",
+ "peaceful",
+ "smile",
+ "smiling",
+ "spirit",
+ "tale"
+ ],
+ "keywordsDe": [
+ "brav",
+ "Engel",
+ "Gesicht",
+ "glรผcklich",
+ "Heiligenschein",
+ "Lรคcheln",
+ "lieb",
+ "seelig",
+ "Smiley"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F917",
+ "emoji": "๐ค",
+ "nameEn": "smiling face with open hands",
+ "nameDe": "Gesicht mit umarmenden Hรคnden",
+ "keywordsEn": [
+ "face",
+ "hands",
+ "hug",
+ "hugging",
+ "open",
+ "smiling"
+ ],
+ "keywordsDe": [
+ "drรผcken",
+ "Gesicht",
+ "Hรคnde",
+ "Smiley",
+ "umarmen",
+ "Umarmung"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F624",
+ "emoji": "๐ค",
+ "nameEn": "face with steam from nose",
+ "nameDe": "schnaubendes Gesicht",
+ "keywordsEn": [
+ "anger",
+ "angry",
+ "face",
+ "feels",
+ "fume",
+ "fuming",
+ "furious",
+ "fury",
+ "mad",
+ "nose",
+ "steam",
+ "triumph",
+ "unhappy",
+ "won"
+ ],
+ "keywordsDe": [
+ "erleichtert",
+ "Gesicht",
+ "gewonnen",
+ "schnaubendes Gesicht"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F970",
+ "emoji": "๐ฅฐ",
+ "nameEn": "smiling face with hearts",
+ "nameDe": "lรคchelndes Gesicht mit Herzen",
+ "keywordsEn": [
+ "3",
+ "adore",
+ "crush",
+ "face",
+ "heart",
+ "hearts",
+ "ily",
+ "love",
+ "romance",
+ "smile",
+ "smiling",
+ "you"
+ ],
+ "keywordsDe": [
+ "3",
+ "anhimmeln",
+ "bewundern",
+ "Gesicht",
+ "HDGDL",
+ "HDL",
+ "Herzchen",
+ "Herzen",
+ "lรคcheln",
+ "lieb",
+ "Liebe",
+ "Smiley",
+ "verknallt",
+ "verliebt",
+ "verzรผckt"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F605",
+ "emoji": "๐
",
+ "nameEn": "grinning face with sweat",
+ "nameDe": "grinsendes Gesicht mit Schweiรtropfen",
+ "keywordsEn": [
+ "cold",
+ "dejected",
+ "excited",
+ "face",
+ "grinning",
+ "mouth",
+ "nervous",
+ "open",
+ "smile",
+ "smiling",
+ "stress",
+ "stressed",
+ "sweat"
+ ],
+ "keywordsDe": [
+ "Gesicht",
+ "grinsen",
+ "lรคcheln",
+ "lachen",
+ "lustig",
+ "Mund",
+ "nervรถs",
+ "offen",
+ "Schweiร",
+ "Schweiรtropfen",
+ "schwitzen",
+ "Smiley"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1FAE0",
+ "emoji": "๐ซ ",
+ "nameEn": "melting face",
+ "nameDe": "schmelzendes Gesicht",
+ "keywordsEn": [
+ "disappear",
+ "dissolve",
+ "embarrassed",
+ "face",
+ "haha",
+ "heat",
+ "hot",
+ "liquid",
+ "lol",
+ "melt",
+ "melting",
+ "sarcasm",
+ "sarcastic"
+ ],
+ "keywordsDe": [
+ "auflรถsen",
+ "beschรคmt",
+ "flรผssig",
+ "Gesicht",
+ "Hitze",
+ "Sarkasmus",
+ "sarkastisch",
+ "schmelzen",
+ "schwitzen",
+ "Smiley",
+ "verschwinden"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F610",
+ "emoji": "๐",
+ "nameEn": "neutral face",
+ "nameDe": "neutrales Gesicht",
+ "keywordsEn": [
+ "awkward",
+ "blank",
+ "deadpan",
+ "expressionless",
+ "face",
+ "fine",
+ "jealous",
+ "meh",
+ "neutral",
+ "oh",
+ "shade",
+ "straight",
+ "unamused",
+ "unhappy",
+ "unimpressed",
+ "whatever"
+ ],
+ "keywordsDe": [
+ "ausdruckslos",
+ "Gesicht",
+ "kommentarlos",
+ "neutral",
+ "Smiley",
+ "todernst",
+ "unbeeindruckt"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "2764",
+ "emoji": "โค๏ธ",
+ "nameEn": "red heart",
+ "nameDe": "rotes Herz",
+ "keywordsEn": [
+ "emotion",
+ "heart",
+ "love",
+ "red"
+ ],
+ "keywordsDe": [
+ "Herz",
+ "rotes Herz"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F494",
+ "emoji": "๐",
+ "nameEn": "broken heart",
+ "nameDe": "gebrochenes Herz",
+ "keywordsEn": [
+ "break",
+ "broken",
+ "crushed",
+ "emotion",
+ "heart",
+ "heartbroken",
+ "lonely",
+ "sad"
+ ],
+ "keywordsDe": [
+ "allein",
+ "gebrochen",
+ "Gefรผhl",
+ "Herz",
+ "Herzschmerz",
+ "Liebeskummer",
+ "Schmerz",
+ "traurig",
+ "Trennung",
+ "zerbrochen"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F44D",
+ "emoji": "๐",
+ "nameEn": "thumbs up",
+ "nameDe": "Daumen hoch",
+ "keywordsEn": [
+ "+1",
+ "good",
+ "hand",
+ "like",
+ "thumb",
+ "up",
+ "yes"
+ ],
+ "keywordsDe": [
+ "Daumen",
+ "Daumen hoch",
+ "einverstanden",
+ "Finger",
+ "gut",
+ "Hand",
+ "in Ordnung",
+ "jawohl",
+ "klar",
+ "klasse",
+ "like",
+ "nach oben",
+ "ok",
+ "super",
+ "top"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F44E",
+ "emoji": "๐",
+ "nameEn": "thumbs down",
+ "nameDe": "Daumen runter",
+ "keywordsEn": [
+ "-1",
+ "bad",
+ "dislike",
+ "down",
+ "good",
+ "hand",
+ "no",
+ "nope",
+ "thumb",
+ "thumbs"
+ ],
+ "keywordsDe": [
+ "Daumen",
+ "Daumen runter",
+ "Hand",
+ "nach unten",
+ "schlecht"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F64F",
+ "emoji": "๐",
+ "nameEn": "folded hands",
+ "nameDe": "zusammengelegte Handflรคchen",
+ "keywordsEn": [
+ "appreciate",
+ "ask",
+ "beg",
+ "blessed",
+ "bow",
+ "cmon",
+ "five",
+ "folded",
+ "gesture",
+ "hand",
+ "high",
+ "please",
+ "pray",
+ "thanks",
+ "thx"
+ ],
+ "keywordsDe": [
+ "beten",
+ "betend",
+ "bitten",
+ "danke",
+ "danken",
+ "Gebet",
+ "gefaltet",
+ "grรผรen",
+ "Handflรคchen",
+ "zusammengelegt"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F44F",
+ "emoji": "๐",
+ "nameEn": "clapping hands",
+ "nameDe": "klatschende Hรคnde",
+ "keywordsEn": [
+ "applause",
+ "approval",
+ "awesome",
+ "clap",
+ "congrats",
+ "congratulations",
+ "excited",
+ "good",
+ "great",
+ "hand",
+ "homie",
+ "job",
+ "nice",
+ "prayed",
+ "well",
+ "yay"
+ ],
+ "keywordsDe": [
+ "applaudieren",
+ "Applaus",
+ "Beifall",
+ "Gratulation",
+ "groรartig",
+ "gut",
+ "Hand",
+ "Hรคnde",
+ "Klatschen",
+ "klatschend",
+ "toll"
+ ],
+ "tags": [
+ "emotion"
+ ],
+ "group": "emotions"
+ },
+ {
+ "id": "1F4BB",
+ "emoji": "๐ป",
+ "nameEn": "laptop",
+ "nameDe": "Laptop",
+ "keywordsEn": [
+ "computer",
+ "laptop",
+ "office",
+ "pc",
+ "personal"
+ ],
+ "keywordsDe": [
+ "Bildschirm",
+ "Computer",
+ "Computermonitor",
+ "Laptop",
+ "Monitor",
+ "Notebook",
+ "PC"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F4F1",
+ "emoji": "๐ฑ",
+ "nameEn": "mobile phone",
+ "nameDe": "Mobiltelefon",
+ "keywordsEn": [
+ "cell",
+ "communication",
+ "mobile",
+ "phone",
+ "telephone"
+ ],
+ "keywordsDe": [
+ "Handy",
+ "iPhone",
+ "Mobiltelefon",
+ "Smartphone",
+ "telefonieren"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "231A",
+ "emoji": "โ",
+ "nameEn": "watch",
+ "nameDe": "Armbanduhr",
+ "keywordsEn": [
+ "clock",
+ "time",
+ "watch"
+ ],
+ "keywordsDe": [
+ "Armbanduhr",
+ "Uhr",
+ "Zeit"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F4F7",
+ "emoji": "๐ท",
+ "nameEn": "camera",
+ "nameDe": "Fotoapparat",
+ "keywordsEn": [
+ "camera",
+ "photo",
+ "selfie",
+ "snap",
+ "tbt",
+ "trip",
+ "video"
+ ],
+ "keywordsDe": [
+ "Fotoapparat",
+ "Fotos",
+ "Kamera"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F511",
+ "emoji": "๐",
+ "nameEn": "key",
+ "nameDe": "Schlรผssel",
+ "keywordsEn": [
+ "key",
+ "keys",
+ "lock",
+ "major",
+ "password",
+ "unlock"
+ ],
+ "keywordsDe": [
+ "Passwort",
+ "schlieรen",
+ "Schlรผssel"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F512",
+ "emoji": "๐",
+ "nameEn": "locked",
+ "nameDe": "geschlossenes Schloss",
+ "keywordsEn": [
+ "closed",
+ "lock",
+ "locked",
+ "private"
+ ],
+ "keywordsDe": [
+ "Datenschutz",
+ "geschlossen",
+ "geschlossenes Schloss",
+ "Geschlossenes Schloss",
+ "Schloss",
+ "Sicherheit"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F4DA",
+ "emoji": "๐",
+ "nameEn": "books",
+ "nameDe": "Bรผcherstapel",
+ "keywordsEn": [
+ "book",
+ "books",
+ "education",
+ "fantasy",
+ "knowledge",
+ "library",
+ "novels",
+ "reading",
+ "school",
+ "study"
+ ],
+ "keywordsDe": [
+ "Bรผcher",
+ "Bรผcherstapel",
+ "Fantasie",
+ "lesen",
+ "Roman"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "270F",
+ "emoji": "โ๏ธ",
+ "nameEn": "pencil",
+ "nameDe": "Bleistift",
+ "keywordsEn": [
+ "pencil"
+ ],
+ "keywordsDe": [
+ "Bleistift",
+ "Stift"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F4CE",
+ "emoji": "๐",
+ "nameEn": "paperclip",
+ "nameDe": "Bรผroklammer",
+ "keywordsEn": [
+ "paperclip"
+ ],
+ "keywordsDe": [
+ "Bรผroklammer"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F4A1",
+ "emoji": "๐ก",
+ "nameEn": "light bulb",
+ "nameDe": "Glรผhbirne",
+ "keywordsEn": [
+ "bulb",
+ "comic",
+ "electric",
+ "idea",
+ "light"
+ ],
+ "keywordsDe": [
+ "Glรผhbirne",
+ "hell",
+ "Idee",
+ "Lampe",
+ "Licht"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F527",
+ "emoji": "๐ง",
+ "nameEn": "wrench",
+ "nameDe": "Schraubenschlรผssel",
+ "keywordsEn": [
+ "home",
+ "improvement",
+ "spanner",
+ "tool",
+ "wrench"
+ ],
+ "keywordsDe": [
+ "Schraubenschlรผssel",
+ "Werkzeug"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F528",
+ "emoji": "๐จ",
+ "nameEn": "hammer",
+ "nameDe": "Hammer",
+ "keywordsEn": [
+ "hammer",
+ "home",
+ "improvement",
+ "repairs",
+ "tool"
+ ],
+ "keywordsDe": [
+ "Hammer",
+ "Werkzeug"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F4B0",
+ "emoji": "๐ฐ",
+ "nameEn": "money bag",
+ "nameDe": "Geldsack",
+ "keywordsEn": [
+ "bag",
+ "bank",
+ "bet",
+ "billion",
+ "cash",
+ "cost",
+ "dollar",
+ "gold",
+ "million",
+ "money",
+ "moneybag",
+ "paid",
+ "paying",
+ "pot",
+ "rich",
+ "win"
+ ],
+ "keywordsDe": [
+ "Geld",
+ "Geldsack",
+ "Moneten",
+ "reich",
+ "Sack"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F381",
+ "emoji": "๐",
+ "nameEn": "wrapped gift",
+ "nameDe": "Geschenk",
+ "keywordsEn": [
+ "birthday",
+ "bow",
+ "box",
+ "celebration",
+ "christmas",
+ "gift",
+ "present",
+ "surprise",
+ "wrapped"
+ ],
+ "keywordsDe": [
+ "Feier",
+ "Geburtstag",
+ "Geschenk",
+ "Schachtel",
+ "Schleife",
+ "รberraschung",
+ "verpackt",
+ "Weihnachten"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F388",
+ "emoji": "๐",
+ "nameEn": "balloon",
+ "nameDe": "Luftballon",
+ "keywordsEn": [
+ "balloon",
+ "birthday",
+ "celebrate",
+ "celebration"
+ ],
+ "keywordsDe": [
+ "Ballon",
+ "Feier",
+ "Geburtstag",
+ "Luftballon"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "23F0",
+ "emoji": "โฐ",
+ "nameEn": "alarm clock",
+ "nameDe": "Wecker",
+ "keywordsEn": [
+ "alarm",
+ "clock",
+ "hours",
+ "hrs",
+ "late",
+ "time",
+ "waiting"
+ ],
+ "keywordsDe": [
+ "Uhr",
+ "Uhrzeit",
+ "Wecker",
+ "Zeit"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F4C5",
+ "emoji": "๐
",
+ "nameEn": "calendar",
+ "nameDe": "Kalender",
+ "keywordsEn": [
+ "calendar",
+ "date"
+ ],
+ "keywordsDe": [
+ "Datum",
+ "Kalender",
+ "Kalenderblatt"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F5D1",
+ "emoji": "๐๏ธ",
+ "nameEn": "wastebasket",
+ "nameDe": "Papierkorb",
+ "keywordsEn": [
+ "can",
+ "garbage",
+ "trash",
+ "waste",
+ "wastebasket"
+ ],
+ "keywordsDe": [
+ "Abfalleimer",
+ "Papierkorb"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F9F9",
+ "emoji": "๐งน",
+ "nameEn": "broom",
+ "nameDe": "Besen",
+ "keywordsEn": [
+ "broom",
+ "cleaning",
+ "sweeping",
+ "witch"
+ ],
+ "keywordsDe": [
+ "Besen",
+ "fegen",
+ "Fegen",
+ "Hexe",
+ "Hexenbesen",
+ "kehren",
+ "Kehren",
+ "reinigen",
+ "sauber machen",
+ "sรคubern"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F6D2",
+ "emoji": "๐",
+ "nameEn": "shopping cart",
+ "nameDe": "Einkaufswagen",
+ "keywordsEn": [
+ "cart",
+ "shopping",
+ "trolley"
+ ],
+ "keywordsDe": [
+ "einkaufen",
+ "Einkaufswagen"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F3AE",
+ "emoji": "๐ฎ",
+ "nameEn": "video game",
+ "nameDe": "Gamepad",
+ "keywordsEn": [
+ "controller",
+ "entertainment",
+ "game",
+ "video"
+ ],
+ "keywordsDe": [
+ "Gamepad",
+ "Gaming",
+ "Unterhaltung",
+ "Videospiel"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F3B2",
+ "emoji": "๐ฒ",
+ "nameEn": "game die",
+ "nameDe": "Spielwรผrfel",
+ "keywordsEn": [
+ "dice",
+ "die",
+ "entertainment",
+ "game"
+ ],
+ "keywordsDe": [
+ "Casino",
+ "Spiel",
+ "Spielwรผrfel",
+ "Unterhaltung",
+ "Wรผrfel",
+ "wรผrfeln",
+ "Zufall"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F9E9",
+ "emoji": "๐งฉ",
+ "nameEn": "puzzle piece",
+ "nameDe": "Puzzleteil",
+ "keywordsEn": [
+ "clue",
+ "interlocking",
+ "jigsaw",
+ "piece",
+ "puzzle"
+ ],
+ "keywordsDe": [
+ "Bestandteile",
+ "Hinweise",
+ "Hobby",
+ "passen",
+ "puzzeln",
+ "Puzzle",
+ "Puzzlestรผck",
+ "Puzzleteil",
+ "Puzzleteile",
+ "Spiel",
+ "Teile",
+ "zusammenpassen",
+ "zusammensetzen"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F3E0",
+ "emoji": "๐ ",
+ "nameEn": "house",
+ "nameDe": "Haus",
+ "keywordsEn": [
+ "building",
+ "country",
+ "heart",
+ "home",
+ "house",
+ "ranch",
+ "settle",
+ "simple",
+ "suburban",
+ "suburbia",
+ "where"
+ ],
+ "keywordsDe": [
+ "Gebรคude",
+ "Haus",
+ "Zuhause"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F6BF",
+ "emoji": "๐ฟ",
+ "nameEn": "shower",
+ "nameDe": "Dusche",
+ "keywordsEn": [
+ "shower",
+ "water"
+ ],
+ "keywordsDe": [
+ "Dusche",
+ "Duschkopf"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ },
+ {
+ "id": "1F6CF",
+ "emoji": "๐๏ธ",
+ "nameEn": "bed",
+ "nameDe": "Bett",
+ "keywordsEn": [
+ "bed",
+ "hotel",
+ "sleep"
+ ],
+ "keywordsDe": [
+ "Bett",
+ "Hotel",
+ "schlafen",
+ "รbernachtung"
+ ],
+ "tags": [
+ "object",
+ "everyday"
+ ],
+ "group": "objects"
+ }
+ ]
+}
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/golden-queries.json b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/golden-queries.json
new file mode 100644
index 0000000..23f9c53
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/composeResources/files/catalog/golden-queries.json
@@ -0,0 +1 @@
+[{"query":"healthy things to eat","vector":[0.07284907,0.002390583,-0.008331553,-0.024967035,0.052106317,0.07135026,0.08710375,0.08348018,-0.04791309,-0.07235214,0.004430864,0.04625607,-0.01122365,-0.011344578,0.09124756,0.016883548,0.070083044,0.10707923,-0.015652323,0.069581285,0.03239937,-0.010904154,0.03803893,0.0320713,0.11064041,-0.004715649,0.0060041556,0.024751253,-0.017183777,-0.01081847,-0.013599367,0.052328058,0.042778235,-0.057675697,0.01846452,0.037307892,0.036314443,-0.019112695,-0.04481465,-0.024396041,-0.037371382,0.02647604,-0.067535,0.019472994,0.0076937973,0.0029685763,-0.09172296,0.0476956,-0.07433935,-0.019591615,5.448054E-4,0.03600658,0.12008389,-0.0013587624,0.08028994,-0.0022283965,-0.026610123,0.01490511,-0.030759618,0.044624593,-4.340438E-4,0.06313527,-0.037477203,-0.05248122,0.0863433,-0.017335633,0.04692081,-0.012288979,0.024230734,-0.008297804,-0.04208716,0.023831127,-0.05977369,-0.02874237,0.07170364,-0.11010806,-0.0650917,-0.016185764,-0.010138073,-0.03789661,-0.012422002,0.03646874,0.076344796,0.040639956,-0.02228522,-0.038783737,0.04375215,-0.03833747,-0.08583359,0.073170684,-0.009038978,0.11052969,0.04428195,-0.008316288,0.0031951892,-0.05421135,0.006951917,-0.11172203,0.0028213311,-0.07167933,-0.049013074,-0.09474253,-0.0044527603,-0.03192673,-0.12907207,-0.03931008,-0.08266533,-0.03047245,0.038206547,-0.0036035832,-0.044230174,0.018468838,-0.032563575,-0.045784626,-0.024221942,0.06907895,-0.013028465,0.0011220505,-0.059685376,-0.09757149,-0.0040686564,0.008996998,0.029671863,-0.016903,0.0050494787,-0.0186182,-0.023601776,-0.0071221157,0.030448643,3.0186752E-4,0.083335735,0.023492942,-0.05192084,-0.0483384,-0.027660662,0.005072202,0.04004709,0.009118048,0.019805294,-0.0012505195,-0.0056252703,-0.03494127,0.009396538,-0.05656234,0.0065693613,-0.061102316,-0.0025752792,-0.050362136,0.016710946,-0.014520799,0.029974181,0.04715673,0.02148913,-0.0061849933,-0.017011752,0.060739405,-0.015874704,0.05006712,0.057809442,0.0051836637,0.039750997,0.020663176,-0.0053423685,-0.07148375,-0.03659101,-0.03752719,-0.007977565,0.02282836,-0.008415334,0.037154086,-0.014721103,-6.4573594E-4,-0.008567137,-0.04825772,-0.05679068,0.017196786,0.010116211,-0.014816526,0.042866867,0.0054431604,-0.03013168,0.049282297,0.042468388,0.063316904,0.02210394,-0.0040863072,0.10456851,-0.01947084,-0.022710957,0.05478165,-0.07714149,-0.067269735,0.07263931,8.0354606E-5,0.029248973,0.0037717146,-0.0053079855,0.047374506,0.05245629,-0.04168245,-0.091024935,-0.02519087,0.092071734,-9.743711E-4,-0.086961694,7.149419E-5,0.0013942041,-0.008311505,0.032664973,0.059402708,-0.060060102,-0.037353814,0.14909172,0.08384817,-0.02034206,-0.042144086,-0.0039313245,0.022522198,0.019734522,-0.09017956,-0.022891803,-0.026295487,-0.0039512254,0.046194214,-0.025658334,0.07936229,0.1209384,-0.027730335,0.008972501,0.007804158,-0.034866504,6.8950705E-4,-0.020588834,-0.0044352757,0.032726716,0.04445829,0.022027444,-0.022321438,0.05480193,-0.095232025,0.0889969,0.032091126,-0.08172376,0.00780641,-0.01489576,-0.009663476,0.011174454,-0.05959532,-0.034130827,-0.031080876,-0.03129469,0.18077171,0.08404938,0.08570738,-0.024097122,-0.040771246,0.0059437687,0.0022754904,0.0028366032,0.033482745,-0.024909155,-0.0042233807,-0.010972431,-0.023082465,-0.011480668,0.0018041214,-0.008337784,-0.04825634,-0.028510291,-0.015386528,0.0055377604,0.012759135,0.036202215,0.011240934,-0.0041706227,-0.02827486,5.207298E-4,0.053916156,-0.04527867,-0.04975717,0.0069765155,0.017061677,0.029824493,0.031966332,0.019877255,0.023923064,0.05552478,0.030026799,-0.014320796,0.016111294,0.021093244,0.031387273,-0.048003975,-0.057678707,0.0028772103,0.0054985895,0.0055863443,-0.012086347,-0.035998996,-0.03070836,0.024136517,-0.0043579713,-4.2736545E-4,0.058804166,0.022643011,0.03957581,-0.0018939283,-0.07869229,-0.085996546,-0.033773646,0.011905039,0.0054452987,-0.03483062,-0.019060284,0.021631628,-0.0036308863,-0.038257483,0.02777264,-0.02086212,0.02682406,-0.01374042,-0.024208564,0.031374577,-0.009859004,0.002990763,-0.07039063,0.03332469,0.03276515,0.037772156,0.006289097,-0.026497604,-0.014950671,-0.028259683,-0.024973199,0.033802085,0.072888635,0.002602701,0.0014059792,-0.023608055,0.040162016,-0.018948598,0.005788424,0.024903528,0.016650142,-0.014726503,0.025604531,-0.021542037,-0.004089688,-0.044115417,0.020481883,0.007191264,0.02117103,0.016286876,0.0057632187,-0.030785,-0.026171686,0.004550318,-0.002470153,-0.012834399,-0.016944123,0.011712212,0.019181095,-0.048347708,0.03376784,-0.0032869608,0.003227429,-0.003307836,-0.03842209,-0.02030733,0.0072936253,0.008275929,-0.0038129473,0.006662956,5.108638E-4,0.02514524,-0.06561553,-0.02737613,-0.0075002816,-0.00977871,0.008090096,-0.016439455,-0.042639505,-0.04584564,-0.0060852882,-0.010588951,0.0010204185,-0.015795814,-0.016987266,-0.017434822,-0.047503985,-0.022787007,0.030189952,0.02216643,-0.022571122,0.006631617,-9.4200973E-4,-0.02110066,-0.01393558,0.019753367,-0.0024403941,0.007492939,0.010292473,0.033892654,0.020431064,2.7236907E-4,-0.049974725,-0.014788341,-0.0152976075,0.012492951,-0.010898642,-0.013227006,0.042150334,0.00391266,0.004083413,-0.011531445,0.011223181,-0.016044823,-0.021705948,-0.048691522,0.017360924,-0.03590659,-0.0063495645,-0.018703181,0.02411073,0.028439062,0.023421481,0.02927423,0.04265585,0.04252935,-0.0054632667,0.021800771,-0.008491276,0.023500508,-0.02305413,-1.19809716E-4,-0.05124756,-0.039268374,0.0040453603,0.015074989,-0.021333458,0.06168787,-0.00553488,0.03324899,-0.011755387,-0.02922123,-0.029154936,-0.007740291,-0.017879983,0.004852835,8.0813287E-4,0.049739007,-0.009202462,-0.01341185,0.018110914,7.636392E-4,0.042941872,-0.026595995,-0.009746392,-0.006912334,0.0115444325,-0.0018558993,0.014817685,0.0082888175,-0.007382212,0.033288136,0.011499318,-0.019600403,-0.011314329,-0.005860306,-0.029195223,-0.0062685963,0.010409848,0.008325058,0.014334556,0.010291462,0.021452066,-0.034034107,-0.049522746,0.0573059,2.363822E-4,0.010296092,0.03395466,0.03398866,-0.024273818,0.029240664,0.043988094,-0.010473252,0.025986891,0.01827778,-0.014100314,0.049628813,-0.0065952344,-0.011954908,-0.0024885365,-0.018849352,-0.029149204,-8.3355285E-4,-0.010235755,-0.028065499,-0.024536477,-0.011652977,3.050669E-4,-0.005279515,-0.004594109,-0.012401805,0.036739483,-0.0060019945,0.0010717936,0.05459036,0.025080722,-0.010849238,-5.5179663E-4,0.009794187,-0.011330473,8.2392653E-4,0.0104202395,-0.03790639,0.021216206,-0.015941454,0.030521888,0.021987053,0.006944646,-0.040046014,-0.018460117,-0.011702303,0.02426015,-0.010513847,-0.040754557,0.011574961,-0.0020334644,-0.029419394,-0.018846134,0.022650331,0.0026670673,0.0027510845,0.009645723,0.03588732,0.022347583,0.04370102,0.015004757,0.033122886,0.0065444657,-0.06201774,-0.006115251,-0.0073235044,-0.00892582,0.015069821,0.059462905,-0.015271412,0.011384896,0.005552976,0.010421146,0.012059199,0.06776953,0.0045202347,-0.035646755,-0.01591616,-0.018734956,0.02627652,-0.02529182,0.021677608,-0.003073378,-0.07704349,-0.042533603,-0.014978905,-0.02025507,-0.0133806495,0.010630204,-0.00994241,0.023922708,0.060765095,0.06610737,2.7844618E-4,-0.0024701024,0.018456323,-0.009405476,-0.019481113,-0.026263632,-0.0179836,-0.044223156,0.01417303,-0.004312645,0.06894223,5.120661E-4,0.017003087,-0.053371496,-0.02216818,-0.095885456,0.009612818,-0.016303746,-0.028880863,-0.015754564,0.0056394213,-0.07444534,-0.033266988,0.006171467,0.03377871,0.0018226502,0.011998649,0.003654531,-0.0060937423,0.03211934,0.04706914,0.008647589,0.044451725,-0.019848334,0.005012655,0.0063788416,-0.0011065918,-0.005436531,-0.0024466387,0.00521939,0.013161067,0.026088547,0.026278822,-0.0022822102,-0.03166597,0.009227336,0.0077010407,0.023725986,0.00924921,-0.021206558,-0.0022646263,-0.031105699,0.011651439,-0.039566707,-0.004174276,0.047922857,0.012152512,-0.013452181,-0.004303145,-0.022972124,-0.04038673,-0.017937252,-0.013496336,0.02234291,-0.005046699,0.10702754,0.014336809,0.013147355,-0.0014463668,-0.013824586,-0.012700405,0.036243863,0.0038729273,-0.057023913,-0.046826795,0.017882265,0.015678475,-0.021947205,0.0071065743,0.008027549,0.0119136395,-0.0058616153,-0.02206894,0.0012676921,-0.0055833235,-0.048675157,-0.025911719,0.039898463,0.02348459,0.049088314,0.020959534,0.00747388,-0.013797274,0.020395325,0.029219812,0.016532358,-0.021766456,0.03677316,0.05793027,-0.012930052,-0.0070918393,-0.0028130752,-0.04543944,-0.024643654,-0.0039012623,0.002884889,0.04552848,-0.006821123,0.0123996,0.03877125,0.0016253468,0.05063086,-0.019812664,-3.124627E-4,5.8241264E-4,-0.0015243586,-0.021279253,0.0051183356,-0.044304136,-0.02530377,-0.00864473,-0.008075254,-0.010326094,-0.005606972,0.03634543,0.005592264,-0.0038643107,0.015821354,0.0101335,-0.029724821,0.029195251,-0.005243966,-0.011825944,0.0071554678,-0.030945096,-0.012376742,0.008707618,0.051515494,-0.029635586,0.02308313,-0.024038574,-6.3045236E-4,-0.026122145,0.015646402,-0.010864386,0.013911439,0.019578166,0.020332629,0.0127959885,-0.032480553,0.06553239,0.024868414,0.011455229,-0.039614193,0.02975751,-0.011852479,0.007440266,0.0011759286,-0.009004016,0.021605758,-0.025099725,-0.018478256,-0.055595577,0.014472115,-0.009658225,-0.0065089166,0.020613352,0.018411878,0.056517705,-6.053995E-4,-0.0062812385,5.1438157E-4,0.016712192,0.0062159346,-0.0087655,0.05399468,-0.0452976,-0.0077455933,0.040048014,7.688531E-4,-0.025993994,-0.0024877908,0.0022109714,-0.013052232,-0.019896915,0.0028258173,-0.06327115,-0.009607183,-0.022728493,0.043174017,-0.008628771,0.031514037,0.03307769,-0.010260407,-0.011458944,-0.008395597],"expectedTop":["1F96C","1F957","1F953"]},{"query":"feeling sick","vector":[-0.02871558,-0.056136232,0.009754225,-0.070136845,0.041570436,0.011469248,0.066318415,-0.038374618,0.005857757,-0.08997732,-0.006495695,0.008957287,-0.0153896855,0.051147647,0.05297592,0.043724813,0.097177945,0.018195346,0.008484896,-0.024695864,0.0033583622,0.011582527,0.016125333,0.053362094,0.08160148,-0.03399027,0.009523299,0.012938881,-0.10849655,1.0644631E-5,0.09563966,-0.0032065313,0.045793578,-0.05043143,0.0014582026,0.03705423,-0.06569287,0.03601634,-0.063784346,-0.07996312,-0.006836795,-0.06145802,-0.011664084,0.009336883,0.009840068,-0.010706579,-0.046812367,0.0344565,-0.108945444,-0.048111446,0.008366062,-0.009425257,0.059027005,-0.02966429,0.061396997,-4.4917985E-4,-0.020920493,0.043501597,-0.0046486007,-0.0076399352,0.042242497,0.018037492,-0.00660156,-0.04065207,0.041268036,0.028801529,0.030978158,0.04615089,-0.01132766,-0.08848102,-0.04889287,0.08854083,-0.011499931,-0.053911082,0.082003236,-0.0601437,-0.0366962,-0.07414031,-0.005609705,0.03920702,-0.0042649335,0.026326107,-0.03585257,0.01481613,-9.529397E-4,-0.077099964,-0.034371525,0.06339827,0.014154207,-0.002161314,-0.038762774,0.044815846,0.0650237,-0.0050609084,0.0027599714,-0.04635834,0.081861876,-0.04638399,-0.0012241375,-0.065423526,0.016568122,-0.07688664,8.346223E-4,-0.04339717,-0.030086685,-0.022857513,0.03535792,-0.041976035,-0.012862058,-0.0331675,-0.07964023,0.015363917,-0.018991746,-0.09883768,-0.04483924,0.031656682,0.039062373,-0.05696652,-0.03305068,-0.092152126,0.08741176,-0.038447965,0.02541014,-0.041219566,-0.016885024,0.024154337,-0.017552808,0.0072326288,0.030349584,0.017896457,0.019458894,0.023853129,-0.10613646,-0.0048406944,-0.029239679,0.05216515,-0.002019622,0.022761453,0.004456313,0.035932183,0.028235033,-0.047065023,-0.028898755,-3.9434654E-4,0.049595628,0.007601671,-0.10874166,-0.003680633,-0.055830516,-0.013307436,-0.027524924,0.04235121,-0.016544985,-0.016620891,-0.05999327,0.02610343,-0.037125994,-0.030678993,0.04426521,0.035224576,0.027056973,0.055897858,0.028555848,0.011910154,-0.026344534,-0.0148685705,-0.012348298,-0.0074444567,-0.0104126325,-0.01700316,-0.06091781,-0.050888117,0.023109699,-0.03657705,0.008930757,0.00736764,0.016873248,-0.0028896797,0.1203521,0.018430078,-0.029438902,0.109276585,-0.0010108154,0.055332307,-0.011733601,-0.05704884,0.025309034,0.018302772,0.04400879,0.025923695,-0.087354995,-0.027049905,0.09582214,-0.023737494,0.005919169,0.03832674,-0.046795983,0.017715538,0.03278783,-0.045323323,-0.09726333,-0.045503132,0.064269334,0.02889401,-0.08143723,0.05040556,0.05483275,-0.030363925,-0.0022422893,0.11056871,0.023680707,-0.06047489,0.086955026,0.08730629,-0.06295021,-0.06748054,0.01938352,-0.019849993,0.033065107,-0.06055432,0.03403592,-0.029855864,-0.029650908,0.05666303,-0.040279783,0.09243613,0.10478931,-0.07709104,0.015548422,-0.094050854,-0.030418718,0.03871299,-0.05226117,-0.039873708,-0.08054453,-0.008931553,-0.03947044,-0.036490627,-0.03310085,-0.13280356,0.031770352,0.077725604,-0.044376478,-0.058071773,-0.02261017,-0.01785945,0.076460496,-0.068493076,-0.03902277,-0.016874248,0.043046806,0.16543728,0.01583526,-0.006577141,-0.04952751,-0.10046413,-0.0034536773,0.026936706,-0.0024832494,0.049354736,-0.013617534,0.024123896,0.012298727,-0.007868108,0.011583781,0.04514427,-0.008807201,0.003964734,0.021903208,-0.020766422,0.035348393,0.0150201805,0.035566036,-0.01071848,-0.021542044,0.006909101,0.0039485195,0.04708985,-0.04835003,-0.033906434,0.015364515,0.014928921,0.027406987,0.010573091,0.003354672,-0.0104323225,0.05668851,-0.0027357421,0.027329504,0.018896883,-0.03294888,0.01469187,0.007071262,-0.014418615,0.01657246,-0.033669505,7.146074E-4,-0.002171843,-0.019543268,3.3363298E-4,0.04993607,0.0014628095,-0.031189054,0.04865679,-0.024671715,0.036341228,-0.024688546,-0.053008575,-0.077083394,-0.037571184,0.061843604,0.011123901,0.014801245,-0.015103868,-0.0056130653,4.1744014E-4,-0.0243314,0.02277096,-0.03979599,0.03198132,-0.021060035,-0.03304179,0.051934622,0.010365877,-0.011542374,0.015637951,0.021578517,-0.029668514,0.025184648,0.030830529,-0.029887939,-0.0068818782,-0.014019574,0.03379756,0.028316272,0.04929651,0.0021802823,0.026185691,0.023735277,0.028759241,-0.012186423,0.03280786,0.015330914,0.009562606,-0.012598433,-0.021771042,-0.051444247,0.0063997633,-0.0280276,0.0035421217,0.010167354,0.028322453,0.0044418955,0.013681752,-0.035697408,-0.020670572,0.016155874,0.045639187,0.036097027,-0.017868364,0.0046545686,0.016628314,-0.009219477,0.019378403,0.010353454,0.004234831,-0.0020883773,0.011826443,-0.015367572,-0.01652217,0.0012850872,-0.028237594,-0.0055561443,-0.011750735,0.07034505,-0.025211053,0.015799977,0.009103527,0.010168275,0.020818356,0.03238386,-0.040343802,-0.0367967,0.007963177,-0.019038476,-0.037259784,-0.018888652,-0.002306179,-0.019723443,-0.028575554,-0.042742163,0.016125942,0.03975963,-0.009061137,-0.03425824,-0.0063120266,0.029237777,5.522263E-4,0.02161678,-0.010014997,-0.008180838,-0.007997511,-0.015005876,0.030851638,0.00470501,0.0040378706,0.017201899,-0.010618943,0.03248528,0.0011897362,-0.013406726,-0.01378912,0.02283256,0.015845213,-0.01759475,0.026356697,0.024560913,0.026229892,-0.060649436,0.02316285,0.021126151,-0.0067345584,-0.0046470063,0.0129146995,0.02060629,0.02775278,0.0044110958,0.055866778,0.0041726236,3.9512644E-4,-0.007458183,-0.013425022,-0.0036005694,-0.016319431,0.021112429,-0.037973844,0.0010982184,0.023581272,0.043535955,-0.0110680275,0.033279832,-0.025313418,0.048939914,0.01548603,-0.014578245,0.017330289,0.06173577,0.02724389,0.0010590331,-0.0025461032,-0.012283236,-0.007256023,-0.0013515677,0.0334152,0.038807053,0.05604103,-0.0140816225,0.026154282,-0.0047614095,0.03324894,-0.012119039,-0.026821397,-0.054440126,0.030490259,0.0016992413,-0.030706935,-0.024182515,0.0028727462,-0.025533581,-0.03865157,0.0042939144,-0.056033738,2.9801013E-4,-0.012431856,0.0322415,0.004184278,-0.022201749,-0.033832513,-8.578353E-4,0.008101217,0.047952093,0.022348179,0.029382546,0.038583543,-0.016492372,-0.017692564,0.020784669,0.040415913,-0.024119277,-6.6911255E-4,0.033138245,-0.0067416513,0.03296551,0.011115896,0.0037114783,-0.024993958,9.377319E-5,0.0072645224,0.010937582,-0.01149042,-0.023878703,-0.023004184,-0.019049443,-0.045503393,0.014058104,0.023551824,-0.026946131,0.0036270523,0.033844598,0.029955462,0.0035320015,0.0042856587,0.012984052,0.016841365,-0.012687717,-0.009605294,-0.03237916,0.006781051,0.011358547,0.041577138,-0.02305707,0.012927879,0.017049935,0.027463857,-0.019354396,0.025604922,0.006791602,0.016331421,-0.0017378756,0.011867918,0.009634124,-0.0014399769,-0.020944066,-0.008436653,-6.6296867E-4,-0.0067836586,0.022264915,0.055059344,0.039706912,0.033532348,0.02244268,0.03604702,-0.07067285,0.027443107,-0.00181168,-0.0037428173,-0.005693182,0.012563299,0.022622546,-0.022702688,0.010702444,-0.032244336,0.03471888,0.042766415,0.010786413,-0.01729031,0.012319853,0.0083111925,-0.001784867,0.014569765,0.04047685,-0.02156435,-0.0042178268,-0.020830564,0.0082475105,0.006880338,-0.0060458,-0.013303259,0.0022637115,-0.019697046,0.06497029,0.035361882,0.00419575,-0.0044189333,0.028387148,9.6536183E-4,-0.020645984,-0.017166536,0.013423532,-0.053274695,0.017009314,0.004585721,0.05181735,0.037729796,0.022238795,-0.034242686,-0.018429162,-0.11486315,-0.0017802614,0.03886312,-0.004346855,-0.013988508,0.033083085,-0.054295037,0.044072323,0.01357085,-0.015185512,0.025622243,-0.004459758,-0.025130874,0.013077392,0.040706217,0.0142102735,0.0014364631,0.033775106,0.02560795,0.0027280613,8.8032766E-4,-0.03597661,-0.015750645,-0.0100258775,0.001948957,0.041914206,0.0010187359,-0.014734627,-0.03450442,-0.017033463,0.016177239,0.0072764084,0.03519005,-0.02221414,-0.041928455,-0.018603971,-0.027802773,0.017310528,-0.0733018,-0.038166583,-0.061249115,0.02593581,-0.012198536,-7.119128E-4,-0.027929291,0.016979117,-0.047201574,-0.025303436,-0.0067002843,-0.013697407,0.10799454,0.02204277,-0.04551313,0.028585706,0.019136457,-0.015111462,0.007446836,0.026984876,-0.035121255,-0.031221876,0.039236154,0.021232232,-0.023848763,-0.011031483,0.03901097,-0.028093932,-0.037311215,-0.011545545,0.011283376,0.01928337,-0.03577534,0.006110952,-0.01730974,0.033338547,-0.004760103,0.019177193,-7.004025E-4,0.008164839,0.03361837,-0.016088113,0.03063642,-0.023410816,-0.025563601,0.027566232,0.0041305292,-0.018644292,-0.023333786,0.005206238,-0.024403583,0.02439696,-0.004901367,0.045757435,-0.025323689,-0.006714824,0.017024804,0.02272323,0.018854367,0.015375232,-0.044152603,-0.034858722,-0.006430224,-0.007571185,-0.010200135,-0.037708405,-0.0025461072,0.0051007094,-0.004222537,-0.014842329,0.0028021478,0.008677303,0.034156434,0.021705037,0.037540622,0.015238966,-0.0012777329,0.015020284,0.0017519513,-0.013731784,0.009282791,-0.039816335,0.0031434745,0.020555895,0.041357223,-0.022525866,-0.013158772,-0.025936393,0.031846352,-0.01102464,0.03505599,-0.030357875,0.038094547,0.057214823,0.044212166,0.051334474,0.040758114,0.058697358,0.023172544,0.057264384,-0.016193654,0.03351919,-0.0071996446,-0.010731948,0.018522216,0.0050465283,-3.7134482E-4,-0.0058282386,-0.026308844,-0.037237898,-0.029448772,0.041132018,-0.04744917,-0.008286669,0.02886925,0.043355238,0.006296277,0.0037864496,3.2968185E-4,-0.019592818,-0.013882602,0.030511528,-0.0103627015,-0.01845749,0.008225731,0.026560698,-0.004913351,-0.0037014957,-0.0056664892,-0.023863817,-0.00878642,-0.012151925,0.026179109,-0.041930035,-0.023880403,-0.03061455,-2.8640858E-4,0.03343242,0.006185333,0.0155359935,0.02069003,-0.0022995884,0.051288914],"expectedTop":["1F922","1F92E","1F927"]},{"query":"rock band","vector":[-0.012390657,0.013517345,-0.023544889,-0.04623684,0.053279214,0.037156615,0.044788077,0.037844427,-0.016494296,-0.011535259,-0.103428796,0.074854314,-0.023974342,0.007929548,-0.015894903,0.11882872,0.031073464,0.02746281,-0.0035793725,-0.05281356,-0.004690447,0.0694257,0.026127668,0.054296732,0.03539469,-0.009685262,0.010996138,-0.047175962,-0.021919422,0.060856875,0.055724718,0.036414057,0.011384833,0.0036850714,0.034159046,0.008941249,0.039152276,0.017352173,-0.028658085,-0.11143548,0.04156348,-0.05658162,-0.030585716,0.013899636,-0.026896002,0.0075162984,-0.0104902815,0.102331765,-0.10248795,-0.03866794,7.2882307E-4,0.0061962097,0.07874264,-0.009385353,0.08061173,-0.10607016,0.008689205,0.042914286,-0.0398362,0.012440652,0.040780623,0.034297064,0.005168554,-0.094257765,0.025849968,0.07631224,0.06625352,-0.013264231,-0.053093236,-0.08421605,-0.009673742,0.0676547,-0.063695714,-0.070916444,0.022084422,-0.10154962,-0.053271886,0.0063732243,-0.022164097,0.042259805,0.022362607,0.040151082,0.07501085,0.07756214,-0.05504779,-0.06175986,-4.7644787E-4,-0.034945715,-0.07904515,0.021232096,-0.113351084,-0.06360195,0.02408557,0.05616695,0.018511184,-0.048583146,0.013190851,-0.007960615,0.05586495,-0.04305494,0.035625283,-0.0795024,-0.0079651605,-0.024635134,-0.107273385,-0.015053406,-0.081340574,0.044037122,-0.023125712,-0.05547649,-0.026383232,-0.022173626,0.014371207,-0.084150694,0.028855413,0.06259865,-4.1135214E-4,-0.04792765,-0.028252942,1.1599832E-4,0.029108752,-0.0037644922,-0.015812024,-0.015207376,-0.009475686,0.034582272,-0.021283174,0.018769683,-0.017614964,-0.042060763,-0.0037717444,0.043938518,-0.048729572,0.06959051,-0.03629858,-0.0481699,0.016892899,7.2664843E-4,-0.041750662,0.07664791,0.034712277,0.015778113,-0.055318214,-0.0037029025,0.04436564,-0.095618874,0.008710853,-0.0051013716,-0.034696754,-0.03445677,-0.008324354,-0.034112256,-0.061760463,-0.00889805,-0.10433001,0.051598575,0.010569864,-0.015122616,0.022040002,-0.002838348,-0.0020860692,0.0045789042,0.01822405,-0.0018419169,-0.016494766,0.039091155,0.037522495,6.1662623E-4,0.007847403,-0.07564569,-0.09614695,-0.018532468,-0.009577915,0.05311442,-0.0038836857,0.0139466645,0.038149897,0.027241148,0.07600347,-0.002249551,-0.02568616,0.05511768,0.10012927,0.049393028,0.01150656,0.0035094845,0.06658984,0.1052315,0.09081355,0.0031510212,-0.021939967,-0.05740046,0.07041863,-0.06997034,0.098132595,-0.03039084,-0.031208174,-0.009158222,0.0680422,-0.0023703654,-0.111611456,-0.04223082,0.07297682,-0.02401352,-0.10125623,0.023223046,0.08358003,-0.052517366,0.037388217,-0.00803517,-0.0548226,-2.7247972E-4,0.08307253,0.057180576,0.03239764,-0.03320685,-0.007003216,-0.030171778,0.02011378,-0.056301147,0.0058521605,0.032196984,-0.034031406,0.029655598,0.011795349,0.13603222,0.06408021,0.03954711,-0.0049027232,-0.017248755,-0.018674994,0.036640458,0.02144733,-0.022303669,0.025442138,-0.015255123,0.021545632,-0.06807415,0.0054859873,-0.05815723,0.11146355,0.095123485,-0.051634856,0.07342422,0.04363278,-0.0036746676,0.070367575,-0.0957655,-0.010922055,0.039177198,-0.02270094,0.09289605,0.05405735,0.031170638,-0.016394468,0.0072354767,-0.002653956,-0.006230287,-0.0066567827,0.035588253,-0.03331131,-0.018354278,0.03848081,-0.021491647,-0.048624735,0.025698151,0.03391164,0.021271456,-0.013225182,0.0051049585,0.0058474517,0.025017848,0.026844498,0.03778453,-0.036215305,0.055811923,-0.0028392733,0.015888013,-0.022594366,0.0056991256,0.033356488,0.028069526,0.031329557,0.0026193997,0.004466514,0.02575247,0.029503858,-0.010377852,0.0017769614,0.02028216,-0.04286394,-0.014184489,-0.05080529,-0.0120025845,0.0023832654,-0.016736753,0.016302595,-0.046376266,-0.06304312,-0.010430062,1.3554168E-4,-0.01580349,-0.03048913,0.014118976,0.10048343,-0.021017073,0.029511752,-0.033243883,-0.078317635,-0.016818533,0.018176122,0.02516125,0.020351363,-0.029631145,0.022145806,-0.0049148193,-0.042134136,-0.008978752,-0.053096574,0.029776188,-0.02398885,-0.033460848,0.020431692,-0.009303119,0.02156444,-0.026929712,0.0271227,0.0026668403,-0.021506092,0.023062836,-0.0072492696,-0.0057943873,-0.015566816,0.028639434,0.05884572,0.0126685705,-9.761552E-4,-0.019974785,-0.0061820243,0.0022036247,-0.0126638785,-0.010884913,0.021404404,-0.0077996906,0.008612839,-0.009305237,0.0038627095,-0.008614393,-0.038969737,0.018315583,0.0076345075,0.011402302,-0.018914314,0.019576596,-0.0048235534,0.026140692,8.918164E-4,0.03677473,8.689486E-4,-0.016486662,0.017861353,0.020481605,0.036635067,-0.024930876,0.0026883811,0.00769881,0.009011091,0.028195944,-0.010445318,0.0018644551,-0.031496752,-0.039036375,0.014938591,-0.054923233,0.01181665,-0.033819627,-0.014894067,0.0017584509,0.07458764,-0.031987723,-0.002021085,-0.044074588,-0.038695257,-0.018657928,0.0030844773,-0.021092232,-0.07382106,0.01759472,-0.0061116284,-0.013352094,-0.014197023,0.039233167,0.029892359,0.024042042,-0.010020974,0.011300068,-0.017362764,-0.017655162,0.014893516,-0.02044196,-0.024653027,-0.0056166598,-0.0015465989,0.018407315,0.015900807,-0.03308681,-0.012355413,-0.043351606,0.048176393,-0.0027996174,0.023110557,0.008082805,-0.008837123,0.02825683,0.006666953,-0.03018847,-0.013282139,0.025478048,4.4325565E-4,-0.03456595,0.02108438,-0.031455033,0.006364295,6.678738E-4,0.0036077828,-0.026997862,0.0025569403,0.028305987,0.0069536106,-9.1416313E-4,0.03938562,-0.0142653575,-0.035986602,0.0011632098,0.007542369,-0.008670128,0.026600247,-2.3246146E-4,0.0033650408,-0.052316897,0.038659133,-0.011537666,0.008114647,0.004288967,-0.014361825,-0.022766693,0.034876898,-0.0144020105,0.029475488,-0.0064549507,0.010326741,0.0047583934,0.015196661,0.015986076,0.06251701,-0.012956537,-0.012729015,0.027959608,0.019820679,-0.0011811331,0.030130748,-0.021926787,0.050068073,0.05152819,7.430123E-4,-0.0246409,-0.014293078,0.044998772,-0.0072328406,-0.030142851,-0.003443403,-0.0125163365,0.012507779,-0.024361968,0.004422344,-0.014994742,0.039454076,-0.01239575,0.014689694,-0.0027788347,-0.032564264,0.025981447,7.184855E-4,0.013775507,-0.002199794,0.045778334,0.028282907,0.020258151,-0.003750624,0.032220356,0.02710247,0.04167282,-0.013242685,0.02822482,-0.07058358,-0.0419678,-0.0055445265,0.005423962,-0.026502918,-0.038169004,-0.025165038,-0.07209038,0.0020609435,-0.010404664,-0.0069235326,0.017618665,-0.019127058,-0.048206665,0.032500207,0.012153664,0.037385177,-0.0145863425,-0.005772955,0.011582798,-0.018777022,0.0014714745,-0.035162322,0.020307131,-0.011992293,0.008466869,-0.03492526,-0.049034987,-0.03621055,-0.024300383,0.01568273,-0.01124985,3.4239515E-6,0.009961401,-0.022485645,0.026823284,-0.012337199,0.006829382,0.0010287095,-0.0059086047,-0.009829804,0.03559883,0.03746141,0.045549892,0.048863232,0.06278348,-0.028394185,-0.0304781,-0.028585259,0.038289778,-0.028938437,-3.3748787E-4,3.089079E-4,0.028756823,-0.009513555,0.023224857,0.018157788,-0.026376355,0.013834978,0.043311547,0.036981422,-6.7508494E-4,-0.015160634,-0.010777408,0.028847719,-0.008124368,0.0013338096,-7.9611095E-4,-0.03485536,0.017352063,-0.003120767,0.0022753193,-0.0085640205,-0.029507248,-0.0104817785,-0.017573275,0.027223283,0.018995982,0.014068732,-0.016215712,0.02905126,-0.012535101,0.011257926,0.06452753,-0.01983311,-0.060964894,0.024151394,-0.0049088085,-0.0011718159,0.03046556,0.015567832,-0.054501668,-0.022771593,-0.076490894,0.015107793,0.044552494,0.016998142,0.0042516985,0.011289771,-0.042122733,0.04437298,-0.007946803,-0.024035592,0.004555591,3.0227756E-4,-0.025091084,-0.006821017,0.030008541,0.019208083,-0.008066111,0.04129044,-0.015175993,0.020394957,0.012115061,-0.027414229,0.010412108,-4.7068635E-4,0.0069184066,0.016539661,-0.013394051,-0.017857455,-0.023127088,-0.0460133,-0.030272262,-0.01397313,0.025983697,-0.03695703,-0.007943058,6.159386E-4,-0.025281537,-0.016931972,-0.05957568,2.3830026E-4,-0.008216262,-0.0016375327,-0.029206835,0.0116521465,-0.0029068124,-0.007647817,-0.0015271324,-0.0011450768,0.015440333,-0.007445217,0.02887614,0.05062023,-0.040018007,-0.0045013214,0.05036451,-0.050488953,-0.0048654675,0.024134539,0.02162717,-0.034047194,0.0019799522,-0.025875533,-0.024946433,-0.014495603,-0.03578717,-0.0013426701,-0.029260032,-0.040676307,9.1075106E-4,-0.0060202074,-0.006054606,0.011110663,0.0016299324,-0.031967938,0.041785624,0.04025988,0.018578831,-0.020907043,0.04475006,0.008289054,0.008125245,-0.005522892,0.0045208717,0.042935554,-0.011330366,-0.01167376,-0.029515525,-0.0013155381,-0.040103145,-0.003447084,0.0032143365,0.036947444,-0.0070382403,0.026193012,0.025542896,-0.018916005,0.010394382,-0.019948177,0.021896133,-0.010031632,-6.8866403E-4,-0.028922975,0.023102108,0.00510501,-0.03615828,0.006523553,-0.0014946464,0.008552364,0.06096332,-0.001970257,0.012497508,0.050090853,-0.014700961,-0.02122658,-0.0314681,-0.02328307,-0.026079772,-0.035495363,0.025158709,-0.014745473,0.015310742,0.025600037,0.058248773,0.005504523,-0.03532838,-0.017224045,-3.75177E-5,-0.009586072,-0.0054412163,-0.007958105,-0.0016559131,0.08115718,-0.0012897367,0.027951421,0.013603399,0.02626116,0.013834976,0.0016070162,-0.03273506,0.0076275,-0.0025633252,-0.0028601657,0.029376984,0.025347354,0.03308177,-0.056017045,0.011548905,-0.023847535,-0.0535025,0.003535107,0.018105945,0.048485763,0.023202162,0.031550147,-0.0068374802,0.022321906,-0.040765427,-0.0040207114,-0.015464247,0.010970489,0.034959514,-0.012637875,0.03004005,0.0597078,-0.013445838,-0.031478938,-0.0022079577,-0.023338398,0.0341763,-0.055334803,0.011160312,0.007697788,-0.0020418328,0.006436752,-0.022841547,-0.015433639,-0.0014711906,0.009252561,0.0057344823,0.033795297,-0.008279411],"expectedTop":["1F941","1F3B8","1F4FB"]},{"query":"cute pets","vector":[0.028929677,0.003825738,-0.06493506,0.008344362,0.058253497,0.06429811,0.06042013,0.047365084,-0.04137987,-0.03095531,-0.06124185,0.016775275,-0.020067034,-0.03782306,-0.031561445,0.07890958,0.05540032,0.10671164,-0.026047107,-0.061959885,-0.06464769,-0.08834675,-0.013939303,-0.05775191,-0.025832046,-0.020600386,0.04105191,0.06485221,-0.08743547,0.009208508,0.044897836,0.01329475,0.04647149,-0.024592653,0.0065806704,-0.020797925,-0.026253356,0.05403033,-0.060303878,-0.102868065,0.035378948,-0.036481857,-0.06421559,-0.006933605,-0.025914038,-0.069428675,-0.03493705,0.009582623,-0.03520889,-0.07988231,-0.007896218,0.028064871,0.08212707,0.01629525,0.035515707,0.07314923,-0.02965458,-0.023678282,-0.051923804,0.013868744,0.01444392,0.009822648,0.026683757,-0.002545046,0.054401625,0.045652878,0.05559746,-0.007480752,-0.02724321,-0.05082168,-0.0020056355,0.06982674,-0.07922832,-0.048780255,0.03772438,-0.0060241343,-0.048605867,-0.008384768,0.0723228,0.07453318,0.030263007,0.04622338,0.061247427,0.06836648,0.04469075,-0.024349166,0.026521865,-0.047292255,-0.07361344,0.009189962,-0.037026733,-0.04788816,-0.032608263,0.044440854,0.022645557,-0.0050110742,0.00491942,-0.121394075,0.06014069,-0.056106705,-0.07766667,-0.05261771,0.023708748,-0.051476084,-0.12102486,-0.045624834,-0.13328628,-0.049851682,-0.035282012,-0.039538972,-0.07776524,-0.024843618,0.06960914,-0.017431991,-0.010806385,0.058497895,0.02799392,0.0116313035,-0.04912294,-0.0012500607,0.060774572,-0.04037994,-0.002076107,-0.046989556,0.010783919,0.030343158,-0.04118428,0.01711221,2.7630143E-4,-0.016346661,0.034207627,0.04473894,-0.032520022,-0.015021063,0.0011061048,-0.03728593,-0.02707667,-0.018089907,-0.016087389,0.022174077,-0.028835483,0.027303915,-0.018287443,0.032349143,0.022247316,-0.02148074,-0.0030706665,-0.025690366,-0.072593555,-0.017167263,-0.0073929452,0.010233833,0.015846571,0.016822213,-0.029644392,0.020712899,0.013003957,0.028211227,0.022082461,-0.020616405,0.00416794,0.0019524408,-0.061596476,-0.04303071,-0.0031272592,-0.022256773,-0.008455339,0.06808232,0.0018980526,0.03216891,-0.008638237,-0.0025887915,0.11063812,0.0097407345,-0.012183011,0.026988085,0.10042499,-0.07716991,0.08140279,0.009227389,-0.031671885,0.06367418,0.025852585,0.042483345,0.056519292,0.05062869,0.107767135,0.056022055,-0.008590862,-0.07069402,-0.053412594,0.0015475003,0.057141993,-0.0043171137,0.086660065,0.036077987,-0.027110787,0.03295435,0.030860757,0.00305485,-0.06804496,-0.029875431,0.09707537,0.011499849,-0.10056253,-0.027082777,-0.058709066,-0.010090866,0.027441533,0.048531644,-0.06638761,-0.017467348,0.119645014,0.06966863,0.0268844,-0.031582918,-0.013678273,-0.0072365347,-0.03970513,-0.085612275,-0.024097031,0.06683119,-0.038771804,0.0048238253,0.016182723,0.09028628,0.13385238,-0.06060302,0.05908428,0.024131507,-0.014135381,0.0068867737,-0.026544914,-0.08238608,-0.0702611,-0.017368052,0.04107423,-0.084123224,0.07915909,-0.09230866,0.031280342,0.08109286,-0.035237905,0.040718026,-0.00851626,-0.06003413,0.023144023,-0.057829976,-0.06313707,0.008997855,0.028810278,0.13078332,0.045135006,0.012891454,0.0041932836,-0.04848175,-0.03455752,-0.029341869,-0.036209896,0.0392897,-0.017090186,0.012667217,0.033582326,-0.051879223,-0.015592038,-0.0053286646,0.02389496,-0.0067456546,-0.008646893,-0.0051156073,0.013535165,-0.003004753,0.027497496,0.001957803,0.019719861,0.029688636,0.004794821,0.021496857,-0.01945484,-0.034313746,0.0011808935,0.016599113,0.019670736,-0.022716956,0.013275904,-0.007556629,-0.0077978666,-0.039989747,-0.0359523,0.033702604,-0.02370634,-0.004905898,-0.028659943,0.022039639,-0.04498514,0.0020840275,0.006159381,0.044223268,-0.030499095,5.049231E-4,0.042374287,-0.0052213874,-0.0020586622,0.010690357,0.06172805,0.0048922407,0.006025374,-0.05394643,-0.076913506,-0.021599907,0.011876434,0.057571996,8.78564E-4,-0.021880727,0.049726605,-0.001093572,-0.057650197,0.036946867,-0.007568486,-0.012918694,-0.057940513,-2.899288E-5,0.035493653,-0.024873069,-0.030692553,0.017375812,0.02910682,0.02300551,0.002031955,0.039346494,-0.025441332,0.015627336,-0.012987707,-0.0029517752,0.06962956,0.008019207,-0.021031039,-0.045142736,0.016382542,0.021236742,-0.041097175,0.033532023,0.015917711,0.010486036,0.013789891,0.019554349,0.0144230705,0.00780966,-0.008435629,0.0015955992,-0.016696448,0.03227692,0.03231384,-0.029622203,-0.010149908,-0.019665102,0.009790483,-0.017176379,0.011966502,0.024258452,-0.001148011,0.0044453423,0.016714104,0.026308997,-0.038445484,-0.003919415,-0.018475592,-0.020388426,-0.036174983,-0.014100156,-0.017194945,-0.009651957,0.044928018,-0.020233704,-0.008111379,-0.03330987,0.010851048,0.021167196,0.02426973,-0.025223326,-0.046967838,-0.033831142,-0.014986496,0.014160934,0.014689084,-0.007224939,-0.04292596,-0.019566903,0.046173338,-0.043349564,-0.0299736,0.050414994,0.012802256,-0.0375483,-0.0014056696,-0.0040932456,-0.05525239,4.0987824E-4,0.03100116,-8.925989E-4,0.024006963,-0.020680448,0.008049981,0.010145373,0.029042609,-0.026007352,-0.028400065,-0.022534419,0.0477699,-0.012540054,-0.015698673,0.057920005,-0.010457634,-0.021296706,-0.0034422632,-0.014369857,0.008285418,0.017836679,-0.056677498,-0.045362186,-0.02170636,0.011265241,0.008242303,0.024863083,0.056091502,0.035377137,0.02813198,0.07016948,-0.0024280131,-0.0010024428,0.003459709,-0.0061806897,0.0010782143,-0.0112437755,-0.014875449,-0.022176746,-0.039740782,-0.029958157,0.04175547,-0.05567342,-2.2149137E-5,-0.008836149,0.018949604,0.022703862,-0.009855347,-0.029700123,0.014884876,-0.04275325,-0.016324965,0.013549122,-0.0062895035,-0.0017926164,-0.019258147,0.0100618545,0.06820212,0.017515697,0.003090897,0.021066682,-0.04910811,-0.009026635,-0.014339891,-0.005784143,-0.023361277,-0.01899879,0.020107163,0.01569521,-0.016313503,0.039715596,-0.01924398,0.03005872,-0.03677851,-0.028092656,-0.027729573,-0.035011142,0.042810127,-0.029248592,-0.0029410848,-0.00836333,0.056887615,0.017362045,0.021705551,0.04183428,0.016103083,-0.0072702006,0.009579486,0.05137042,-0.007564021,0.025366524,7.765823E-4,-0.014483411,0.021290088,-0.00860043,0.021301445,0.022842824,0.0047289976,0.004848913,0.0253288,0.022707317,-0.034435548,0.008747995,-3.778006E-4,0.040476818,0.02001973,0.021523783,2.4402789E-4,0.010937742,-0.0019060826,-0.021738665,0.02119332,0.025656668,0.0058157253,-0.016021447,-6.641124E-4,0.0373555,-0.0024028653,-0.019225549,0.020127637,-0.004122668,-0.0064463983,0.020071166,0.0051664934,-0.012386992,-0.022125125,-0.039674506,0.0037447342,-0.028735816,-0.017700234,-0.022384938,-0.028325155,-0.0193082,-2.9669376E-4,0.006157387,0.013327393,-0.010989134,0.011616936,0.05926041,0.023512559,-0.006884579,0.039022215,0.034871954,0.04129992,-0.0051811324,-0.052160922,-0.0010315528,-0.026192317,0.022273067,0.02405195,0.03463622,0.024053544,0.01818661,0.009452791,-0.002209467,-0.030993113,0.054516222,-0.0063985367,-0.03529936,0.00142714,-0.022105487,0.017244166,-0.0078322515,0.028358728,0.03130845,-0.05323963,-0.0013031422,-0.0025034782,0.027489008,0.018891571,-0.021306057,0.024379646,0.027285768,0.0075568454,0.05894507,0.0087758675,-0.001008817,0.054868795,0.0010598684,0.008094219,0.058063887,0.004742157,-0.016486913,0.060148396,-0.007957375,0.04413041,-0.020421317,0.02453485,-0.023019006,-0.018210094,-0.08846854,0.028372247,0.016834518,-9.600306E-4,0.045757942,-0.005917809,-0.029693978,0.006561785,-0.031285767,0.03413109,-0.02486361,0.05127393,0.01964357,0.0107186735,-0.0076136184,0.02273244,0.047729827,0.037206464,-0.022564277,-0.02431677,0.013253476,-0.0044071446,0.0068489853,-0.0020604723,-0.040849674,0.029628027,0.001397039,-0.0056820125,-0.024086801,-0.05563823,0.024127379,-0.025119422,0.00750194,-0.024222981,0.01613104,0.010551005,-0.040493343,-0.026450122,-0.046626106,0.009239497,-0.015794624,0.022528406,-0.01960218,-0.04501262,0.0052493303,-0.028427362,-0.023038592,-0.02294096,0.011539957,0.03106672,0.10538505,0.046285737,-0.005455894,0.032791056,-0.008458742,-0.014285005,0.005045599,0.02823079,0.0075946823,0.009597305,0.0056454027,-0.0014422778,-0.024816114,-0.010053161,-0.014194206,-0.017652992,-5.9066707E-4,-0.018393308,0.0072476934,-0.0028747674,-0.04038711,0.006083234,0.059409432,-0.01854276,0.036676638,0.033372153,-0.011194512,-0.0387211,-7.477837E-4,0.0056881043,-9.6085697E-4,0.008940237,0.062375188,0.00952442,0.022146104,0.005578748,0.02444453,0.027918477,-0.016307296,0.016397249,-0.009352582,0.03733338,0.025393901,0.03336303,0.030645348,-0.028271386,0.031195948,-0.0057826466,-0.01713768,-0.011791055,-0.012817529,-0.020296607,-0.011579024,-0.0048555112,-0.0013543165,0.02904434,-0.027591178,0.010006974,0.0013838185,0.022658255,-0.0074876016,0.00838637,-0.030192474,0.008669644,0.013865415,-0.014185543,-0.03680872,-0.033926904,0.018857908,-0.030032394,-0.0062646265,0.030656692,0.04618659,0.015695862,-0.031025248,-0.01914331,-0.032663938,0.016696772,0.003859098,-5.3143833E-4,0.03716944,0.04161657,0.019411312,-0.004539454,-0.0419213,0.032365605,-0.015610689,-0.022294147,-0.034380563,-0.0066214646,-0.018150326,0.04785161,-0.012360101,0.045505054,-0.002808165,-0.032231726,-0.016635213,-0.0034536102,-0.021270312,0.0059878365,-0.020522349,0.014348095,0.038864225,0.0070705716,0.015373781,0.008347121,-0.009756074,0.0025575296,0.024337059,0.057853263,0.0026761217,0.013047534,0.0023255148,0.03386326,-0.01819058,-0.035584524,-0.0123586785,0.0050195106,0.012062661,-0.007192114,-0.038380887,-0.0050348607,-0.013698452,-0.002109438,-0.012053067,-0.027590878,0.013445799,0.0208257,-0.0068281954,0.020890519,0.02392263],"expectedTop":["1F436","1F431","1F439"]},{"query":"beach holiday","vector":[-0.07963303,-0.054432843,0.0088440245,-0.046173517,0.10632806,0.006476846,0.04822017,0.009202922,-0.059158336,0.013503775,-0.11051782,-0.0048273914,-0.010817993,0.02095244,0.07934652,0.02279714,0.05384368,0.025100997,0.031510826,-0.0843342,-0.018508386,0.045542248,-0.0059847375,0.029747624,0.016597776,-0.074837334,0.031659592,-0.02309742,0.023084639,0.046912234,0.037360463,-0.0037370978,0.0032676854,0.061409358,0.0020064877,-0.041456208,-0.05055183,-0.04949355,-0.11934551,-0.053553544,0.01957413,-0.06591934,-0.05117445,-0.02401836,-0.021023804,0.02612791,-0.007333495,-0.014546551,-0.0776079,0.009107045,-0.06883084,0.06531773,0.041248523,0.029435078,0.091254,-0.03497126,0.010926916,-0.020998897,0.087071404,-0.019151567,0.075448096,0.009046103,0.013784912,0.033452574,0.061122123,0.09691525,-0.010018126,0.0295295,-0.012502718,-0.045636814,-0.032262325,0.04033568,-0.0413811,-0.04000595,0.05834409,-0.009032633,-0.031960256,-0.035947032,-0.009549116,-0.01313429,0.02323884,-0.07502406,0.113486364,0.011881484,-0.010220753,-0.07862505,-0.0033781158,-0.0758862,-0.08854248,-0.041306667,-0.018478056,0.046497356,0.07737058,-0.034647524,-0.015262106,0.029888537,-0.043117397,0.022593262,0.080193676,-0.09521497,-0.010714845,-0.03466572,-0.035119902,0.039070293,-0.1261388,-0.011371696,-0.039300602,-0.039689884,0.0064326106,-0.07718948,-0.036606543,-0.003954924,-0.003182753,-0.045759037,0.021399513,0.028065164,0.021625388,-0.0050756666,-0.07596701,-0.08108265,0.004327781,0.020463046,0.01726083,-0.029235022,-0.011305879,-0.015653472,-0.012732183,0.051815975,-0.022576915,1.3792525E-4,0.09884704,-0.10907239,-0.07311175,-0.0028116805,0.01612174,-0.023361713,-0.027987456,0.03337713,-0.023440728,0.02931552,-0.05830446,-0.06666777,-0.009008861,-0.01989914,0.071869425,-0.009532224,0.011511992,-0.06961984,-0.033645127,-0.048421793,0.049427386,0.061328307,-0.031701397,0.017805923,-0.030048696,0.041181926,0.025150726,0.026853804,0.010169079,-0.015587313,0.039008994,0.010959027,-0.013168868,-0.03444547,0.011606851,0.07342024,0.012416252,0.03490552,0.072367676,0.0038752551,-0.08449916,0.013267705,0.05828674,0.0027361128,-0.03483796,0.017379113,0.051670495,-0.037104923,0.081367455,0.048091263,-0.0010097134,0.02696709,-0.006732512,0.054973695,0.05270791,-0.033964723,0.024254693,0.0083385,0.026598543,0.086358786,-0.043215632,-0.074330375,0.0067286165,-0.093296,0.020848619,-0.053537592,-0.05199818,0.070340395,0.04178487,0.041029356,-0.11161826,0.06759396,5.825482E-4,-0.001986781,-0.09492133,0.031055318,0.08189937,0.009071463,0.034477104,0.09497614,-0.12585866,-0.022998286,0.111082606,0.073598325,-0.0031686185,-0.008542429,0.033492327,0.021206912,0.03971756,-0.06967173,-0.0021647993,0.06989947,-0.055552065,0.027233912,-0.0010035564,0.06888031,0.066047154,-0.041912053,0.0199556,0.04265776,-0.015382967,-0.00933048,-0.027838355,0.00927622,0.0047670277,0.09766282,0.012890634,-0.072778024,0.011146993,-0.038569197,0.0016566428,0.070479706,0.008273144,-0.015783489,-0.041178748,-0.014324176,0.046889365,-0.12048543,-0.023885874,0.036882363,0.049763538,0.09933825,-0.014621227,-0.006205573,-0.004288513,-0.0065337983,-0.0113373995,0.007416627,0.0061484124,0.022190267,-0.03563364,-0.021243172,0.0040753637,0.019873975,-0.005169146,0.0014989412,-0.008026937,0.013995218,0.030414816,0.05052528,-0.00509918,-0.0013714331,0.024300378,0.021418143,-0.025584998,0.02438528,0.014448477,0.029753389,-0.023699773,-0.022612924,0.0075278506,0.0026758625,-0.015925104,-0.011316588,0.011778744,-0.00977326,0.023906749,-0.010424964,-0.033783287,0.039466895,-0.04703805,-0.0074884193,0.017177694,-0.017571397,0.043238018,-0.0072386283,0.0024773565,0.021602862,-0.043211885,-0.01849101,-0.009126774,-0.0073762788,-0.018024737,0.035310052,0.05711443,0.034640897,-7.939272E-4,0.017621638,-0.07800536,-0.014563351,0.004810229,0.03102615,-0.04550205,-0.014952808,0.021567473,-0.020074617,-0.036131732,-0.009346289,-0.0037821974,-0.018751705,-0.03877962,-0.011960491,0.05670888,-0.024270592,0.016116032,-0.054316882,0.03197114,-0.03674988,0.0052751666,0.047705647,-0.013550077,-0.043300238,-0.0030855497,-0.002501199,0.018129077,-0.026292454,-0.020612916,0.026448723,1.1029971E-4,-0.0015600744,-0.009275617,0.03725176,0.021751553,0.020410549,-0.017346546,-0.007198916,-0.0023439063,-0.016707918,-0.04707258,0.0058725234,-0.021074936,0.043656755,0.025479548,0.008701275,0.035023328,-0.0027783313,0.022356939,4.3983333E-4,-4.2939902E-4,-0.019189037,-0.004255697,-0.025670197,0.013684734,0.029196052,-0.026288107,-0.026153406,0.009176024,-0.031286005,0.008845508,0.0138312485,-0.003620701,0.008007131,0.026872834,0.0190213,0.0162508,-0.023486257,-0.008638847,0.03147558,0.03355834,-0.029081227,0.0063609206,-0.03989714,-0.032822356,0.01428267,0.016758844,-0.054430515,-0.010737964,-0.0011794746,0.033890937,-0.039993983,-0.03961148,0.06516592,0.005789826,0.009687984,0.01386737,0.004733437,-0.00923727,-0.036026604,0.011583358,-0.007615892,0.024167346,0.0041413093,-0.02818811,0.043291036,-0.01671421,-0.036071718,0.014040047,-0.028030587,-0.011766261,-0.024555322,-0.014431116,-0.0028126463,-0.03084018,0.024778266,0.012096848,9.770384E-4,-0.057074323,0.012410996,-0.024001477,0.02972384,-0.016443174,0.0033346873,0.012732993,0.004090607,-6.4459105E-4,0.006744787,-0.007827299,0.01747642,0.018716112,-0.016741155,-0.024904916,0.038430855,0.02041102,-0.028670639,-0.023305891,-0.037157007,-0.040631376,0.031217894,-0.016515616,0.01948488,0.033297833,-0.025133716,0.034815945,-0.0016129185,-0.03750059,0.02143168,0.043693203,0.025858268,0.028198594,0.0071283686,0.0111885825,0.03274018,-0.03871655,0.015752036,-0.032968834,0.021233803,-0.04687745,0.009492276,-0.034713987,-0.027582224,-0.0063992226,-0.0032566101,0.012558592,0.032699443,0.014165535,-0.024815558,-0.008293237,0.010567448,0.0013901055,-0.040181834,-0.021663098,-0.018655311,-0.022530433,0.0026007046,-0.013960794,0.017836114,0.070698574,-1.8485148E-4,-0.036280975,0.020877298,0.0048234765,0.015450989,-0.0027281155,-0.002012245,-0.023554575,0.018198494,0.029890811,0.028464561,-0.013230595,0.007894402,0.021700215,-0.005275038,0.017207492,0.0065659587,-0.045180924,0.009904541,0.019133218,0.06658131,9.047835E-4,0.0051820683,0.017504869,-0.02193364,-0.01805626,-0.048472334,0.019786887,0.0021887734,-0.040513184,-0.020177428,0.013668566,0.034834344,0.0068713543,0.049337495,-0.024483468,-0.0039912765,-2.2519505E-5,-0.017559193,-0.009062891,0.045316122,-0.04829218,0.04839371,-0.0020716607,-0.019766774,-0.039044175,0.014924166,0.014176961,-0.05415623,-0.011935516,-0.020416824,-0.026812503,-7.404409E-4,-0.0029582707,-0.023996973,0.04079299,-0.0073047895,-0.008842825,1.2034181E-4,0.041024856,0.01364171,0.034831613,0.023647415,-0.0137273315,-3.424513E-4,-0.03689093,0.015454799,-0.047293294,-0.030315576,0.048397623,0.015962189,-0.022292446,9.6847885E-5,0.05476564,-0.02731805,-0.006631735,0.021898892,-0.0076504913,-0.019725839,-0.004507953,-0.0010081686,-0.029898988,-0.029154813,-0.0050037443,-0.013744115,-0.055818614,-0.006396947,-0.025458146,-0.011986988,2.141752E-4,0.032654177,0.039311055,0.0512203,0.025635373,0.04566192,5.770231E-4,-0.031039476,0.043929007,-0.0047437088,0.0062327106,0.065493435,0.006874208,-0.036954068,-0.02856795,1.9717492E-4,0.02080817,0.01849478,0.07019876,-0.0320534,0.050494477,-0.067704745,-0.011162943,0.016875045,0.011595276,0.012169263,-0.031155037,-0.061940324,0.02772592,0.029153693,0.022272123,0.0015970579,0.034842454,-0.018941477,-0.0049516526,-6.1077083E-4,0.0298142,-0.026698524,0.013776675,0.009501306,-0.035241444,0.0010250739,-0.017886063,0.07751888,-0.018195812,5.5260817E-4,-0.022477955,0.014732108,0.016669827,-0.04244514,0.002102467,0.019401478,0.01128637,0.063871495,-0.0060866163,-0.014999643,0.0065355524,0.004500682,0.020448271,-0.08493584,0.017482411,-0.035353433,0.017530855,-0.025979865,-0.001622798,-0.01910857,-0.008486585,-0.025757017,-0.02980255,0.01177618,0.014517931,0.0698788,-0.022598945,-0.06304089,-0.0024908516,0.017104235,-0.03899448,0.010387713,0.040681526,-0.026752247,-0.022488672,0.0090130735,0.0067122607,0.010441552,-0.010198026,-0.026763923,0.018037753,-0.038461775,-0.033633593,-0.026276506,0.061853327,-0.010886384,0.03136021,-0.0023108732,0.050298832,0.03704599,0.032166786,0.03205909,-0.03338262,0.014355752,0.033012472,0.021880781,-0.029516513,0.03392107,0.01580273,0.013144535,-0.029518984,-0.040019087,0.010673164,0.007505442,0.030771693,0.007885583,0.07152035,-0.0068496293,0.08934252,0.0073061995,-0.014717185,0.03688952,-0.019618215,0.007239382,0.001834603,-0.036625475,0.017192712,-0.0048527992,0.024248762,-0.028231375,0.0020400432,-0.051852874,0.00974908,0.023380287,-0.013632228,0.03456182,-1.22015495E-4,-0.0024471844,-0.002994698,0.02735264,-0.012906227,-0.008698391,-0.0102176005,0.010287441,-0.0076783826,-0.01730671,-0.030030629,4.0347892E-4,-0.013401679,-4.6155896E-5,0.004558358,0.02173677,0.028522592,-0.017443167,0.021699086,0.05280036,0.0030963316,0.07595889,0.020296533,0.013997492,0.03247566,0.022683354,-0.034675356,-0.0037208272,0.012456763,-0.007194763,-0.0056053037,0.013884621,0.022980679,-0.0087979175,0.005728454,0.00920431,-0.017274108,-0.035167206,0.0027386376,-0.045813765,0.012034591,0.007440761,0.03180931,0.028232526,0.011507818,-0.029093102,-0.038504865,0.0041697696,0.026249906,0.022935301,0.007548175,0.034350183,0.02711791,-0.018642453,-0.025566552,-0.02389443,-0.013585137,0.019161757,-0.008196181,-0.009390055,0.015590772,0.03979339,0.030820716,-0.011555034,0.010136462,0.024578834,0.004264863,0.040283103,0.035798367,0.01060102],"expectedTop":["1F3D6","1F3C4","1F6CF"]}]
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/App.kt b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/App.kt
new file mode 100644
index 0000000..8b3e659
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/App.kt
@@ -0,0 +1,13 @@
+package sk.ainet.samples.emoji
+
+import androidx.compose.runtime.Composable
+import sk.ainet.samples.emoji.ui.SearchScreen
+import sk.ainet.samples.emoji.vm.EmojiSearchViewModel
+import sk.ainet.ui.theme.SKaiNETTheme
+
+@Composable
+fun App(viewModel: EmojiSearchViewModel, skainetVersion: String) {
+ SKaiNETTheme {
+ SearchScreen(viewModel = viewModel, skainetVersion = skainetVersion)
+ }
+}
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/BuildInfo.kt b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/BuildInfo.kt
new file mode 100644
index 0000000..bc9fc0d
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/BuildInfo.kt
@@ -0,0 +1,5 @@
+package sk.ainet.samples.emoji
+
+/** Desktop/wasm display version โ keep in sync with `skainet` in gradle/libs.versions.toml.
+ * Android reads the real catalog value at build time via `BuildConfig.SKAINET_VERSION` instead. */
+const val SKAINET_VERSION = "0.59.0"
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/CatalogResources.kt b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/CatalogResources.kt
new file mode 100644
index 0000000..f8a22da
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/CatalogResources.kt
@@ -0,0 +1,32 @@
+package sk.ainet.samples.emoji
+
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import semanticemojifilter.composeapp.generated.resources.Res
+import sk.ainet.samples.emoji.domain.Calibration
+import sk.ainet.samples.emoji.domain.Catalog
+import sk.ainet.samples.emoji.engine.CatalogIndex
+import sk.ainet.samples.emoji.engine.EmbeddingEngine
+
+/** The bundled catalog, calibration and (optionally) precomputed embeddings. */
+@OptIn(ExperimentalResourceApi::class)
+object CatalogResources {
+ suspend fun catalog(): Catalog = Catalog.parse(Res.readBytes("files/catalog/catalog.json").decodeToString())
+
+ suspend fun calibration(): Calibration =
+ runCatching { Calibration.parse(Res.readBytes("files/catalog/calibration.json").decodeToString()) }.getOrDefault(Calibration.DEFAULT)
+
+ /**
+ * The index for [catalog]: the shipped `catalog-embeddings.bin` when its digest matches the
+ * catalog (the web build always takes this path), otherwise computed with [engine].
+ */
+ suspend fun index(catalog: Catalog, engine: EmbeddingEngine, forceCompute: Boolean = false, onProgress: (String) -> Unit): CatalogIndex {
+ if (!forceCompute && catalog.embeddingsSha256.isNotBlank()) {
+ val bytes = runCatching { Res.readBytes("files/catalog/catalog-embeddings.bin") }.getOrNull()
+ if (bytes != null && bytes.size == catalog.items.size * engine.dimensions * 4) {
+ onProgress("Using precomputed catalog embeddingsโฆ")
+ return CatalogIndex.fromBytes(catalog.items, bytes, engine.dimensions)
+ }
+ }
+ return CatalogIndex.build(catalog.items, engine) { done, total -> if (done % 10 == 0 || done == total) onProgress("Embedding catalogโฆ $done / $total") }
+ }
+}
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/DiagnosticsRow.kt b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/DiagnosticsRow.kt
new file mode 100644
index 0000000..0be6782
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/DiagnosticsRow.kt
@@ -0,0 +1,49 @@
+package sk.ainet.samples.emoji.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import sk.ainet.samples.emoji.verify.VerifierState
+import sk.ainet.samples.emoji.vm.SearchStats
+
+/** Timings and the verifier state, so the demo shows its numbers instead of claiming them. */
+@Composable
+fun DiagnosticsRow(stats: SearchStats, verifierState: VerifierState, kernel: String) {
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ AssistChip(onClick = {}, label = { Text(kernel) })
+ AssistChip(onClick = {}, label = { Text("load ${stats.modelLoadMs} ms ยท index ${stats.indexBuildMs} ms") })
+ if (stats.queries > 0) {
+ AssistChip(onClick = {}, label = { Text("query ${stats.lastQueryMs} ms ยท p50 ${stats.p50QueryMs} ms") })
+ }
+ VerifierChip(verifierState)
+ }
+}
+
+@Composable
+private fun VerifierChip(state: VerifierState) {
+ val label = when (state) {
+ is VerifierState.Disabled -> "System 2: off (${state.reason})"
+ VerifierState.Idle -> "System 2: Laya ready"
+ is VerifierState.Loading -> "System 2: ${state.message}"
+ is VerifierState.Busy -> "System 2: ${state.done}/${state.total}"
+ is VerifierState.Error -> "System 2: error"
+ }
+ AssistChip(
+ onClick = {},
+ label = { Text(label, color = if (state is VerifierState.Error) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface) },
+ leadingIcon = {
+ if (state is VerifierState.Loading || state is VerifierState.Busy) {
+ CircularProgressIndicator(Modifier.width(14.dp).height(14.dp), strokeWidth = 2.dp)
+ }
+ },
+ )
+}
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/EmojiGrid.kt b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/EmojiGrid.kt
new file mode 100644
index 0000000..c0ee5e2
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/EmojiGrid.kt
@@ -0,0 +1,117 @@
+package sk.ainet.samples.emoji.ui
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import sk.ainet.samples.emoji.domain.RankedItem
+import sk.ainet.samples.emoji.domain.Tier
+import sk.ainet.samples.emoji.verify.Verdict
+
+/**
+ * Two tiers: the items above the calibrated threshold first, with their confidence badge and,
+ * when the verifier has spoken, its probability next to it; everything else below in rank order.
+ */
+@Composable
+fun EmojiGrid(ranked: List, verdicts: Map, german: Boolean, queryActive: Boolean, modifier: Modifier = Modifier) {
+ val top = if (queryActive) ranked.filter { it.tier == Tier.TOP } else emptyList()
+ val rest = if (queryActive) ranked.filter { it.tier == Tier.REST } else ranked
+ LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 112.dp), modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ if (queryActive) {
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ SectionLabel(if (german) "Treffer (${top.size})" else "Matches (${top.size})")
+ }
+ if (top.isEmpty()) {
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ Text(if (german) "Nichts รผber der Schwelle โ die nรคchsten Kandidaten stehen unten." else "Nothing above the threshold; the closest candidates are below.", style = MaterialTheme.typography.bodySmall)
+ }
+ }
+ items(top, key = { it.item.id }) { EmojiCard(it, verdicts[it.item.id], german, highlighted = true, pending = queryActive && verdicts[it.item.id] == null) }
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ SectionLabel(if (german) "Alle anderen" else "Everything else")
+ }
+ }
+ items(rest, key = { it.item.id }) { EmojiCard(it, verdicts[it.item.id], german, highlighted = false, pending = false) }
+ }
+}
+
+@Composable
+private fun SectionLabel(text: String) {
+ Text(text, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 8.dp, bottom = 2.dp))
+}
+
+@Composable
+fun EmojiCard(ranked: RankedItem, verdict: Verdict?, german: Boolean, highlighted: Boolean, pending: Boolean) {
+ val container by animateColorAsState(
+ when {
+ verdict?.accepted == true -> MaterialTheme.colorScheme.primaryContainer
+ verdict?.accepted == false -> MaterialTheme.colorScheme.errorContainer
+ highlighted -> MaterialTheme.colorScheme.secondaryContainer
+ else -> MaterialTheme.colorScheme.surfaceVariant
+ },
+ )
+ Card(colors = CardDefaults.cardColors(containerColor = container)) {
+ Column(Modifier.padding(10.dp).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(ranked.item.emoji, fontSize = 34.sp)
+ Spacer(Modifier.height(4.dp))
+ Text(ranked.item.name(german), style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ if (highlighted || ranked.cosine != 0f) {
+ Spacer(Modifier.height(4.dp))
+ ConfidenceBadge(ranked, verdict, pending)
+ }
+ }
+ }
+}
+
+/** `cos 0.71 ยท Laya 0.12`; the vector score and the decision model side by side. */
+@Composable
+fun ConfidenceBadge(ranked: RankedItem, verdict: Verdict?, pending: Boolean) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ Text("${(ranked.confidence * 100).toInt()} %", style = MaterialTheme.typography.labelSmall)
+ Text("ยท", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ when {
+ verdict != null -> {
+ val text = verdict.probabilityTrue?.let { "Laya ${(it * 100).toInt()} %" + (if (verdict.accepted == null) " ?" else "") } ?: "Laya !"
+ val color = when (verdict.accepted) {
+ true -> Color(0xFF2E7D32)
+ false -> MaterialTheme.colorScheme.error
+ null -> MaterialTheme.colorScheme.onSurfaceVariant
+ }
+ Text(text, style = MaterialTheme.typography.labelSmall, color = color)
+ }
+ pending -> Box { CircularProgressIndicator(Modifier.width(10.dp).height(10.dp), strokeWidth = 1.5.dp) }
+ else -> Text("cos ${formatCosine(ranked.cosine)}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ }
+}
+
+/** Two decimals without `String.format`, which the wasm target does not have. */
+private fun formatCosine(value: Float): String {
+ val hundredths = (value * 100).toInt()
+ val sign = if (hundredths < 0) "-" else ""
+ val abs = kotlin.math.abs(hundredths)
+ return "$sign${abs / 100}.${(abs % 100).toString().padStart(2, '0')}"
+}
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/PlatformChips.kt b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/PlatformChips.kt
new file mode 100644
index 0000000..2bc7c48
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/PlatformChips.kt
@@ -0,0 +1,22 @@
+package sk.ainet.samples.emoji.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import sk.ainet.samples.emoji.platform.SamplePlatform
+import sk.ainet.samples.emoji.platform.currentSamplePlatform
+
+/** One chip per platform this sample runs on, the current one highlighted; informational only. */
+@Composable
+fun PlatformChips(modifier: Modifier = Modifier) {
+ Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ for (platform in SamplePlatform.entries) {
+ FilterChip(selected = platform == currentSamplePlatform, onClick = {}, label = { Text(platform.label) })
+ }
+ }
+}
diff --git a/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/SearchScreen.kt b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/SearchScreen.kt
new file mode 100644
index 0000000..38ff96e
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/commonMain/kotlin/sk/ainet/samples/emoji/ui/SearchScreen.kt
@@ -0,0 +1,99 @@
+package sk.ainet.samples.emoji.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import sk.ainet.samples.emoji.platform.kernelTierLabel
+import sk.ainet.samples.emoji.vm.EmojiSearchViewModel
+import sk.ainet.samples.emoji.vm.LoadState
+
+@Composable
+fun SearchScreen(viewModel: EmojiSearchViewModel, skainetVersion: String) {
+ val status by viewModel.status.collectAsStateWithLifecycle()
+ val query by viewModel.query.collectAsStateWithLifecycle()
+ val ranked by viewModel.ranked.collectAsStateWithLifecycle()
+ val verdicts by viewModel.verdicts.collectAsStateWithLifecycle()
+ val stats by viewModel.stats.collectAsStateWithLifecycle()
+ val verifierState by viewModel.verifier.state.collectAsStateWithLifecycle()
+ var german by remember { mutableStateOf(false) }
+
+ Scaffold { padding ->
+ Column(Modifier.fillMaxSize().padding(padding).padding(horizontal = 16.dp, vertical = 12.dp)) {
+ Header(skainetVersion, german, onLanguage = { german = it })
+ Spacer(Modifier.height(12.dp))
+
+ OutlinedTextField(
+ value = query,
+ onValueChange = { viewModel.query.value = it },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ enabled = status is LoadState.Ready,
+ label = { Text(if (german) "Beschreibe, was du meinstโฆ" else "Describe what you meanโฆ") },
+ placeholder = { Text(if (german) "gesundes Essen ยท mir ist schlecht ยท Rockband" else "healthy things to eat ยท feeling sick ยท rock band") },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ )
+ Spacer(Modifier.height(8.dp))
+
+ when (val s = status) {
+ is LoadState.Idle, is LoadState.Loading -> {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ CircularProgressIndicator(Modifier.width(18.dp).height(18.dp), strokeWidth = 2.dp)
+ Spacer(Modifier.width(8.dp))
+ Text((s as? LoadState.Loading)?.message ?: "Startingโฆ", style = MaterialTheme.typography.bodySmall)
+ }
+ LinearProgressIndicator(Modifier.fillMaxWidth().padding(vertical = 8.dp))
+ }
+ is LoadState.Failed -> Text("Model failed to load: ${s.message}", color = MaterialTheme.colorScheme.error)
+ LoadState.Ready -> {
+ DiagnosticsRow(stats = stats, verifierState = verifierState, kernel = kernelTierLabel())
+ Spacer(Modifier.height(8.dp))
+ EmojiGrid(ranked = ranked, verdicts = verdicts, german = german, queryActive = query.isNotBlank(), modifier = Modifier.fillMaxSize())
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Header(skainetVersion: String, german: Boolean, onLanguage: (Boolean) -> Unit) {
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
+ Column {
+ Text("Semantic Emoji Filter", style = MaterialTheme.typography.headlineSmall)
+ Text(
+ "On-device sentence embeddings rank the catalog as you type; a decision model double-checks the top hits. SKaiNET $skainetVersion",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ FilterChip(selected = !german, onClick = { onLanguage(false) }, label = { Text("EN") })
+ FilterChip(selected = german, onClick = { onLanguage(true) }, label = { Text("DE") })
+ }
+ }
+ Spacer(Modifier.height(8.dp))
+ PlatformChips()
+}
diff --git a/SemanticEmojiFilter/composeApp/src/jvmMain/kotlin/sk/ainet/samples/emoji/main.kt b/SemanticEmojiFilter/composeApp/src/jvmMain/kotlin/sk/ainet/samples/emoji/main.kt
new file mode 100644
index 0000000..567c3ec
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/jvmMain/kotlin/sk/ainet/samples/emoji/main.kt
@@ -0,0 +1,28 @@
+package sk.ainet.samples.emoji
+
+import androidx.compose.runtime.remember
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import androidx.compose.ui.window.rememberWindowState
+import sk.ainet.samples.emoji.engine.LeafEmbeddingEngine
+import sk.ainet.samples.emoji.model.DesktopLeafProvider
+import sk.ainet.samples.emoji.vm.EmojiSearchViewModel
+
+fun main() = application {
+ Window(onCloseRequest = ::exitApplication, title = "SKaiNET Semantic Emoji Filter", state = rememberWindowState(width = 1100.dp, height = 800.dp)) {
+ val viewModel = remember {
+ val calibration = kotlinx.coroutines.runBlocking { CatalogResources.calibration() }
+ EmojiSearchViewModel(
+ loadEngine = { onProgress ->
+ val source = DesktopLeafProvider().resolve(onProgress)
+ LeafEmbeddingEngine.load(source, onProgress = onProgress)
+ },
+ loadIndex = { engine, onProgress -> CatalogResources.index(CatalogResources.catalog(), engine, onProgress = onProgress) },
+ calibration = calibration,
+ verifier = createVerifier(),
+ )
+ }
+ App(viewModel = viewModel, skainetVersion = SKAINET_VERSION)
+ }
+}
diff --git a/SemanticEmojiFilter/composeApp/src/jvmNoVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt b/SemanticEmojiFilter/composeApp/src/jvmNoVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt
new file mode 100644
index 0000000..1c99b8a
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/jvmNoVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt
@@ -0,0 +1,7 @@
+package sk.ainet.samples.emoji
+
+import sk.ainet.samples.emoji.verify.NoopVerifier
+import sk.ainet.samples.emoji.verify.Verifier
+
+/** Built without `-PlayaVerifier=true`: the second stage is off. */
+fun createVerifier(): Verifier = NoopVerifier("build with -PlayaVerifier=true")
diff --git a/SemanticEmojiFilter/composeApp/src/jvmVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt b/SemanticEmojiFilter/composeApp/src/jvmVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt
new file mode 100644
index 0000000..c7a6c02
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/jvmVerifier/kotlin/sk/ainet/samples/emoji/VerifierFactory.kt
@@ -0,0 +1,8 @@
+package sk.ainet.samples.emoji
+
+import sk.ainet.samples.emoji.verify.Verifier
+import sk.ainet.samples.emoji.verifier.LayaVerifier
+import sk.ainet.samples.emoji.verifier.layaDesktopSource
+
+/** Built with `-PlayaVerifier=true`: Laya loads lazily on the first stable query. */
+fun createVerifier(): Verifier = LayaVerifier(layaDesktopSource())
diff --git a/SemanticEmojiFilter/composeApp/src/wasmJsMain/kotlin/sk/ainet/samples/emoji/main.kt b/SemanticEmojiFilter/composeApp/src/wasmJsMain/kotlin/sk/ainet/samples/emoji/main.kt
new file mode 100644
index 0000000..6256bc9
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/wasmJsMain/kotlin/sk/ainet/samples/emoji/main.kt
@@ -0,0 +1,45 @@
+package sk.ainet.samples.emoji
+
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.window.ComposeViewport
+import kotlinx.browser.document
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import semanticemojifilter.composeapp.generated.resources.Res
+import sk.ainet.samples.emoji.domain.Calibration
+import sk.ainet.samples.emoji.engine.LeafEmbeddingEngine
+import sk.ainet.samples.emoji.engine.LeafFiles
+import sk.ainet.samples.emoji.engine.LeafSource
+import sk.ainet.samples.emoji.vm.EmojiSearchViewModel
+
+/**
+ * The web build has no filesystem: the checkpoint is bundled as Compose resources (see
+ * scripts/fetch-model.sh) and read into memory once. The catalog embeddings are the shipped
+ * precomputed file, so the page is interactive right after the encoder is built.
+ */
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class)
+fun main() {
+ ComposeViewport(document.body!!) {
+ val viewModel = remember {
+ EmojiSearchViewModel(
+ loadEngine = { onProgress ->
+ onProgress("Fetching the LEAF checkpoint (~92 MB)โฆ")
+ val source = LeafSource.Bytes(
+ model = Res.readBytes("files/leaf/${LeafFiles.MODEL}"),
+ denseModel = Res.readBytes("files/leaf/${LeafFiles.DENSE_MODEL}"),
+ configJson = Res.readBytes("files/leaf/${LeafFiles.CONFIG}").decodeToString(),
+ denseConfigJson = Res.readBytes("files/leaf/${LeafFiles.DENSE_CONFIG}").decodeToString(),
+ poolingConfigJson = Res.readBytes("files/leaf/${LeafFiles.POOLING_CONFIG}").decodeToString(),
+ tokenizerJson = Res.readBytes("files/leaf/${LeafFiles.TOKENIZER}").decodeToString(),
+ tokenizerConfigJson = Res.readBytes("files/leaf/${LeafFiles.TOKENIZER_CONFIG}").decodeToString(),
+ )
+ LeafEmbeddingEngine.load(source, onProgress = onProgress)
+ },
+ loadIndex = { engine, onProgress -> CatalogResources.index(CatalogResources.catalog(), engine, onProgress = onProgress) },
+ calibration = Calibration.DEFAULT,
+ verifierTopK = 0,
+ )
+ }
+ App(viewModel = viewModel, skainetVersion = SKAINET_VERSION)
+ }
+}
diff --git a/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/app.html b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/app.html
new file mode 100644
index 0000000..79a9f79
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/app.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ Semantic Emoji Filter
+
+
+
+
+
+
+
+
diff --git a/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/coi-serviceworker.js b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/coi-serviceworker.js
new file mode 100644
index 0000000..9901474
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/coi-serviceworker.js
@@ -0,0 +1,146 @@
+/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
+let coepCredentialless = false;
+if (typeof window === 'undefined') {
+ self.addEventListener("install", () => self.skipWaiting());
+ self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
+
+ self.addEventListener("message", (ev) => {
+ if (!ev.data) {
+ return;
+ } else if (ev.data.type === "deregister") {
+ self.registration
+ .unregister()
+ .then(() => {
+ return self.clients.matchAll();
+ })
+ .then(clients => {
+ clients.forEach((client) => client.navigate(client.url));
+ });
+ } else if (ev.data.type === "coepCredentialless") {
+ coepCredentialless = ev.data.value;
+ }
+ });
+
+ self.addEventListener("fetch", function (event) {
+ const r = event.request;
+ if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
+ return;
+ }
+
+ const request = (coepCredentialless && r.mode === "no-cors")
+ ? new Request(r, {
+ credentials: "omit",
+ })
+ : r;
+ event.respondWith(
+ fetch(request)
+ .then((response) => {
+ if (response.status === 0) {
+ return response;
+ }
+
+ const newHeaders = new Headers(response.headers);
+ newHeaders.set("Cross-Origin-Embedder-Policy",
+ coepCredentialless ? "credentialless" : "require-corp"
+ );
+ if (!coepCredentialless) {
+ newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
+ }
+ newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: newHeaders,
+ });
+ })
+ .catch((e) => console.error(e))
+ );
+ });
+
+} else {
+ (() => {
+ const reloadedBySelf = window.sessionStorage.getItem("coiReloadedBySelf");
+ window.sessionStorage.removeItem("coiReloadedBySelf");
+ const coepDegrading = (reloadedBySelf == "coepdegrade");
+
+ // You can customize the behavior of this script through a global `coi` variable.
+ const coi = {
+ shouldRegister: () => !reloadedBySelf,
+ shouldDeregister: () => false,
+ coepCredentialless: () => true,
+ coepDegrade: () => true,
+ doReload: () => window.location.reload(),
+ quiet: false,
+ ...window.coi
+ };
+
+ const n = navigator;
+ const controlling = n.serviceWorker && n.serviceWorker.controller;
+
+ // Record the failure if the page is served by serviceWorker.
+ if (controlling && !window.crossOriginIsolated) {
+ window.sessionStorage.setItem("coiCoepHasFailed", "true");
+ }
+ const coepHasFailed = window.sessionStorage.getItem("coiCoepHasFailed");
+
+ if (controlling) {
+ // Reload only on the first failure.
+ const reloadToDegrade = coi.coepDegrade() && !(
+ coepDegrading || window.crossOriginIsolated
+ );
+ n.serviceWorker.controller.postMessage({
+ type: "coepCredentialless",
+ value: (reloadToDegrade || coepHasFailed && coi.coepDegrade())
+ ? false
+ : coi.coepCredentialless(),
+ });
+ if (reloadToDegrade) {
+ !coi.quiet && console.log("Reloading page to degrade COEP.");
+ window.sessionStorage.setItem("coiReloadedBySelf", "coepdegrade");
+ coi.doReload("coepdegrade");
+ }
+
+ if (coi.shouldDeregister()) {
+ n.serviceWorker.controller.postMessage({ type: "deregister" });
+ }
+ }
+
+ // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
+ // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
+ if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
+
+ if (!window.isSecureContext) {
+ !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
+ return;
+ }
+
+ // In some environments (e.g. Firefox private mode) this won't be available
+ if (!n.serviceWorker) {
+ !coi.quiet && console.error("COOP/COEP Service Worker not registered, perhaps due to private mode.");
+ return;
+ }
+
+ n.serviceWorker.register(window.document.currentScript.src).then(
+ (registration) => {
+ !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
+
+ registration.addEventListener("updatefound", () => {
+ !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
+ window.sessionStorage.setItem("coiReloadedBySelf", "updatefound");
+ coi.doReload();
+ });
+
+ // If the registration is active, but it's not controlling the page
+ if (registration.active && !n.serviceWorker.controller) {
+ !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
+ window.sessionStorage.setItem("coiReloadedBySelf", "notcontrolling");
+ coi.doReload();
+ }
+ },
+ (err) => {
+ !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
+ }
+ );
+ })();
+}
diff --git a/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/index.html b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/index.html
new file mode 100644
index 0000000..0a91300
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/index.html
@@ -0,0 +1,222 @@
+
+
+
+
+
+ Semantic Emoji Filter - SKaiNET Examples
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/logo.png b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/logo.png
new file mode 100644
index 0000000..3ae74ca
Binary files /dev/null and b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/logo.png differ
diff --git a/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/styles.css b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/styles.css
new file mode 100644
index 0000000..8e94d43
--- /dev/null
+++ b/SemanticEmojiFilter/composeApp/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,7 @@
+html, body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+}
diff --git a/SemanticEmojiFilter/gradle.properties b/SemanticEmojiFilter/gradle.properties
new file mode 100644
index 0000000..4a47f07
--- /dev/null
+++ b/SemanticEmojiFilter/gradle.properties
@@ -0,0 +1,12 @@
+kotlin.code.style=official
+
+#Gradle
+org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1536m -Dfile.encoding=UTF-8
+kotlin.daemon.jvmargs=-Xmx4g
+
+#Android
+android.nonTransitiveRClass=true
+android.useAndroidX=true
+
+#Kotlin Multiplatform
+kotlin.mpp.enableCInteropCommonization=true
diff --git a/SemanticEmojiFilter/gradle/libs.versions.toml b/SemanticEmojiFilter/gradle/libs.versions.toml
new file mode 100644
index 0000000..be8796c
--- /dev/null
+++ b/SemanticEmojiFilter/gradle/libs.versions.toml
@@ -0,0 +1,64 @@
+[versions]
+agp = "9.3.1"
+android-compileSdk = "37"
+android-minSdk = "24"
+android-targetSdk = "37"
+androidx-activityCompose = "1.13.0"
+androidx-lifecycle = "2.11.0"
+# Must match the shared :skainet-ui design system's own pin (included build) โ a mismatch
+# pulls two Skiko versions onto the desktop classpath, which crashes with
+# UnsatisfiedLinkError (getAdapterMaxTextureSize) at first render on macOS/Metal.
+composeMultiplatform = "1.11.1"
+kotlin = "2.4.10"
+kotlinx-coroutines = "1.11.0"
+kotlinxIo = "0.9.1"
+# The transformers 0.58.0 pin; the catalog JSON and calibration files are read with it.
+kotlinxSerialization = "1.11.0"
+# The bert encoder, the SafeTensors reader and the engine tokenizers (WordPiece for LEAF) come from
+# the engine BOM; the typed-decision contract, runtime and the Laya adapter need transformers 0.59.0+
+# (0.60.0 runs Laya's exact GELU through engine 0.59.0's gelu op instead of a host-side loop).
+skainet = "0.59.0"
+skainet-transformers = "0.60.0"
+
+[libraries]
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
+compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
+androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
+kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
+kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIo" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
+
+# SKaiNET core โ BOM-governed
+skainet-bom = { module = "sk.ainet:skainet-bom", version.ref = "skainet" }
+skainet-lang-core = { module = "sk.ainet.core:skainet-lang-core" }
+skainet-backend-cpu = { module = "sk.ainet.core:skainet-backend-cpu" }
+# Compile-time only on the JVM: PanamaVectorKernelProvider.isAvailable() for the kernel label.
+skainet-backend-api = { module = "sk.ainet.core:skainet-backend-api" }
+# Hand-written ARM NEON kernels โ Android JNI only.
+skainet-backend-jni-cpu = { module = "sk.ainet.core:skainet-backend-jni-cpu" }
+skainet-data-source = { module = "sk.ainet.core:skainet-data-source" }
+skainet-io-core = { module = "sk.ainet.core:skainet-io-core" }
+skainet-io-safetensors = { module = "sk.ainet.core:skainet-io-safetensors" }
+skainet-frontend-text = { module = "sk.ainet.core:skainet-frontend-text" }
+
+# SKaiNET-transformers โ BOM-governed
+skainet-transformers-bom = { module = "sk.ainet.transformers:skainet-transformers-bom", version.ref = "skainet-transformers" }
+skainet-transformers-inference-bert = { module = "sk.ainet.transformers:skainet-transformers-inference-bert" }
+# Verifier only (-PlayaVerifier=true)
+skainet-transformers-decisions-api = { module = "sk.ainet.transformers:skainet-transformers-decisions-api" }
+skainet-transformers-decisions-runtime = { module = "sk.ainet.transformers:skainet-transformers-decisions-runtime" }
+skainet-transformers-inference-laya = { module = "sk.ainet.transformers:skainet-transformers-inference-laya" }
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
+composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
+composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
diff --git a/SemanticEmojiFilter/gradle/wrapper/gradle-wrapper.jar b/SemanticEmojiFilter/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f8e1ee3
Binary files /dev/null and b/SemanticEmojiFilter/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/SemanticEmojiFilter/gradle/wrapper/gradle-wrapper.properties b/SemanticEmojiFilter/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..5dd3c01
--- /dev/null
+++ b/SemanticEmojiFilter/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/SemanticEmojiFilter/gradlew b/SemanticEmojiFilter/gradlew
new file mode 100755
index 0000000..adff685
--- /dev/null
+++ b/SemanticEmojiFilter/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright ยฉ 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป,
+# ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป;
+# * compound commands having a testable exit status, especially ยซcaseยป;
+# * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป.
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/SemanticEmojiFilter/gradlew.bat b/SemanticEmojiFilter/gradlew.bat
new file mode 100644
index 0000000..e509b2d
--- /dev/null
+++ b/SemanticEmojiFilter/gradlew.bat
@@ -0,0 +1,93 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/SemanticEmojiFilter/scripts/catalog-selection.json b/SemanticEmojiFilter/scripts/catalog-selection.json
new file mode 100644
index 0000000..54bc2cc
--- /dev/null
+++ b/SemanticEmojiFilter/scripts/catalog-selection.json
@@ -0,0 +1,1580 @@
+[
+ {
+ "cp": "1F957",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F34E",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F966",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F955",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F34C",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F347",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F353",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F951",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F345",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F952",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F34A",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F34B",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F349",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F96C",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F360",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F95C",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F95B",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1FAD0",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F95D",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F352",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F351",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F350",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F33D",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F954",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F363",
+ "group": "food-healthy",
+ "tags": [
+ "food",
+ "healthy"
+ ]
+ },
+ {
+ "cp": "1F355",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F354",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F35F",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F32D",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F369",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F36A",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F382",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F370",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F9C1",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F36B",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F36C",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F36D",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F366",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F37F",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F953",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F9C0",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F32E",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F950",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F37A",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F377",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "2615",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F35D",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F35C",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F95E",
+ "group": "food-indulgent",
+ "tags": [
+ "food",
+ "indulgent"
+ ]
+ },
+ {
+ "cp": "1F912",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F927",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F92E",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F922",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F637",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F915",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F48A",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1FA7A",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F321-FE0F",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F3E5",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F489",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1FA79",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F6CC",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F691",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F9A0",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F9EC",
+ "group": "health-sick",
+ "tags": [
+ "health",
+ "sick"
+ ]
+ },
+ {
+ "cp": "1F3B8",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F941",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3A4",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3B9",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3BA",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3B7",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3BB",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F918",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3B6",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3B5",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3A7",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F50A",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F3BC",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F4FB",
+ "group": "music-rock",
+ "tags": [
+ "music",
+ "rock-band"
+ ]
+ },
+ {
+ "cp": "1F436",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F431",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F42D",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F439",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F430",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F98A",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F43B",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F43C",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F428",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F981",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F42E",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F437",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F438",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F435",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F414",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F427",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F98B",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F41D",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F40D",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F422",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F42C",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F433",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F988",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F419",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F989",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "1F418",
+ "group": "animals",
+ "tags": [
+ "animal"
+ ]
+ },
+ {
+ "cp": "2600-FE0F",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F327-FE0F",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "26C8-FE0F",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "2744-FE0F",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F308",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F32A-FE0F",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F319",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "2B50",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F525",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F4A7",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F30A",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F338",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F33B",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F332",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F342",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "2601-FE0F",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "1F30B",
+ "group": "weather-nature",
+ "tags": [
+ "weather",
+ "nature"
+ ]
+ },
+ {
+ "cp": "26BD",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F3C0",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F3BE",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F3C8",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "26BE",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F3CA",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F6B4",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F3C3",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F3CB-FE0F",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "26F7-FE0F",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F3C4",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F9D7",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F94A",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F3C6",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F947",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "26F3",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "1F9D8",
+ "group": "sport",
+ "tags": [
+ "sport"
+ ]
+ },
+ {
+ "cp": "2708-FE0F",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F697",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F682",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F6B2",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F680",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "26F5",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F3D6-FE0F",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F3D4-FE0F",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F5FA-FE0F",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F9F3",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F3E8",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F695",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F68C",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F6F3-FE0F",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "26FA",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F5FD",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F9ED",
+ "group": "travel",
+ "tags": [
+ "travel"
+ ]
+ },
+ {
+ "cp": "1F600",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F602",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F60D",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F622",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F62D",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F621",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F631",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F634",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F914",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F60E",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F973",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F62C",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F644",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F607",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F917",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F624",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F970",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F605",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1FAE0",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F610",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "2764-FE0F",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F494",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F44D",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F44E",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F64F",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F44F",
+ "group": "emotions",
+ "tags": [
+ "emotion"
+ ]
+ },
+ {
+ "cp": "1F4BB",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F4F1",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "231A",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F4F7",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F511",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F512",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F4DA",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "270F-FE0F",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F4CE",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F4A1",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F527",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F528",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F4B0",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F381",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F388",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "23F0",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F4C5",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F5D1-FE0F",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F9F9",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F6D2",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F3AE",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F3B2",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F9E9",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F3E0",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F6BF",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ },
+ {
+ "cp": "1F6CF-FE0F",
+ "group": "objects",
+ "tags": [
+ "object",
+ "everyday"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/SemanticEmojiFilter/scripts/fetch-model.sh b/SemanticEmojiFilter/scripts/fetch-model.sh
new file mode 100755
index 0000000..773028a
--- /dev/null
+++ b/SemanticEmojiFilter/scripts/fetch-model.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# Downloads the MongoDB LEAF retrieval encoder (mdbr-leaf-ir, Apache-2.0) at a pinned revision
+# into a directory laid out exactly as on the Hugging Face Hub. Default target: the wasm
+# Compose resources, so the web build bundles the checkpoint (~92 MB). Desktop and Android
+# download the same files at runtime into their own cache instead.
+#
+# scripts/fetch-model.sh [target-dir]
+set -euo pipefail
+
+REPO="MongoDB/mdbr-leaf-ir"
+REVISION="4262131b32c3182bd06e67e92ae69d7bd66e0c5c"
+FILES=(
+ "model.safetensors"
+ "config.json"
+ "tokenizer.json"
+ "tokenizer_config.json"
+ "2_Dense/model.safetensors"
+ "2_Dense/config.json"
+ "1_Pooling/config.json"
+)
+MIN_MODEL_BYTES=$((80 * 1024 * 1024))
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+TARGET="${1:-$SCRIPT_DIR/../composeApp/src/wasmJsMain/composeResources/files/leaf}"
+mkdir -p "$TARGET"
+
+# A local copy (the desktop/CLI cache, or $LEAF_MODEL_DIR) is reused instead of downloading twice.
+LOCAL="${LEAF_MODEL_DIR:-$HOME/.skainet-examples/semantic-emoji-filter/leaf}"
+
+for f in "${FILES[@]}"; do
+ out="$TARGET/$f"
+ if [[ ! -s "$out" && -s "$LOCAL/$f" && "$LOCAL" != "$TARGET" ]]; then
+ mkdir -p "$(dirname "$out")"
+ cp "$LOCAL/$f" "$out"
+ echo "fetch-model: $f copied from $LOCAL"
+ continue
+ fi
+ if [[ -s "$out" ]]; then
+ if [[ "$f" != "model.safetensors" || $(stat -c %s "$out" 2>/dev/null || stat -f %z "$out") -ge $MIN_MODEL_BYTES ]]; then
+ echo "fetch-model: $f present, skipping"
+ continue
+ fi
+ fi
+ mkdir -p "$(dirname "$out")"
+ echo "fetch-model: downloading $f"
+ curl -fL --retry 3 "https://huggingface.co/$REPO/resolve/$REVISION/$f" -o "$out.part"
+ mv "$out.part" "$out"
+done
+
+size=$(stat -c %s "$TARGET/model.safetensors" 2>/dev/null || stat -f %z "$TARGET/model.safetensors")
+if (( size < MIN_MODEL_BYTES )); then
+ echo "fetch-model: model.safetensors is only $size bytes โ download incomplete" >&2
+ exit 1
+fi
+echo "fetch-model: LEAF checkpoint ready in $TARGET"
diff --git a/SemanticEmojiFilter/scripts/generate-catalog.py b/SemanticEmojiFilter/scripts/generate-catalog.py
new file mode 100644
index 0000000..6c25481
--- /dev/null
+++ b/SemanticEmojiFilter/scripts/generate-catalog.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+"""Builds catalog.json from Unicode CLDR emoji annotations and the curated selection.
+
+ python3 -I scripts/generate-catalog.py --cldr --selection scripts/catalog-selection.json \
+ --out composeApp/src/commonMain/composeResources/files/catalog/catalog.json
+
+ holds the CLDR files common/annotations/{en,de}.xml and common/annotationsDerived/{en,de}.xml
+of one release (default names: annotations_en.xml, annotations_de.xml, annotationsDerived_en.xml,
+annotationsDerived_de.xml). Names come from the `tts` annotation, keywords from the plain one.
+The script fails on a missing annotation or a duplicate id, so the catalog is always complete.
+"""
+import argparse
+import json
+import sys
+import xml.etree.ElementTree as ET
+
+VS16 = "๏ธ"
+
+
+def load_annotations(path):
+ names, keywords = {}, {}
+ root = ET.parse(path).getroot()
+ for node in root.iter("annotation"):
+ cp = node.get("cp", "").replace(VS16, "")
+ text = (node.text or "").strip()
+ if node.get("type") == "tts":
+ names[cp] = text
+ else:
+ keywords[cp] = [k.strip() for k in text.split("|") if k.strip()]
+ return names, keywords
+
+
+def merge(a, b):
+ out = dict(a)
+ out.update(b)
+ return out
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--cldr", required=True)
+ ap.add_argument("--cldr-release", default="47")
+ ap.add_argument("--selection", required=True)
+ ap.add_argument("--out", required=True)
+ ap.add_argument("--embedding-text-version", type=int, default=1)
+ args = ap.parse_args()
+
+ en_names, en_kw = load_annotations(f"{args.cldr}/annotations_en.xml")
+ de_names, de_kw = load_annotations(f"{args.cldr}/annotations_de.xml")
+ den, dek = load_annotations(f"{args.cldr}/annotationsDerived_en.xml")
+ ddn, ddk = load_annotations(f"{args.cldr}/annotationsDerived_de.xml")
+ en_names, en_kw = merge(en_names, den), merge(en_kw, dek)
+ de_names, de_kw = merge(de_names, ddn), merge(de_kw, ddk)
+
+ with open(args.selection, encoding="utf-8") as f:
+ selection = json.load(f)
+
+ items, seen, errors = [], set(), []
+ for entry in selection:
+ cps = [int(h, 16) for h in entry["cp"].split("-")]
+ emoji = "".join(chr(c) for c in cps)
+ key = emoji.replace(VS16, "")
+ item_id = "-".join(f"{c:04X}" for c in cps if c != 0xFE0F)
+ if item_id in seen:
+ errors.append(f"duplicate id {item_id}")
+ continue
+ seen.add(item_id)
+ if key not in en_names or key not in de_names:
+ errors.append(f"{item_id} ({emoji}): no CLDR name in {'en' if key not in en_names else 'de'}")
+ continue
+ items.append({
+ "id": item_id,
+ "emoji": emoji,
+ "nameEn": en_names[key],
+ "nameDe": de_names[key],
+ "keywordsEn": en_kw.get(key, []),
+ "keywordsDe": de_kw.get(key, []),
+ "tags": entry.get("tags", []),
+ "group": entry.get("group", ""),
+ })
+
+ if errors:
+ print("\n".join(errors), file=sys.stderr)
+ sys.exit(1)
+
+ catalog = {
+ "version": 1,
+ "cldr": args.cldr_release,
+ "embeddingTextVersion": args.embedding_text_version,
+ "embeddingsSha256": "",
+ "items": items,
+ }
+ # Keep an existing digest if the items did not change (the CLI writes it after embedding).
+ try:
+ with open(args.out, encoding="utf-8") as f:
+ previous = json.load(f)
+ if previous.get("items") == items and previous.get("embeddingTextVersion") == args.embedding_text_version:
+ catalog["embeddingsSha256"] = previous.get("embeddingsSha256", "")
+ except (OSError, ValueError):
+ pass
+ with open(args.out, "w", encoding="utf-8") as f:
+ json.dump(catalog, f, ensure_ascii=False, indent=2)
+ f.write("\n")
+ print(f"catalog: {len(items)} items -> {args.out}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/SemanticEmojiFilter/settings.gradle.kts b/SemanticEmojiFilter/settings.gradle.kts
new file mode 100644
index 0000000..2500646
--- /dev/null
+++ b/SemanticEmojiFilter/settings.gradle.kts
@@ -0,0 +1,43 @@
+rootProject.name = "SemanticEmojiFilter"
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
+// Opt-in second stage: the Laya decision model as a verifier behind the embedding filter
+// (the SKaiNET-transformers `decisions` and `laya` modules, 0.59.0+). Desktop and Android only.
+val layaVerifier = providers.gradleProperty("layaVerifier").orNull == "true"
+
+pluginManagement {
+ repositories {
+ google {
+ mavenContent {
+ includeGroupAndSubgroups("androidx")
+ includeGroupAndSubgroups("com.android")
+ includeGroupAndSubgroups("com.google")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ google {
+ mavenContent {
+ includeGroupAndSubgroups("androidx")
+ includeGroupAndSubgroups("com.android")
+ includeGroupAndSubgroups("com.google")
+ }
+ }
+ mavenCentral()
+ }
+}
+
+// Shared SKaiNET design system (theme + components), consumed as an included build like the
+// sibling examples.
+includeBuild("../skainet-ui")
+
+include(":androidApp")
+include(":composeApp")
+include(":shared")
+include(":cli")
+if (layaVerifier) include(":verifier")
diff --git a/SemanticEmojiFilter/shared/build.gradle.kts b/SemanticEmojiFilter/shared/build.gradle.kts
new file mode 100644
index 0000000..4def24d
--- /dev/null
+++ b/SemanticEmojiFilter/shared/build.gradle.kts
@@ -0,0 +1,91 @@
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.androidMultiplatformLibrary)
+ alias(libs.plugins.kotlinSerialization)
+}
+
+kotlin {
+ jvmToolchain(21)
+
+ android {
+ namespace = "sk.ainet.samples.emoji.shared"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ minSdk = libs.versions.android.minSdk.get().toInt()
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_21)
+ }
+
+ // Runs commonTest on the JVM against Android stubs (testAndroidHostTest) โ CI relies on it.
+ withHostTest {}
+ }
+
+ jvm()
+
+ // No iOS: the bert, laya and decisions modules of SKaiNET-transformers publish no Apple targets.
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ browser()
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.kotlinx.coroutines)
+ implementation(libs.kotlinx.io.core)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.androidx.lifecycle.viewmodel)
+
+ // SKaiNET core. BOM applied via api() so its version constraints also reach the
+ // composeApp / cli classpaths, which construct the execution context themselves.
+ api(project.dependencies.platform(libs.skainet.bom))
+ api(libs.skainet.lang.core)
+ api(libs.skainet.backend.cpu)
+ api(libs.skainet.io.core)
+ api(libs.skainet.io.safetensors)
+ api(libs.skainet.frontend.text)
+
+ // SKaiNET-transformers: the BERT encoder runtime (mean pooling + 2_Dense projection).
+ api(project.dependencies.platform(libs.skainet.transformers.bom))
+ api(libs.skainet.transformers.inference.bert)
+ }
+ // Desktop and Android share the file-based checkpoint download (both are JVM code).
+ val jvmAndroidMain by creating {
+ dependsOn(commonMain.get())
+ dependencies {
+ // Streamed download of the checkpoint files from the Hugging Face Hub.
+ implementation(libs.skainet.data.source)
+ }
+ }
+ jvmMain {
+ dependsOn(jvmAndroidMain)
+ dependencies {
+ // PanamaVectorKernelProvider.isAvailable() for the kernel-tier label.
+ implementation(libs.skainet.backend.api)
+ }
+ }
+ androidMain {
+ dependsOn(jvmAndroidMain)
+ dependencies {
+ // Hand-written ARM NEON kernels, dispatched at runtime.
+ implementation(libs.skainet.backend.jni.cpu)
+ }
+ }
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ implementation(libs.kotlinx.coroutines.test)
+ }
+ }
+}
+
+tasks.withType().configureEach {
+ // SIMD-accelerated CPU ops via the JDK Vector API (incubator), same flags as the desktop run.
+ jvmArgs(
+ "--add-modules", "jdk.incubator.vector",
+ "--enable-preview",
+ "-Dskainet.cpu.vector.enabled=true",
+ )
+ maxHeapSize = "4g"
+}
diff --git a/SemanticEmojiFilter/shared/src/androidMain/kotlin/sk/ainet/samples/emoji/model/AndroidLeafProvider.kt b/SemanticEmojiFilter/shared/src/androidMain/kotlin/sk/ainet/samples/emoji/model/AndroidLeafProvider.kt
new file mode 100644
index 0000000..6c27079
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/androidMain/kotlin/sk/ainet/samples/emoji/model/AndroidLeafProvider.kt
@@ -0,0 +1,21 @@
+package sk.ainet.samples.emoji.model
+
+import android.content.Context
+import java.io.File
+import sk.ainet.samples.emoji.engine.LeafSource
+
+/**
+ * Resolves the checkpoint files into `filesDir/leaf`, streamed from the Hugging Face Hub on
+ * first use; afterwards the app runs offline.
+ */
+class AndroidLeafProvider(
+ private val context: Context,
+ private val hfToken: String = "",
+) : LeafProvider {
+ override suspend fun resolve(onProgress: (String) -> Unit): LeafSource {
+ val dir = File(context.filesDir, "leaf").apply { mkdirs() }
+ val headers = hfToken.takeIf { it.isNotBlank() }?.let { mapOf("Authorization" to "Bearer $it") } ?: emptyMap()
+ downloadMissingLeafFiles(dir, onProgress, headers)
+ return LeafSource.Directory(dir.path)
+ }
+}
diff --git a/SemanticEmojiFilter/shared/src/androidMain/kotlin/sk/ainet/samples/emoji/platform/Platform.android.kt b/SemanticEmojiFilter/shared/src/androidMain/kotlin/sk/ainet/samples/emoji/platform/Platform.android.kt
new file mode 100644
index 0000000..95ea9f8
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/androidMain/kotlin/sk/ainet/samples/emoji/platform/Platform.android.kt
@@ -0,0 +1,21 @@
+package sk.ainet.samples.emoji.platform
+
+import android.util.Log
+import java.io.File
+import sk.ainet.exec.kernel.jni.JniKernelProvider
+
+actual val currentSamplePlatform: SamplePlatform = SamplePlatform.ANDROID
+
+actual fun kernelTierLabel(): String =
+ if (JniKernelProvider.isAvailable()) "ARM NEON" else "NEON (unavailable)"
+
+actual fun readTextFile(path: String): String = File(path).readText()
+
+internal actual fun platformLog(line: String) {
+ // Host unit tests run against stubbed android.jar, where Log throws "not mocked".
+ try {
+ Log.i("SKAINET_PERF", line)
+ } catch (_: RuntimeException) {
+ println("[SKAINET_PERF] $line")
+ }
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Calibration.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Calibration.kt
new file mode 100644
index 0000000..7f068c7
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Calibration.kt
@@ -0,0 +1,103 @@
+package sk.ainet.samples.emoji.domain
+
+import kotlin.math.exp
+import kotlin.math.ln
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+/**
+ * Turns a cosine similarity into a match confidence with a logistic curve,
+ * `confidence = 1 / (1 + exp(-k (cos - x0)))`. The two numbers are fitted on a small labelled
+ * query set by the CLI (`calibrate`) and shipped next to the catalog: cosine thresholds are
+ * encoder-specific, so the PRD's guessed `k = 10, x0 = 0.5` is replaced by measured values.
+ */
+@Serializable
+data class Calibration(
+ val k: Double,
+ val x0: Double,
+ val threshold: Double = 0.5,
+ /** At most this many items form the top tier, however many pass [threshold]. */
+ val maxTop: Int = 12,
+ /** How many labelled queries the fit used; informational. */
+ val queries: Int = 0,
+ /** Training accuracy at [threshold]; informational. */
+ val accuracy: Double = 0.0,
+ /** Training F1 at [threshold]; informational. */
+ val f1: Double = 0.0,
+) {
+ init {
+ require(k > 0) { "k must be positive" }
+ require(threshold in 0.0..1.0) { "threshold must be within [0, 1]" }
+ require(maxTop > 0) { "maxTop must be positive" }
+ }
+
+ fun confidence(cosine: Double): Double = 1.0 / (1.0 + exp(-k * (cosine - x0)))
+
+ fun isTop(cosine: Double): Boolean = confidence(cosine) >= threshold
+
+ /** The cosine at which the confidence crosses [threshold]. */
+ val cosineAtThreshold: Double get() = x0 - ln(1.0 / threshold - 1.0) / k
+
+ companion object {
+ /** Before calibration ran: a steep curve around a typical retrieval-encoder boundary. */
+ val DEFAULT = Calibration(k = 14.0, x0 = 0.45)
+
+ private val json = Json { ignoreUnknownKeys = true }
+ fun parse(text: String): Calibration = json.decodeFromString(serializer(), text)
+ fun encode(c: Calibration): String = Json { prettyPrint = true }.encodeToString(serializer(), c)
+
+ /**
+ * One-dimensional logistic regression by gradient descent on `(cosine, isMatch)` pairs,
+ * class-balanced: every positive weighs `negatives / positives`, otherwise the few matches
+ * among hundreds of non-matches push the boundary above every real cosine (the unbalanced
+ * fit on this catalog accepted nothing). Returns `k = slope`, `x0 = -intercept / slope`.
+ */
+ fun fit(samples: List>, steps: Int = 4000, learningRate: Double = 0.5): Calibration {
+ val positives = samples.count { it.second }
+ val negatives = samples.size - positives
+ require(positives > 0 && negatives > 0) { "calibration needs positives and negatives" }
+ val wPos = negatives.toDouble() / positives
+ val total = 2.0 * negatives
+ var a = 10.0
+ var b = -5.0
+ repeat(steps) {
+ var ga = 0.0
+ var gb = 0.0
+ for ((x, y) in samples) {
+ val p = 1.0 / (1.0 + exp(-(a * x + b)))
+ val w = if (y) wPos else 1.0
+ val err = w * (p - (if (y) 1.0 else 0.0))
+ ga += err * x
+ gb += err
+ }
+ a -= learningRate * ga / total
+ b -= learningRate * gb / total
+ }
+ // The curve maps cosine to a probability; the threshold on it is the F1-optimal cut.
+ val curve = Calibration(k = a, x0 = -b / a)
+ var best = curve
+ var bestF1 = -1.0
+ for (step in 5..95 step 5) {
+ val candidate = curve.copy(threshold = step / 100.0)
+ val f1 = f1(candidate, samples)
+ if (f1 > bestF1) {
+ bestF1 = f1
+ best = candidate
+ }
+ }
+ val correct = samples.count { (x, y) -> best.isTop(x) == y }
+ return best.copy(accuracy = correct / samples.size.toDouble(), f1 = bestF1)
+ }
+
+ private fun f1(c: Calibration, samples: List>): Double {
+ var tp = 0
+ var fp = 0
+ var fn = 0
+ for ((x, y) in samples) {
+ val predicted = c.isTop(x)
+ if (predicted && y) tp++ else if (predicted) fp++ else if (y) fn++
+ }
+ return if (tp == 0) 0.0 else 2.0 * tp / (2.0 * tp + fp + fn)
+ }
+ }
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Catalog.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Catalog.kt
new file mode 100644
index 0000000..596c285
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Catalog.kt
@@ -0,0 +1,58 @@
+package sk.ainet.samples.emoji.domain
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+/** One emoji of the catalog: CLDR names and keywords in English and German, plus curated tags. */
+@Serializable
+data class CatalogItem(
+ /** Hex code points joined with `-`, e.g. `1F355`; stable across catalog versions. */
+ val id: String,
+ val emoji: String,
+ val nameEn: String,
+ val nameDe: String,
+ val keywordsEn: List = emptyList(),
+ val keywordsDe: List = emptyList(),
+ /** Hand-curated for the showcase groups (`healthy`, `indulgent`, `sick`, `rock-band`, โฆ). */
+ val tags: List = emptyList(),
+ val group: String = "",
+) {
+ /**
+ * The single text the encoder sees for this item. CLI, tests and app share this definition:
+ * the precomputed embeddings and the parity golden depend on it (see `Catalog.embeddingTextVersion`).
+ */
+ fun embeddingText(): String = buildString {
+ append(nameEn)
+ if (keywordsEn.isNotEmpty()) append(". ").append(keywordsEn.joinToString(", "))
+ append(". ").append(nameDe)
+ }
+
+ fun name(german: Boolean): String = if (german) nameDe else nameEn
+}
+
+@Serializable
+data class Catalog(
+ val version: Int,
+ /** CLDR release the names and keywords were generated from. */
+ val cldr: String,
+ /** Bump when [CatalogItem.embeddingText] changes; the shipped embeddings are only valid for the same version. */
+ val embeddingTextVersion: Int,
+ /** SHA-256 of `catalog-embeddings.bin` computed for these items, empty until the CLI wrote it. */
+ val embeddingsSha256: String = "",
+ val items: List,
+) {
+ init {
+ require(items.isNotEmpty()) { "catalog has no items" }
+ require(items.map { it.id }.toSet().size == items.size) { "catalog has duplicate ids" }
+ }
+
+ fun item(id: String): CatalogItem? = items.firstOrNull { it.id == id }
+
+ companion object {
+ const val EMBEDDING_TEXT_VERSION = 1
+ private val json = Json { ignoreUnknownKeys = true }
+
+ fun parse(text: String): Catalog = json.decodeFromString(serializer(), text)
+ fun encode(catalog: Catalog): String = Json { prettyPrint = true }.encodeToString(serializer(), catalog)
+ }
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Ranking.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Ranking.kt
new file mode 100644
index 0000000..9d4d839
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/domain/Ranking.kt
@@ -0,0 +1,14 @@
+package sk.ainet.samples.emoji.domain
+
+/** Where an item lands after a sweep: above the calibrated threshold or below. */
+enum class Tier { TOP, REST }
+
+/** One catalog item scored against the current query. */
+data class RankedItem(
+ val item: CatalogItem,
+ /** Cosine similarity between the query and the item embedding (both L2-normalised). */
+ val cosine: Float,
+ /** Calibrated match confidence in `[0, 1]`, see [Calibration]. */
+ val confidence: Float,
+ val tier: Tier,
+)
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/ByteArrayRandomAccessSource.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/ByteArrayRandomAccessSource.kt
new file mode 100644
index 0000000..51fd812
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/ByteArrayRandomAccessSource.kt
@@ -0,0 +1,25 @@
+package sk.ainet.samples.emoji.engine
+
+import sk.ainet.io.RandomAccessSource
+
+/**
+ * In-memory [RandomAccessSource] over a fully materialised byte array. The web build has no
+ * filesystem: the SafeTensors files arrive as Compose resource bytes and the streaming reader
+ * wants random access, so this wraps them. Desktop and Android read straight off disk instead.
+ */
+class ByteArrayRandomAccessSource(private val bytes: ByteArray) : RandomAccessSource {
+ override val size: Long = bytes.size.toLong()
+
+ override fun readAt(position: Long, length: Int): ByteArray {
+ require(position >= 0 && length >= 0 && position + length <= size) { "read [$position, +$length) outside $size bytes" }
+ return bytes.copyOfRange(position.toInt(), (position + length).toInt())
+ }
+
+ override fun readAt(position: Long, buffer: ByteArray, offset: Int, length: Int): Int {
+ val n = minOf(length, (size - position).toInt())
+ bytes.copyInto(buffer, offset, position.toInt(), position.toInt() + n)
+ return n
+ }
+
+ override fun close() {}
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/CatalogIndex.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/CatalogIndex.kt
new file mode 100644
index 0000000..328a528
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/CatalogIndex.kt
@@ -0,0 +1,107 @@
+package sk.ainet.samples.emoji.engine
+
+import kotlin.math.sqrt
+import sk.ainet.samples.emoji.domain.Calibration
+import sk.ainet.samples.emoji.domain.CatalogItem
+import sk.ainet.samples.emoji.domain.RankedItem
+import sk.ainet.samples.emoji.domain.Tier
+
+/**
+ * The catalog embeddings, one L2-normalised row per item, and the cosine sweep over them.
+ * 180 ร 768 floats is 553 KB: computed once at startup on desktop and Android, shipped as
+ * `catalog-embeddings.bin` for the web build (and as the parity golden for the tests).
+ */
+class CatalogIndex(
+ val items: List,
+ /** Row-major `items.size ร dimensions`. */
+ val vectors: FloatArray,
+ val dimensions: Int,
+) {
+ init {
+ require(vectors.size == items.size * dimensions) { "expected ${items.size} ร $dimensions floats, got ${vectors.size}" }
+ }
+
+ /** Cosine of every item against [query] (unit vector), in catalog order. */
+ fun cosines(query: FloatArray): FloatArray {
+ require(query.size == dimensions) { "query has ${query.size} dimensions, index has $dimensions" }
+ val out = FloatArray(items.size)
+ for (i in items.indices) {
+ val base = i * dimensions
+ var dot = 0f
+ for (d in 0 until dimensions) dot += vectors[base + d] * query[d]
+ out[i] = dot
+ }
+ return out
+ }
+
+ /** Every item scored and sorted by cosine; the top tier is what passes the threshold, at most `maxTop` items. */
+ fun rank(query: FloatArray, calibration: Calibration): List {
+ val cos = cosines(query)
+ val sorted = items.indices.sortedByDescending { cos[it] }
+ return sorted.mapIndexed { rank, i ->
+ val c = cos[i].toDouble()
+ val top = rank < calibration.maxTop && calibration.isTop(c)
+ RankedItem(items[i], cos[i], calibration.confidence(c).toFloat(), if (top) Tier.TOP else Tier.REST)
+ }
+ }
+
+ /** The unscored catalog, for an empty query. */
+ fun unranked(): List = items.map { RankedItem(it, 0f, 0f, Tier.REST) }
+
+ fun vector(index: Int): FloatArray = vectors.copyOfRange(index * dimensions, (index + 1) * dimensions)
+
+ /** Little-endian float32, row-major, no header. */
+ fun toBytes(): ByteArray {
+ val out = ByteArray(vectors.size * 4)
+ for (i in vectors.indices) {
+ val bits = vectors[i].toRawBits()
+ val o = i * 4
+ out[o] = bits.toByte()
+ out[o + 1] = (bits ushr 8).toByte()
+ out[o + 2] = (bits ushr 16).toByte()
+ out[o + 3] = (bits ushr 24).toByte()
+ }
+ return out
+ }
+
+ companion object {
+ fun fromBytes(items: List, bytes: ByteArray, dimensions: Int = 768): CatalogIndex {
+ require(bytes.size == items.size * dimensions * 4) { "embeddings file has ${bytes.size} bytes, expected ${items.size * dimensions * 4}" }
+ val floats = FloatArray(bytes.size / 4)
+ for (i in floats.indices) {
+ val o = i * 4
+ val bits = (bytes[o].toInt() and 0xFF) or
+ ((bytes[o + 1].toInt() and 0xFF) shl 8) or
+ ((bytes[o + 2].toInt() and 0xFF) shl 16) or
+ ((bytes[o + 3].toInt() and 0xFF) shl 24)
+ floats[i] = Float.fromBits(bits)
+ }
+ return CatalogIndex(items, floats, dimensions)
+ }
+
+ /** Embeds every item with [engine]; [onProgress] gets `(done, total)`. */
+ fun build(items: List, engine: EmbeddingEngine, onProgress: (Int, Int) -> Unit = { _, _ -> }): CatalogIndex {
+ val dim = engine.dimensions
+ val vectors = FloatArray(items.size * dim)
+ items.forEachIndexed { i, item ->
+ val v = engine.embed(item.embeddingText())
+ require(v.size == dim) { "embedding of '${item.id}' has ${v.size} dimensions, expected $dim" }
+ v.copyInto(vectors, i * dim)
+ onProgress(i + 1, items.size)
+ }
+ return CatalogIndex(items, vectors, dim)
+ }
+
+ fun cosine(a: FloatArray, b: FloatArray): Float {
+ var dot = 0f
+ var na = 0f
+ var nb = 0f
+ for (i in a.indices) {
+ dot += a[i] * b[i]
+ na += a[i] * a[i]
+ nb += b[i] * b[i]
+ }
+ return if (na == 0f || nb == 0f) 0f else dot / (sqrt(na) * sqrt(nb))
+ }
+ }
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/EmbeddingEngine.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/EmbeddingEngine.kt
new file mode 100644
index 0000000..d242da2
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/EmbeddingEngine.kt
@@ -0,0 +1,122 @@
+package sk.ainet.samples.emoji.engine
+
+import kotlin.time.TimeSource
+import sk.ainet.context.DirectCpuExecutionContext
+import sk.ainet.context.ExecutionContext
+import sk.ainet.frontend.text.BatchTokenizer
+import sk.ainet.frontend.tokenizer.TextFrontEnds
+import sk.ainet.io.RandomAccessSource
+import sk.ainet.io.openRandomAccessSource
+import sk.ainet.io.safetensors.SafeTensorsParametersLoader
+import sk.ainet.lang.types.FP32
+import sk.ainet.models.bert.BertConfigParser
+import sk.ainet.models.bert.BertEncoderRuntime
+import sk.ainet.models.bert.BertExecutionMode
+import sk.ainet.models.bert.BertNetworkLoader
+import sk.ainet.models.bert.PooledExecutionContext
+import sk.ainet.models.bert.createBertEncoderRuntime
+import sk.ainet.samples.emoji.platform.readTextFile
+
+/** Sentence embeddings: one L2-normalised vector per text, so cosine is a dot product. */
+interface EmbeddingEngine {
+ val dimensions: Int
+
+ /** Embeds a catalog text (a "document" in retrieval terms). */
+ fun embed(text: String): FloatArray
+
+ /**
+ * Embeds a search query. LEAF is an asymmetric retrieval encoder: queries carry the prompt
+ * its model card prescribes, documents do not; a symmetric engine may ignore the distinction.
+ */
+ fun embedQuery(text: String): FloatArray = embed(QUERY_PROMPT + text)
+
+ companion object {
+ /** The `query` prompt of `MongoDB/mdbr-leaf-ir` (sentence-transformers `prompt_name="query"`). */
+ const val QUERY_PROMPT = "Represent this sentence for searching relevant passages: "
+ }
+}
+
+/** The execution context every target uses; the kernel backend is picked by what is on the classpath. */
+fun defaultExecutionContext(): ExecutionContext = PooledExecutionContext(DirectCpuExecutionContext())
+
+/** LEAF on the SKaiNET-transformers BERT runtime: WordPiece tokenizer, encoder, mean pooling, 2_Dense, L2. */
+class LeafEmbeddingEngine(
+ private val runtime: BertEncoderRuntime,
+ private val tokenizer: BatchTokenizer,
+ val loadMillis: Long,
+) : EmbeddingEngine {
+ override val dimensions: Int get() = runtime.dimensions
+
+ override fun embed(text: String): FloatArray {
+ val encoding = tokenizer.encodeWithMetadata(text)
+ return runtime.encode(encoding.ids, encoding.attentionMask)
+ }
+
+ companion object {
+ /**
+ * Builds the engine from a [LeafSource]. The weights stream through the SafeTensors reader
+ * from disk or from the in-memory bytes; the projection head comes from the second file.
+ */
+ suspend fun load(
+ source: LeafSource,
+ ctx: ExecutionContext = defaultExecutionContext(),
+ mode: BertExecutionMode = BertExecutionMode.DIRECT,
+ onProgress: (String) -> Unit = {},
+ ): LeafEmbeddingEngine {
+ val started = TimeSource.Monotonic.markNow()
+ val files = when (source) {
+ is LeafSource.Directory -> DirectoryFiles(source.path)
+ is LeafSource.Bytes -> BytesFiles(source)
+ }
+ onProgress("Reading model configurationโฆ")
+ val config = BertConfigParser.parse(files.text(LeafFiles.CONFIG), files.text(LeafFiles.DENSE_CONFIG))
+ val pooling = BertConfigParser.parsePooling(files.text(LeafFiles.POOLING_CONFIG))
+ // LEAF's tokenizer.json is WordPiece; the engine returns the BERT-class tokenizer with masks.
+ val tokenizer = TextFrontEnds.fromTokenizerJson(files.text(LeafFiles.TOKENIZER), files.textOrNull(LeafFiles.TOKENIZER_CONFIG)) as? BatchTokenizer
+ ?: error("tokenizer.json is not a WordPiece/BERT-class tokenizer")
+
+ onProgress("Loading encoder weightsโฆ")
+ val loaders = listOf(
+ SafeTensorsParametersLoader(sourceProvider = { files.source(LeafFiles.MODEL) }),
+ SafeTensorsParametersLoader(sourceProvider = { files.source(LeafFiles.DENSE_MODEL) }),
+ )
+ val tensors = BertNetworkLoader.loadWeightTensors(loaders, ctx, FP32::class)
+ onProgress("Building encoderโฆ")
+ val runtime = createBertEncoderRuntime(config, tensors, ctx, mode = mode, pooling = pooling)
+ return LeafEmbeddingEngine(runtime, tokenizer, started.elapsedNow().inWholeMilliseconds)
+ }
+ }
+}
+
+private interface LeafFileAccess {
+ fun text(file: String): String
+ fun textOrNull(file: String): String?
+ fun source(file: String): RandomAccessSource
+}
+
+private class DirectoryFiles(private val dir: String) : LeafFileAccess {
+ private fun path(file: String) = "$dir/$file"
+ override fun text(file: String): String = readTextFile(path(file))
+ override fun textOrNull(file: String): String? = runCatching { readTextFile(path(file)) }.getOrNull()
+ override fun source(file: String): RandomAccessSource =
+ openRandomAccessSource(path(file)) ?: error("no random-access file source on this platform for ${path(file)}")
+}
+
+private class BytesFiles(private val bytes: LeafSource.Bytes) : LeafFileAccess {
+ override fun text(file: String): String = when (file) {
+ LeafFiles.CONFIG -> bytes.configJson
+ LeafFiles.DENSE_CONFIG -> bytes.denseConfigJson
+ LeafFiles.POOLING_CONFIG -> bytes.poolingConfigJson
+ LeafFiles.TOKENIZER -> bytes.tokenizerJson
+ LeafFiles.TOKENIZER_CONFIG -> bytes.tokenizerConfigJson ?: error("no tokenizer_config.json")
+ else -> error("not a text file: $file")
+ }
+
+ override fun textOrNull(file: String): String? = if (file == LeafFiles.TOKENIZER_CONFIG) bytes.tokenizerConfigJson else text(file)
+
+ override fun source(file: String): RandomAccessSource = when (file) {
+ LeafFiles.MODEL -> ByteArrayRandomAccessSource(bytes.model)
+ LeafFiles.DENSE_MODEL -> ByteArrayRandomAccessSource(bytes.denseModel)
+ else -> error("not a tensor file: $file")
+ }
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/LeafFiles.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/LeafFiles.kt
new file mode 100644
index 0000000..5f1b2fa
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/engine/LeafFiles.kt
@@ -0,0 +1,49 @@
+package sk.ainet.samples.emoji.engine
+
+/**
+ * The MongoDB LEAF retrieval encoder (`mdbr-leaf-ir`, Apache-2.0): a 6-layer BERT with hidden
+ * size 384, mean pooling and a `2_Dense` projection to 768 dimensions. Files are addressed by
+ * their path inside the Hugging Face repository, which is also the on-disk layout everywhere.
+ */
+object LeafFiles {
+ const val HF_REPO = "MongoDB/mdbr-leaf-ir"
+ const val HF_REVISION = "4262131b32c3182bd06e67e92ae69d7bd66e0c5c"
+
+ const val MODEL = "model.safetensors"
+ const val CONFIG = "config.json"
+ const val TOKENIZER = "tokenizer.json"
+ const val TOKENIZER_CONFIG = "tokenizer_config.json"
+ const val DENSE_MODEL = "2_Dense/model.safetensors"
+ const val DENSE_CONFIG = "2_Dense/config.json"
+ const val POOLING_CONFIG = "1_Pooling/config.json"
+
+ val ALL: List = listOf(MODEL, CONFIG, TOKENIZER, TOKENIZER_CONFIG, DENSE_MODEL, DENSE_CONFIG, POOLING_CONFIG)
+
+ /** Sizes at [HF_REVISION], for progress reporting and truncation checks. */
+ val SIZES: Map = mapOf(
+ MODEL to 90_272_656L,
+ CONFIG to 829L,
+ TOKENIZER to 711_661L,
+ TOKENIZER_CONFIG to 1_529L,
+ DENSE_MODEL to 1_182_880L,
+ DENSE_CONFIG to 137L,
+ POOLING_CONFIG to 321L,
+ )
+
+ fun url(file: String): String = "https://huggingface.co/$HF_REPO/resolve/$HF_REVISION/$file"
+}
+
+/** Where the checkpoint files are: a directory (desktop, Android) or already-read bytes (web). */
+sealed interface LeafSource {
+ data class Directory(val path: String) : LeafSource
+
+ class Bytes(
+ val model: ByteArray,
+ val denseModel: ByteArray,
+ val configJson: String,
+ val denseConfigJson: String,
+ val poolingConfigJson: String,
+ val tokenizerJson: String,
+ val tokenizerConfigJson: String?,
+ ) : LeafSource
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/model/LeafProvider.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/model/LeafProvider.kt
new file mode 100644
index 0000000..faafb19
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/model/LeafProvider.kt
@@ -0,0 +1,30 @@
+package sk.ainet.samples.emoji.model
+
+import sk.ainet.samples.emoji.engine.LeafSource
+
+/**
+ * Resolves the LEAF checkpoint onto this platform. Desktop and Android implementations do real
+ * file IO (cache hit โ streamed Hugging Face download, one file at a time); the web build reads
+ * the bundled Compose resources directly and needs no provider.
+ */
+interface LeafProvider {
+ suspend fun resolve(onProgress: (String) -> Unit): LeafSource
+}
+
+/** What to do for one checkpoint file, decided without touching any IO. */
+sealed interface FilePlan {
+ data class UseCached(val path: String) : FilePlan
+ data class Download(val path: String, val url: String) : FilePlan
+}
+
+/** Pure resolution policy: a present, complete file is reused; otherwise it is downloaded. */
+object LeafResolver {
+ fun plan(targetPath: String, url: String, existingBytes: Long?, expectedBytes: Long?): FilePlan = when {
+ existingBytes == null -> FilePlan.Download(targetPath, url)
+ expectedBytes != null && existingBytes < expectedBytes -> FilePlan.Download(targetPath, url)
+ else -> FilePlan.UseCached(targetPath)
+ }
+
+ /** Streamed downloads land here first, so an interrupted download never leaves a truncated target. */
+ fun partPath(targetPath: String): String = "$targetPath.part"
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/platform/Platform.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/platform/Platform.kt
new file mode 100644
index 0000000..b088989
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/platform/Platform.kt
@@ -0,0 +1,36 @@
+package sk.ainet.samples.emoji.platform
+
+import kotlin.time.TimeSource
+
+/** Every target this sample runs on โ the fixed set the platform-chips row displays. */
+enum class SamplePlatform(val label: String) {
+ ANDROID("Android"),
+ DESKTOP("Desktop"),
+ WEB("Web"),
+}
+
+/** Which of [SamplePlatform] this process is actually running as. */
+expect val currentSamplePlatform: SamplePlatform
+
+/** Human-readable label for the kernel path this platform/process is running โ shown in the UI. */
+expect fun kernelTierLabel(): String
+
+/** Reads a whole text file; only the filesystem targets (desktop, Android) implement it. */
+expect fun readTextFile(path: String): String
+
+/** Where the platform writes a log line โ `adb logcat` on Android, stdout elsewhere. */
+internal expect fun platformLog(line: String)
+
+private val processStart = TimeSource.Monotonic.markNow()
+
+/** One-line structured events: `event=... | +123ms | key=value`, greppable on every platform. */
+fun logEvent(name: String, vararg details: Pair) {
+ val line = buildString {
+ append("event=").append(name)
+ append(" | +").append(processStart.elapsedNow().inWholeMilliseconds).append("ms")
+ for ((key, value) in details) {
+ if (value != null) append(" | ").append(key).append('=').append(value)
+ }
+ }
+ platformLog(line)
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/verify/Verifier.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/verify/Verifier.kt
new file mode 100644
index 0000000..589d163
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/verify/Verifier.kt
@@ -0,0 +1,42 @@
+package sk.ainet.samples.emoji.verify
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import sk.ainet.samples.emoji.domain.CatalogItem
+
+/** A decision model's verdict on one candidate item for the current query. */
+data class Verdict(
+ val itemId: String,
+ /** `P(item matches query)` as the model sees it; null when the model failed on this item. */
+ val probabilityTrue: Double?,
+ /** The accepted decision under the policy; null when the model abstained or failed. */
+ val accepted: Boolean?,
+ val millis: Long,
+)
+
+sealed interface VerifierState {
+ /** No verifier built in, or the device cannot run it; the reason shows in the UI. */
+ data class Disabled(val reason: String) : VerifierState
+ data object Idle : VerifierState
+ data class Loading(val message: String) : VerifierState
+ data class Busy(val done: Int, val total: Int) : VerifierState
+ data class Error(val message: String) : VerifierState
+}
+
+/**
+ * The second stage ("System 2"): a typed decision model asked, per candidate, whether the
+ * item matches the query. Implementations stream one [Verdict] per item in rank order and
+ * stop as soon as the collector is cancelled, which happens whenever the query changes.
+ */
+interface Verifier {
+ val state: StateFlow
+ fun verify(query: String, items: List): Flow
+}
+
+/** The web build and any build without `-PlayaVerifier=true`. */
+class NoopVerifier(reason: String = "not built in") : Verifier {
+ override val state: StateFlow = MutableStateFlow(VerifierState.Disabled(reason))
+ override fun verify(query: String, items: List): Flow = emptyFlow()
+}
diff --git a/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/vm/EmojiSearchViewModel.kt b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/vm/EmojiSearchViewModel.kt
new file mode 100644
index 0000000..d060634
--- /dev/null
+++ b/SemanticEmojiFilter/shared/src/commonMain/kotlin/sk/ainet/samples/emoji/vm/EmojiSearchViewModel.kt
@@ -0,0 +1,163 @@
+package sk.ainet.samples.emoji.vm
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlin.time.TimeSource
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import sk.ainet.samples.emoji.domain.Calibration
+import sk.ainet.samples.emoji.domain.RankedItem
+import sk.ainet.samples.emoji.domain.Tier
+import sk.ainet.samples.emoji.engine.CatalogIndex
+import sk.ainet.samples.emoji.engine.EmbeddingEngine
+import sk.ainet.samples.emoji.platform.logEvent
+import sk.ainet.samples.emoji.verify.NoopVerifier
+import sk.ainet.samples.emoji.verify.Verdict
+import sk.ainet.samples.emoji.verify.Verifier
+
+sealed interface LoadState {
+ data object Idle : LoadState
+ data class Loading(val message: String) : LoadState
+ data object Ready : LoadState
+ data class Failed(val message: String) : LoadState
+}
+
+/** Timings the diagnostics row shows; all milliseconds. */
+data class SearchStats(
+ val modelLoadMs: Long = 0,
+ val indexBuildMs: Long = 0,
+ val lastQueryMs: Long = 0,
+ val p50QueryMs: Long = 0,
+ val queries: Int = 0,
+)
+
+/**
+ * Drives model loading, the per-keystroke ranking and the optional second-stage verifier.
+ * Everything platform-specific (where the checkpoint comes from, how the index is obtained,
+ * which verifier exists) is injected, so the class runs under `runTest` with fakes.
+ *
+ * Ranking: the query flow is debounced by [debounceMs]; `mapLatest` drops a sweep when a newer
+ * query arrives. Verification: once the ranking has been stable for [stableMs], the top
+ * [verifierTopK] items go to the [verifier], whose verdicts stream in one by one and are
+ * cleared the moment the query changes.
+ */
+@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
+class EmojiSearchViewModel(
+ private val loadEngine: suspend (onProgress: (String) -> Unit) -> EmbeddingEngine,
+ private val loadIndex: suspend (engine: EmbeddingEngine, onProgress: (String) -> Unit) -> CatalogIndex,
+ private val calibration: Calibration,
+ val verifier: Verifier = NoopVerifier(),
+ private val embedDispatcher: CoroutineDispatcher = Dispatchers.Default,
+ private val debounceMs: Long = 120,
+ private val stableMs: Long = 500,
+ private val verifierTopK: Int = 6,
+) : ViewModel() {
+
+ val query = MutableStateFlow("")
+
+ private val _status = MutableStateFlow(LoadState.Idle)
+ val status: StateFlow = _status
+
+ private val _ranked = MutableStateFlow>(emptyList())
+ val ranked: StateFlow> = _ranked
+
+ private val _verdicts = MutableStateFlow