Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
84d05a3
Align the 26-key letter hints with the reference skin
Slacker-LLC Oct 14, 2026
108be29
Align key weights, the nine-key column and candidate spacing with the…
Slacker-LLC Oct 14, 2026
559c711
Align the nine-key reading rail with the reference filter list
Slacker-LLC Oct 14, 2026
5cc0440
Type the zero through the space bar in the rail stress case
Slacker-LLC Oct 14, 2026
1ccb9ef
Skip the stale-capture clipboard tests while this app is the active IME
Slacker-LLC Oct 14, 2026
ffe8db9
Allow a side-by-side development build through openimeAppIdSuffix
Slacker-LLC Oct 14, 2026
5ed5710
Rank candidates that spell out every typed letter above fragment matches
Slacker-LLC Oct 14, 2026
f921b1e
Offer the reference skin's URL marks in the nine-key rail
Slacker-LLC Oct 14, 2026
3781cfb
Draw the first candidate in the accent, not on a filled tile
Slacker-LLC Oct 14, 2026
174aac8
Draw the expanded page the way the reference ships it
Slacker-LLC Oct 14, 2026
6bdaae6
Record the alignment work in the change log
Slacker-LLC Oct 14, 2026
94616ec
Ship the reference's seven-key bottom row and its @# nine-key cell
Slacker-LLC Oct 14, 2026
0538ce1
Split the pre-edit into the reference's two colours
Slacker-LLC Oct 14, 2026
98e3806
Lay the candidate strip out as the reference does
Slacker-LLC Oct 14, 2026
147a2a0
Give the idle toolbar the reference's pill keys
Slacker-LLC Oct 14, 2026
bc1c806
Lay the number page out as the reference's own number layout
Slacker-LLC Oct 14, 2026
530311f
Give the stroke board the reference's nine-cell grid
Slacker-LLC Oct 14, 2026
852f792
Put the idle toolbar in the reference's order and its own scrolling s…
Slacker-LLC Oct 14, 2026
1bdf76c
Move the symbol page's categories into the reference's bottom strip
Slacker-LLC Oct 14, 2026
a771496
Give the symbol page its own 123 and delete keys
Slacker-LLC Oct 14, 2026
d18ad19
List the typed symbols in the symbol board's toolbar
Slacker-LLC Oct 14, 2026
54138e4
Draw the launcher pages on the reference's tile grid
Slacker-LLC Oct 14, 2026
6b6a199
Meet the floating keyboard with the reference's tip banner
Slacker-LLC Oct 14, 2026
45bb047
Cap the pre-edit at the reference's half of the page
Slacker-LLC Oct 14, 2026
1d51bc1
Wear the reference's quick-input row for our own suggestions
Slacker-LLC Oct 14, 2026
59b03c7
Point every new citation at its own line
Slacker-LLC Oct 14, 2026
111604f
Ride the suggestion's origin glyph above its text
Slacker-LLC Oct 14, 2026
dac5435
Point two candidate-strip citations at the lines they quote
Slacker-LLC Oct 14, 2026
c2ba905
Measure the origin glyph from the line it marks
Slacker-LLC Oct 14, 2026
167d9f7
Settle the sixteen instrumented failures the alignment left
Slacker-LLC Oct 14, 2026
6b5be72
Draw every key cap with its own class's reference margins
Slacker-LLC Oct 14, 2026
2c51a43
Round every key and the long-press bubble the way the skin does
Slacker-LLC Oct 14, 2026
cd34267
Shrink a one-line mark to its key instead of trusting autosize
Slacker-LLC Oct 14, 2026
110b2c8
Read the rail in the one font and margin the skin gives it
Slacker-LLC Oct 14, 2026
ae1e6f2
Read every key face at the size the reference gives it
Slacker-LLC Oct 14, 2026
a0ccbab
Split the composing strip the way the reference splits its own
Slacker-LLC Oct 14, 2026
fe9c987
Let the key fills trade places on press, and put the shadow back unde…
Slacker-LLC Oct 14, 2026
f9ceba7
Fill each pressed surface with the reference's own colour
Slacker-LLC Oct 14, 2026
b7a1546
Give the corner hints, idle toolbar and strip split their own colours
Slacker-LLC Oct 14, 2026
7097ccf
Let the dark skin's first candidate be plain, and log the day's ports
Slacker-LLC Oct 14, 2026
693a019
Press tiles and toolbar keys onto their own page, not into the text
Slacker-LLC Oct 14, 2026
0711907
Give the number page's return key the skin's theme colour
Slacker-LLC Oct 14, 2026
1ad3427
Read the switch page and the English board from their own classes
Slacker-LLC Oct 14, 2026
6d2aa7d
Draw the nine-key and stroke bottom rows on white keys
Slacker-LLC Oct 14, 2026
a3bc8c1
Draw the panel header's close key as the reference's circle
Slacker-LLC Oct 14, 2026
20078dd
Record the reference surfaces checked and left unported
Slacker-LLC Oct 14, 2026
f717f6f
Paint the expanded candidates page from its own classes
Slacker-LLC Oct 14, 2026
0bb3ce0
Note the rail's custom-symbol class among the unported ones
Slacker-LLC Oct 15, 2026
9311ff7
Keep the board area's reference side inset
Slacker-LLC Oct 15, 2026
993f240
Give the expanded candidates page its own side inset
Slacker-LLC Oct 15, 2026
daf9556
Record why the landscape faces keep one compactness factor
Slacker-LLC Oct 15, 2026
997ffda
Record the colour sweep and the accent-key label rule
Slacker-LLC Oct 15, 2026
6a39de1
Revert the portrait board's reference side inset
Slacker-LLC Oct 15, 2026
61e15b2
Add the reference's pinyin display setting: candidate bar or input box
Slacker-LLC Oct 15, 2026
4f994f6
Record the reference's candidate size scale
Slacker-LLC Oct 15, 2026
27c9d63
Keep the best syllable paths, not the first ones
Slacker-LLC Oct 15, 2026
441d487
Reach the word table for the reading the user typed
Slacker-LLC Oct 15, 2026
8078ba9
Lead the candidate bar with the reading when no word spells it
Slacker-LLC Oct 15, 2026
cbe6909
Return the committed text and the candidate bar to the volume run
Slacker-LLC Oct 15, 2026
f221ef9
Read the sentence words off the lattice, one position at a time
Slacker-LLC Oct 15, 2026
31ddfce
Spell the syllables the reference spells with v
Slacker-LLC Oct 15, 2026
9d6f480
Lead the 26-key bar with the words the user picked before
Slacker-LLC Oct 15, 2026
33dc757
Let one initial and one final fuzzy pair apply together
Slacker-LLC Oct 15, 2026
c204091
Let a fuzzy spelling be one the catalogue has no syllable for
Slacker-LLC Oct 15, 2026
39b674d
Carry the four fuzzy pairs the reference has beyond the eleven
Slacker-LLC Oct 15, 2026
3f42ad8
Show a committed word the emoji the reference gives it
Slacker-LLC Oct 15, 2026
6865cd4
Record why the stroke candidates are not in the reference's order
Slacker-LLC Oct 15, 2026
4830658
Call the association setting what the reference calls it
Slacker-LLC Oct 15, 2026
ac44805
Name the double-pinyin schemes the way the reference does
Slacker-LLC Oct 15, 2026
12b51e2
Turn on the wubi four-code commit the reference defaults to
Slacker-LLC Oct 15, 2026
fcae0dd
Start the six fuzzy pairs the reference starts
Slacker-LLC Oct 15, 2026
55d979a
Read the words the reference reads differently
Slacker-LLC Oct 15, 2026
8614395
Read the letter-hint order from the reference on P and L
Slacker-LLC Oct 15, 2026
ea7e255
Record the two differences the preference sweep leaves
Slacker-LLC Oct 15, 2026
46e0348
Keep one long-press order on every key
Slacker-LLC Oct 15, 2026
efa79cf
Give the floating card a touchable area again
Slacker-LLC Oct 15, 2026
143b2b2
Stop re-measuring the keyboard on every frame
Slacker-LLC Oct 15, 2026
ca2cf02
Let the top zone claim the gestures that land on bare space
Slacker-LLC Oct 15, 2026
21771a7
Write the floating window only when the card really moves
Slacker-LLC Oct 15, 2026
8e859fb
Stop a press with no finger from blocking every later rebuild
Slacker-LLC Oct 15, 2026
9034005
Preview the keyboard height while dragging and save it once
Slacker-LLC Oct 15, 2026
423de18
Record the four touch-path fixes in the changelog
Slacker-LLC Oct 15, 2026
d4f0089
Find the top zone's blank band next to the icon strip
Slacker-LLC Oct 15, 2026
fd8f235
Add the per-keyboard volume E2E run and its input builder
Slacker-LLC Oct 14, 2026
fa0d93d
Type every common character on every Chinese keyboard
Slacker-LLC Oct 14, 2026
5314611
Recover the volume run when the keyboard is taken away
Slacker-LLC Oct 14, 2026
b0f3840
Keep the screen on while the volume run works
Slacker-LLC Oct 14, 2026
78ae3b4
Record every row the volume run types
Slacker-LLC Oct 15, 2026
c05f11f
Abort the volume run when the device has a different APK
Slacker-LLC Oct 15, 2026
bc97f37
Insert a line break on a long press of a send key
Slacker-LLC Oct 15, 2026
8754912
Split a clipboard entry into words on a long press
Slacker-LLC Oct 15, 2026
1cb1fc4
Recommend the poem line the composition spells
Slacker-LLC Oct 15, 2026
86ebaab
Port four more reference input behaviours
Slacker-LLC Oct 15, 2026
dfac7b1
Add the 快捷格式输入 settings and wire the batch into the service
Slacker-LLC Oct 15, 2026
3070e33
Give nine-key the reference's reading list and its digit-level fuzzy
Slacker-LLC Oct 15, 2026
0f5a71e
Restore the original keyboard layouts and return key styling
Slacker-LLC Oct 15, 2026
3304d29
Match the settings accent, centre the launcher pages and accept the o…
Slacker-LLC Oct 15, 2026
7808c86
Release 1.0.0
Slacker-LLC Oct 15, 2026
ad2a73c
Merge remote-tracking branch 'origin/main' into align/letter-hints
Slacker-LLC Oct 15, 2026
342d5cb
Stop the floating-insets test from calling a hidden framework method
Slacker-LLC Oct 15, 2026
fb3cdda
Let the release's click run before a pending row rebuild
Slacker-LLC Oct 15, 2026
75f3d6e
Merge align/letter-hints into test/volume-e2e
Slacker-LLC Oct 15, 2026
a4be7b9
Merge main into test/volume-e2e
Slacker-LLC Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/TEST_SOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,17 @@ L3 stress covers at least these items:

