Skip to content

Commit 5fd2417

Browse files
Merge pull request #605 from KazumaProject/fix/typo-correction
フリックキーボードのタイピングミスの修正をキーの位置を考慮して行うように修正
2 parents b27df21 + a524742 commit 5fd2417

9 files changed

Lines changed: 238 additions & 29 deletions

File tree

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ android {
2929
applicationId "com.kazumaproject.markdownhelperkeyboard"
3030
minSdk 24
3131
targetSdk 36
32-
versionCode 661
33-
versionName "1.4.540"
32+
versionCode 662
33+
versionName "1.4.541"
3434
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
3535
}
3636

app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/EnglishEngine.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class EnglishEngine {
178178
// 2) typo(補正) - enableTypoCorrection のときだけ
179179
// ============
180180
if (enableTypoCorrection && typoCorrection.isNotEmpty()) {
181-
val typoType = 34.toByte()
181+
val typoType = 35.toByte()
182182
val predictiveSet = predictiveSearchReading.toHashSet()
183183

184184
val maxEdits = maxEditsByLength(lowerInput.length)

app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/graph/GraphBuilder.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class GraphBuilder {
1717

1818
companion object {
1919
private const val OMISSION_SCORE_OFFSET = 1900
20-
private const val TYPO_SCORE_OFFSET = 1600
20+
private const val TYPO_SCORE_OFFSET = 2200
2121
}
2222

2323
/**
@@ -193,20 +193,20 @@ class GraphBuilder {
193193
}
194194

195195
// 3.x システム辞書 (Typo Correction Prefix)
196-
if (enableTypoCorrectionJapaneseFlick && subStr.length > 1) {
196+
if (enableTypoCorrectionJapaneseFlick && subStr.length > 2) {
197197
val typoPrefixResults = yomiTrie.commonPrefixSearchWithTypoCorrectionPrefix(
198198
str = subStr,
199199
succinctBitVector = succinctBitVectorLBSYomi,
200-
maxEdits = 1,
200+
maxResults = 98,
201201
maxLen = 12, // ここは調整(予測変換の最大長に合わせる)
202202
)
203203

204204
// 見つかったら辞書ヒット扱いにして未知語フォールバック抑制
205205
if (typoPrefixResults.isNotEmpty()) foundInAnyDictionary = true
206206

207207
for (typo in typoPrefixResults) {
208-
// edits=0 は通常検索と重複しやすいのでスキップ推奨
209-
if (typo.editsUsed == 0) continue
208+
// penaltyUsed==0 は通常検索と重複しやすいのでスキップ推奨
209+
if (typo.penaltyUsed == 0) continue
210210

211211
val yomiStr = typo.yomi
212212
val nodeIndex = yomiTrie.getNodeIndex(yomiStr, succinctBitVectorLBSYomi)
@@ -219,7 +219,7 @@ class GraphBuilder {
219219
)
220220

221221
val endIndex = i + yomiStr.length
222-
val penalty = TYPO_SCORE_OFFSET * typo.editsUsed
222+
val penalty = TYPO_SCORE_OFFSET * typo.penaltyUsed
223223

224224
listToken
225225
.sortedBy { it.wordCost }

app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/graph/TypoCorrectionResult.kt

Lines changed: 0 additions & 3 deletions
This file was deleted.

app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/louds/with_term_id/LOUDSWithTermId.kt

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import com.kazumaproject.connection_id.deflate
1111
import com.kazumaproject.connection_id.inflate
1212
import com.kazumaproject.markdownhelperkeyboard.converter.bitset.SuccinctBitVector
1313
import com.kazumaproject.markdownhelperkeyboard.converter.graph.OmissionSearchResult
14-
import com.kazumaproject.markdownhelperkeyboard.converter.graph.TypoCorrectionResult
14+
import com.kazumaproject.markdownhelperkeyboard.converter.typo_correction.FlickDir
15+
import com.kazumaproject.markdownhelperkeyboard.converter.typo_correction.KanaFlickLayout
16+
import com.kazumaproject.markdownhelperkeyboard.converter.typo_correction.TypoCandidate
17+
import com.kazumaproject.markdownhelperkeyboard.converter.typo_correction.TypoCategory
18+
import com.kazumaproject.markdownhelperkeyboard.converter.typo_correction.TypoCorrectionResult
1519
import com.kazumaproject.toBitSet
1620
import com.kazumaproject.toByteArray
1721
import com.kazumaproject.toByteArrayFromListChar
@@ -706,32 +710,53 @@ class LOUDSWithTermId {
706710
fun commonPrefixSearchWithTypoCorrectionPrefix(
707711
str: String,
708712
succinctBitVector: SuccinctBitVector,
709-
maxEdits: Int = 1,
713+
maxPenalty: Int = 2,
710714
maxLen: Int = 12,
715+
maxResults: Int = 64, // 任意: 暴発防止
711716
): List<TypoCorrectionResult> {
712-
val results = LinkedHashSet<TypoCorrectionResult>()
713-
val sb = StringBuilder()
714717

715-
fun dfs(strIndex: Int, nodeIndex: Int, editsUsed: Int) {
716-
// ★ 「途中でも leaf なら採用」(ただし空文字は除外)
717-
if (sb.isNotEmpty() && isLeaf[nodeIndex]) {
718-
results.add(TypoCorrectionResult(sb.toString(), editsUsed))
718+
// 同一yomiの重複を最小penaltyで集約
719+
val bestPenaltyByYomi = HashMap<String, Int>(128)
720+
val sb = StringBuilder(maxLen)
721+
722+
fun acceptIfLeaf(nodeIndex: Int, penaltyUsed: Int) {
723+
if (sb.isEmpty()) return
724+
if (!isLeaf[nodeIndex]) return
725+
726+
val yomi = sb.toString()
727+
val prev = bestPenaltyByYomi[yomi]
728+
if (prev == null || penaltyUsed < prev) {
729+
bestPenaltyByYomi[yomi] = penaltyUsed
719730
}
731+
}
732+
733+
fun dfs(strIndex: Int, nodeIndex: Int, penaltyUsed: Int) {
734+
if (penaltyUsed > maxPenalty) return
735+
if (sb.length > maxLen) return
736+
if (bestPenaltyByYomi.size >= maxResults) return
737+
738+
// ★ prefix検索: 途中でも leaf なら採用
739+
acceptIfLeaf(nodeIndex, penaltyUsed)
720740

741+
// 入力を使い切ったら終了(prefixなのでここで止める)
721742
if (strIndex >= str.length) return
722743
if (sb.length >= maxLen) return
723744

724745
val ch = str[strIndex]
725-
for (variant in getCharTypeCorrectionVariations(ch)) {
726-
val nextEdits = editsUsed + if (variant == ch) 0 else 1
727-
if (nextEdits > maxEdits) continue
746+
747+
// ★ 候補をペナルティ昇順で展開(枝刈り)
748+
val candidates: List<TypoCandidate> = getTypoCandidates(ch)
749+
750+
for (cand in candidates) {
751+
val nextPenalty = penaltyUsed + cand.penalty
752+
if (nextPenalty > maxPenalty) continue
728753

729754
var childPos = firstChild(nodeIndex, succinctBitVector)
730755
while (childPos >= 0 && LBS[childPos]) {
731756
val labelNodeId = succinctBitVector.rank1(childPos)
732-
if (labelNodeId < labels.size && labels[labelNodeId] == variant) {
733-
sb.append(variant)
734-
dfs(strIndex + 1, childPos, nextEdits)
757+
if (labelNodeId < labels.size && labels[labelNodeId] == cand.ch) {
758+
sb.append(cand.ch)
759+
dfs(strIndex + 1, childPos, nextPenalty)
735760
sb.setLength(sb.length - 1)
736761
break
737762
}
@@ -740,8 +765,17 @@ class LOUDSWithTermId {
740765
}
741766
}
742767

743-
dfs(strIndex = 0, nodeIndex = 0, editsUsed = 0)
744-
return results.toList()
768+
// ルート開始
769+
dfs(strIndex = 0, nodeIndex = 0, penaltyUsed = 0)
770+
771+
// 出力整形: penalty昇順 → 長さ降順(好み)→ 文字列昇順
772+
return bestPenaltyByYomi.entries
773+
.sortedWith(
774+
compareBy<Map.Entry<String, Int>> { it.value }
775+
.thenByDescending { it.key.length }
776+
.thenBy { it.key }
777+
)
778+
.map { TypoCorrectionResult(it.key, it.value) }
745779
}
746780

747781
private fun searchRecursiveWithTypoCorrection(
@@ -855,4 +889,39 @@ class LOUDSWithTermId {
855889
return (listOf(char) + neighbors).distinct()
856890
}
857891

892+
private fun getTypoCandidates(ch: Char): List<TypoCandidate> {
893+
val key = KanaFlickLayout.keyOf(ch) ?: return listOf(TypoCandidate(ch, TypoCategory.Exact))
894+
895+
val out = ArrayList<TypoCandidate>(16)
896+
897+
// 1) Exact
898+
out.add(TypoCandidate(ch, TypoCategory.Exact))
899+
900+
// 2) TapKeyInFlick: 同じgroup内の別dir
901+
for (dir in FlickDir.entries) {
902+
if (dir == key.dir) continue
903+
val v = KanaFlickLayout.charOf(key.group, dir) ?: continue
904+
out.add(TypoCandidate(v, TypoCategory.TapKeyInFlick))
905+
}
906+
907+
// 3) DistanceNear/Middle/Far: 同dirのまま別groupへ
908+
for (g in KanaFlickLayout.allGroups()) {
909+
if (g == key.group) continue
910+
val v = KanaFlickLayout.charOf(g, key.dir) ?: continue
911+
912+
val dist = KanaFlickLayout.manhattan(key.group, g)
913+
val cat = when (dist) {
914+
1 -> TypoCategory.DistanceNear
915+
2 -> TypoCategory.DistanceMiddle
916+
else -> TypoCategory.DistanceFar
917+
}
918+
out.add(TypoCandidate(v, cat))
919+
}
920+
921+
// 重複排除 + penalty昇順(探索枝刈りに効く)
922+
return out
923+
.distinctBy { it.ch to it.category } // 同じ文字が複数カテゴリに入る設計にしたいならここは要調整
924+
.sortedWith(compareBy<TypoCandidate> { it.penalty }.thenBy { it.ch })
925+
}
926+
858927
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package com.kazumaproject.markdownhelperkeyboard.converter.typo_correction
2+
3+
enum class KeyGroup { A, KA, SA, TA, NA, HA, MA, YA, RA, WA }
4+
5+
enum class FlickDir { CENTER, LEFT, UP, RIGHT, DOWN } // a,i,u,e,o
6+
7+
data class KeyPos(val x: Int, val y: Int)
8+
data class KanaKey(val group: KeyGroup, val dir: FlickDir)
9+
10+
11+
object KanaFlickLayout {
12+
13+
// 12キーの代表的な配置
14+
private val pos = mapOf(
15+
KeyGroup.A to KeyPos(0, 0),
16+
KeyGroup.KA to KeyPos(1, 0),
17+
KeyGroup.SA to KeyPos(2, 0),
18+
19+
KeyGroup.TA to KeyPos(0, 1),
20+
KeyGroup.NA to KeyPos(1, 1),
21+
KeyGroup.HA to KeyPos(2, 1),
22+
23+
KeyGroup.MA to KeyPos(0, 2),
24+
KeyGroup.YA to KeyPos(1, 2),
25+
KeyGroup.RA to KeyPos(2, 2),
26+
27+
KeyGroup.WA to KeyPos(0, 3),
28+
)
29+
30+
// 各キーグループ内の5方向(CENTER/LEFT/UP/RIGHT/DOWN)
31+
// ※必要なら拗音/濁点/半濁点は別カテゴリで拡張可能
32+
private val table: Map<KeyGroup, Map<FlickDir, Char>> = mapOf(
33+
KeyGroup.A to mapOf(
34+
FlickDir.CENTER to '',
35+
FlickDir.LEFT to '',
36+
FlickDir.UP to '',
37+
FlickDir.RIGHT to '',
38+
FlickDir.DOWN to ''
39+
),
40+
KeyGroup.KA to mapOf(
41+
FlickDir.CENTER to '',
42+
FlickDir.LEFT to '',
43+
FlickDir.UP to '',
44+
FlickDir.RIGHT to '',
45+
FlickDir.DOWN to ''
46+
),
47+
KeyGroup.SA to mapOf(
48+
FlickDir.CENTER to '',
49+
FlickDir.LEFT to '',
50+
FlickDir.UP to '',
51+
FlickDir.RIGHT to '',
52+
FlickDir.DOWN to ''
53+
),
54+
KeyGroup.TA to mapOf(
55+
FlickDir.CENTER to '',
56+
FlickDir.LEFT to '',
57+
FlickDir.UP to '',
58+
FlickDir.RIGHT to '',
59+
FlickDir.DOWN to ''
60+
),
61+
KeyGroup.NA to mapOf(
62+
FlickDir.CENTER to '',
63+
FlickDir.LEFT to '',
64+
FlickDir.UP to '',
65+
FlickDir.RIGHT to '',
66+
FlickDir.DOWN to ''
67+
),
68+
KeyGroup.HA to mapOf(
69+
FlickDir.CENTER to '',
70+
FlickDir.LEFT to '',
71+
FlickDir.UP to '',
72+
FlickDir.RIGHT to '',
73+
FlickDir.DOWN to ''
74+
),
75+
KeyGroup.MA to mapOf(
76+
FlickDir.CENTER to '',
77+
FlickDir.LEFT to '',
78+
FlickDir.UP to '',
79+
FlickDir.RIGHT to '',
80+
FlickDir.DOWN to ''
81+
),
82+
KeyGroup.YA to mapOf(
83+
FlickDir.CENTER to '',
84+
FlickDir.LEFT to '',
85+
FlickDir.UP to '',
86+
FlickDir.RIGHT to '',
87+
FlickDir.DOWN to ''
88+
// LEFT/RIGHT を実運用の「ゃ/ゅ/ょ」等に合わせたいならここを差し替え
89+
),
90+
KeyGroup.RA to mapOf(
91+
FlickDir.CENTER to '',
92+
FlickDir.LEFT to '',
93+
FlickDir.UP to '',
94+
FlickDir.RIGHT to '',
95+
FlickDir.DOWN to ''
96+
),
97+
KeyGroup.WA to mapOf(
98+
FlickDir.CENTER to '',
99+
FlickDir.LEFT to '',
100+
FlickDir.UP to '',
101+
FlickDir.RIGHT to '',
102+
FlickDir.DOWN to ''
103+
// ここもあなたの配列に合わせて調整
104+
),
105+
)
106+
107+
// 逆引き: ひらがな -> (キーグループ,方向)
108+
private val reverse: Map<Char, KanaKey> = buildMap {
109+
for ((g, m) in table) for ((d, ch) in m) put(ch, KanaKey(g, d))
110+
}
111+
112+
fun keyOf(ch: Char): KanaKey? = reverse[ch]
113+
fun charOf(group: KeyGroup, dir: FlickDir): Char? = table[group]?.get(dir)
114+
fun posOf(group: KeyGroup): KeyPos? = pos[group]
115+
116+
fun manhattan(a: KeyGroup, b: KeyGroup): Int {
117+
val pa = posOf(a) ?: return Int.MAX_VALUE
118+
val pb = posOf(b) ?: return Int.MAX_VALUE
119+
return kotlin.math.abs(pa.x - pb.x) + kotlin.math.abs(pa.y - pb.y)
120+
}
121+
122+
fun allGroups(): List<KeyGroup> = pos.keys.toList()
123+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.kazumaproject.markdownhelperkeyboard.converter.typo_correction
2+
3+
enum class TypoCategory(val penalty: Int) {
4+
Exact(0),
5+
TapKeyInFlick(1), // 同一キー内で方向ミス(か⇄き⇄く⇄け⇄こ など)
6+
DistanceNear(1), // 近距離キー誤タップ(同方向)
7+
DistanceMiddle(2), // 中距離キー誤タップ(同方向)
8+
DistanceFar(7), // 遠距離キー誤タップ(同方向)
9+
}
10+
11+
data class TypoCandidate(
12+
val ch: Char,
13+
val category: TypoCategory,
14+
) {
15+
val penalty: Int get() = category.penalty
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.kazumaproject.markdownhelperkeyboard.converter.typo_correction
2+
3+
data class TypoCorrectionResult(val yomi: String, val penaltyUsed: Int)

app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,9 @@ class SuggestionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
513513
(32).toByte() -> ""
514514
/** Zenz **/
515515
(33).toByte() -> "[AI]"
516+
(34).toByte() -> "[履歴]"
516517
/** Typo Correction QWERTY **/
517-
(34).toByte() -> "[修正]"
518+
(35).toByte() -> "[修正]"
518519
else -> ""
519520
}
520521
holder.itemView.isPressed = position == highlightedPosition

0 commit comments

Comments
 (0)