Check CPU, PSS, heat, model loading and recovery from low memory.

The automated volume run covers the same ground on the emulator, one keyboard at a time:

```bash
python3 scripts/build_volume_data.py # input lists from the local dictionaries
ANDROID_SERIAL=emulator-5556 python3 -I scripts/typing_volume_e2e.py --serial emulator-5556
```

Each Chinese keyboard types 2,000 random keys, 1,000 words, 500 four-character words and 100 sentences of fifteen characters or more; the digit, wubi, stroke and English keyboards run the counts that apply to them. A word counts as a hit only when the committed text is the expected text; a sentence is scored by character accuracy. A miss keeps the expected text, the committed text and the candidate list in the report, so a run shows where the engine ranks a worse candidate above the right one.

The run writes `summary.json` and one report per keyboard into `build/typing-volume`. Both scripts read the data already on this machine and write nothing into the repository.

Failure tests create these conditions on purpose:

- a revoked permission
Expand Down
261 changes: 261 additions & 0 deletions scripts/build_volume_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""Build the input lists for the volume E2E harness (scripts/typing_volume_e2e.py).

Everything comes from data already on this machine: the bundled dictionaries
(base.dict.yaml, 8105.dict.yaml, wubi86.dict.yaml, english-words.tsv) and the
Chinese/English prose in this repository's own documents. Nothing is
downloaded and nothing is committed; the output lands in build/volume-data/.

python3 scripts/build_volume_data.py [--out DIR]

Files written (tab separated, no header):

words.tsv word, pinyin 1000 common two- and three-character words
four.tsv word, pinyin 500 four-character words (many are idioms)
sentences.tsv sentence, pinyin 100 Chinese sentences of 15+ characters
wubi.tsv word, code 500 words with their wubi86 code
chars-wubi.tsv char, code 500 single characters with their wubi86 code
chars.tsv char, pinyin 1000 single characters by frequency
english.tsv word 1000 English completion words by rank
en-sentences.tsv sentence 100 English sentences of 15+ characters
"""

from __future__ import annotations

import argparse
import random
import re
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
ASSETS = ROOT / "app" / "src" / "main" / "assets"
DICTS = ASSETS / "rime-data" / "openime_dicts"
CJK = re.compile(r"^[㐀-鿿豈-﫿]+$")
SYLLABLE = re.compile(r"^[a-z]+$")
SENTENCE_CHARS = re.compile(r"^[㐀-鿿豈-﫿,。!?、;:()《》“”‘’…—]+$")
LATIN_SENTENCE = re.compile(r"^[A-Za-z0-9 ,.'()/\-+#%:_\[\]]+$")


# Well-known idioms the dictionaries carry; the dictionary's own reading is used.
IDIOMS = (
"一心一意 三心二意 画蛇添足 掩耳盗铃 刻舟求剑 守株待兔 走马观花 锦上添花 半途而废 百发百中 "
"大器晚成 一箭双雕 亡羊补牢 对牛弹琴 杯弓蛇影 井底之蛙 鹤立鸡群 瓜熟蒂落 流水不腐 户枢不蠹 "
"塞翁失马 杞人忧天 叶公好龙 指鹿为马 望梅止渴 骑虎难下 如鱼得水 雪中送炭 锦囊妙计 抛砖引玉 "
"破釜沉舟 卧薪尝胆 负荆请罪 完璧归赵 四面楚歌 三顾茅庐 草木皆兵 风声鹤唳 势如破竹 如火如荼 "
"水到渠成 水滴石穿 铁杵成针 悬梁刺股 凿壁偷光 囊萤映雪 精益求精 融会贯通 举一反三 循序渐进 "
"温故知新 学以致用 实事求是 一丝不苟 兢兢业业 任重道远 同心协力 众志成城 各抒己见 畅所欲言"
).split()


def read_dict(path: Path, per_character_syllables: bool = True) -> list[tuple[str, str, int]]:
"""Entries of a Rime dictionary; the wubi table stores one code, not syllables."""
entries: list[tuple[str, str, int]] = []
inside = False
for line in path.read_text(encoding="utf-8").splitlines():
if line.startswith("..."):
inside = True
continue
if not inside or not line or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) < 3:
continue
word, pinyin, weight = parts[0], parts[1], parts[2]
if not CJK.match(word) or not weight.isdigit():
continue
syllables = pinyin.split()
if not syllables or not all(SYLLABLE.match(s) for s in syllables):
continue
if per_character_syllables and len(syllables) != len(word):
continue
entries.append((word, pinyin, int(weight)))
return entries


def spread(entries: list[tuple[str, str, int]], count: int, seed: int) -> list[tuple[str, str, int]]:
"""Take [count] entries spread over the list instead of one weight band."""
if len(entries) <= count:
return entries
step = len(entries) / count
return [entries[int(i * step)] for i in range(count)]


def chinese_sentences(limit: int, minimum: int = 15) -> list[str]:
"""Clauses of fifteen characters or more from this repository's Chinese prose."""
found: list[str] = []
seen: set[str] = set()
sources = [ROOT / "CHANGELOG.md", ROOT / "README.zh-CN.md"]
sources += sorted((ROOT / "docs").glob("*.md"))
for path in sources:
if not path.is_file():
continue
text = path.read_text(encoding="utf-8")
for line in text.splitlines():
clauses: list[str] = []
for piece in re.split(r"[。!?;,、,::\n]", line):
sentence = piece.strip().strip(">*-—·  ")
if any(mark in sentence for mark in "`[]()|/\\<>#\"'"):
continue
if not SENTENCE_CHARS.match(sentence):
continue
clauses.append(sentence)
# Long documents are written in clauses shorter than the requested
# length, so neighbouring clauses of one line are joined with a
# comma until the sentence is long enough.
sentence = ""
for clause in clauses:
if len(sentence) + len(clause) > 26:
if len(sentence) >= minimum and sentence not in seen:
seen.add(sentence)
found.append(sentence)
sentence = ""
sentence = f"{sentence},{clause}" if sentence else clause
if len(sentence) >= minimum and sentence not in seen:
seen.add(sentence)
found.append(sentence)
return found[:limit]


def english_sentences(limit: int, minimum: int = 15) -> list[str]:
found: list[str] = []
seen: set[str] = set()
for name in ("README.md", "docs/RELEASE.md", "docs/DECISIONS.md", "docs/REPOSITORY.md"):
path = ROOT / name
if not path.is_file():
continue
for piece in re.split(r"[.!?]\s|\n", path.read_text(encoding="utf-8")):
sentence = piece.strip().strip(">*-—# \t")
if not (minimum <= len(sentence) <= 80):
continue
if sentence.count(" ") < 3 or not LATIN_SENTENCE.match(sentence):
continue
if sum(ch.isalpha() for ch in sentence) < minimum * 0.7:
continue
if sentence in seen:
continue
seen.add(sentence)
found.append(sentence)
return found[:limit]


def build_pinyin_lookup(chars: list[tuple[str, str, int]], words: list[tuple[str, str, int]]) -> dict[str, str]:
"""Longest-match syllable source: the dictionaries' own readings."""
lookup: dict[str, str] = {}
for word, pinyin, _ in words:
lookup.setdefault(word, pinyin)
for char, pinyin, _ in chars:
lookup.setdefault(char, pinyin)
return lookup


def sentence_pinyin(sentence: str, lookup: dict[str, str], longest: int = 6) -> str | None:
"""Read the sentence as spoken pinyin, splitting on the dictionary's words."""
syllables: list[str] = []
index = 0
while index < len(sentence):
for size in range(min(longest, len(sentence) - index), 0, -1):
piece = sentence[index:index + size]
pinyin = lookup.get(piece)
if pinyin is None:
continue
if not all(SYLLABLE.match(part) for part in pinyin.split()):
continue
syllables += pinyin.split()
index += size
break
else:
return None
return " ".join(syllables)


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--out", type=Path, default=ROOT / "build" / "volume-data")
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=True)

base = read_dict(DICTS / "base.dict.yaml")
chars_8105 = read_dict(DICTS / "8105.dict.yaml")
wubi = read_dict(DICTS / "wubi86.dict.yaml", per_character_syllables=False)

# Words: two and three characters, spread over the whole list so both very
# common and tail vocabulary show up.
by_length: dict[int, list[tuple[str, str, int]]] = {2: [], 3: []}
for entry in sorted(base, key=lambda item: -item[2]):
if len(entry[0]) in by_length:
by_length[len(entry[0])].append(entry)
words = spread(by_length[2], 700, 0) + spread(by_length[3], 300, 1)
(args.out / "words.tsv").write_text(
"".join(f"{word}\t{pinyin}\n" for word, pinyin, _ in words), encoding="utf-8",
)

four = spread([entry for entry in sorted(base, key=lambda item: -item[2]) if len(entry[0]) == 4], 450, 2)
readings = {word: pinyin for word, pinyin, _ in base}
curated = [
(word, readings[word]) for word in IDIOMS if word in readings
]
four = [(word, pinyin, 0) for word, pinyin in curated] + four[: max(0, 500 - len(curated))]
(args.out / "four.tsv").write_text(
"".join(f"{word}\t{pinyin}\n" for word, pinyin, _ in four), encoding="utf-8",
)

lookup = build_pinyin_lookup(chars_8105, base)
sentences = []
for sentence in chinese_sentences(400):
# The harness types syllables and compares characters, so the target
# text carries the characters alone.
text = "".join(ch for ch in sentence if CJK.match(ch))
if not 15 <= len(text) <= 26:
continue
pinyin = sentence_pinyin(text, lookup)
if pinyin and len(pinyin) <= 90:
sentences.append((text, pinyin))
if len(sentences) >= 100:
break
(args.out / "sentences.tsv").write_text(
"".join(f"{sentence}\t{pinyin}\n" for sentence, pinyin in sentences), encoding="utf-8",
)

wubi_words = [entry for entry in wubi if len(entry[0]) in (2, 3) and 2 <= len(entry[1]) <= 4]
wubi_sample = spread(sorted(wubi_words, key=lambda item: -item[2]), 500, 3)
(args.out / "wubi.tsv").write_text(
"".join(f"{word}\t{code}\n" for word, code, _ in wubi_sample), encoding="utf-8",
)

# Single characters by their own wubi86 code, so the character coverage
# also reaches the wubi keyboard.
wubi_chars = [entry for entry in wubi if len(entry[0]) == 1 and 1 <= len(entry[1]) <= 4]
wubi_chars_sample = spread(sorted(wubi_chars, key=lambda item: -item[2]), 500, 7)
(args.out / "chars-wubi.tsv").write_text(
"".join(f"{char}\t{code}\n" for char, code, _ in wubi_chars_sample), encoding="utf-8",
)

chars = spread(sorted(chars_8105, key=lambda item: -item[2])[:3000], 1000, 4)
(args.out / "chars.tsv").write_text(
"".join(f"{char}\t{pinyin}\n" for char, pinyin, _ in chars), encoding="utf-8",
)

english: list[tuple[str, str, int]] = []
for line in (ASSETS / "english-words.tsv").read_text(encoding="utf-8").splitlines():
parts = line.split("\t")
if len(parts) == 3:
english.append((parts[0], parts[1], int(parts[2])))
english_sample = spread(english, 1000, 5)
(args.out / "english.tsv").write_text(
"".join(f"{lower}\n" for lower, _, _ in english_sample), encoding="utf-8",
)
random.Random(6).shuffle(english_sample)

en_sentences = english_sentences(100)
(args.out / "en-sentences.tsv").write_text(
"".join(f"{sentence}\n" for sentence in en_sentences), encoding="utf-8",
)

for name in ("words", "four", "sentences", "wubi", "chars", "chars-wubi", "english", "en-sentences"):
path = args.out / f"{name}.tsv"
print(f"{name}: {len(path.read_text(encoding='utf-8').splitlines())} lines")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading