Skip to content

Commit

Permalink
Merge pull request #284 from ensan-hcl/feature/prediction_ui
Browse files Browse the repository at this point in the history
[Feature] AzooKeyKanaKanjiConverterの予測変換APIを使ってゼロクエリ予測変換を実現
  • Loading branch information
ensan-hcl authored Sep 23, 2023
2 parents b463b0b + af61ce5 commit 035792e
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 102 deletions.
2 changes: 1 addition & 1 deletion AzooKeyCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/ensan-hcl/AzooKeyKanaKanjiConverter", branch: "develop")
.package(url: "https://github.com/ensan-hcl/AzooKeyKanaKanjiConverter", branch: "feature/prediction_candidate")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand Down
14 changes: 10 additions & 4 deletions AzooKeyCore/Sources/KeyboardViews/View/KeyboardBar/ResultBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ struct ResultBar<Extension: ApplicationSpecificKeyboardViewExtension>: View {
.matchedGeometryEffect(id: "KeyboardBarButton", in: namespace)
}

private var isResultMode: Bool {
!variableStates.resultModelVariableSection.results.isEmpty
}

private var resultData: [ResultData] {
if !variableStates.resultModelVariableSection.results.isEmpty {
if isResultMode {
variableStates.resultModelVariableSection.results
} else {
variableStates.resultModelVariableSection.predictionResults
Expand Down Expand Up @@ -98,7 +102,7 @@ struct ResultBar<Extension: ApplicationSpecificKeyboardViewExtension>: View {
}
} else {
HStack {
if variableStates.resultModelVariableSection.results.isEmpty && displayTabBarButton {
if !isResultMode && displayTabBarButton {
tabBarButton
Spacer()
}
Expand Down Expand Up @@ -128,7 +132,7 @@ struct ResultBar<Extension: ApplicationSpecificKeyboardViewExtension>: View {
}
.padding(.horizontal, 5)
}
if variableStates.resultModelVariableSection.predictionResults.isEmpty {
if isResultMode {
// 候補を展開するボタン
Button(action: {self.expand()}) {
ZStack {
Expand All @@ -145,7 +149,8 @@ struct ResultBar<Extension: ApplicationSpecificKeyboardViewExtension>: View {
}
}
}
.animation(.easeIn(duration: 0.2), value: resultData.isEmpty)
.animation(.easeIn(duration: 0.2), value: variableStates.resultModelVariableSection.results.isEmpty)
.animation(.easeIn(duration: 0.2), value: variableStates.resultModelVariableSection.predictionResults.isEmpty)
}

private func pressed(candidate: any ResultViewItemData) {
Expand Down Expand Up @@ -231,5 +236,6 @@ struct ResultButtonStyle<Extension: ApplicationSpecificKeyboardViewExtension>: B
theme.resultBackgroundColor.color
)
.cornerRadius(5.0)
.animation(nil, value: configuration.isPressed)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ public struct ResultModelVariableSection {
self.updateResult.toggle()
}
public mutating func setSearchResults(_ results: [any ResultViewItemData]) {
self.searchResults = results.indices.map {ResultData(id: $0, candidate: results[$0])}
self.searchResults = results.enumerated().map {ResultData(id: $0.offset, candidate: $0.element)}
}
public mutating func setPredictionResults(_ results: [any ResultViewItemData]) {
self.predictionResults = results.indices.map {ResultData(id: $0, candidate: results[$0])}
self.predictionResults = results.enumerated().map {ResultData(id: $0.offset, candidate: $0.element)}
self.updateResult.toggle()
}
}
Expand Down
2 changes: 1 addition & 1 deletion Keyboard/Display/DisplayedTextManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import UIKit

/// `textChangedCount`のgetter。
func getTextChangedCount() -> Int {
return self.textChangedCount
self.textChangedCount
}

/// marked textの有効化状態
Expand Down
119 changes: 68 additions & 51 deletions Keyboard/Display/InputManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,48 @@ import UIKit
self.composingText
}

private func getConvertRequestOptions(inputStylePreference: InputStyle? = nil) -> ConvertRequestOptions {
let requireJapanesePrediction: Bool
let requireEnglishPrediction: Bool
switch (isSelected, inputStylePreference ?? .direct) {
case (true, _):
requireJapanesePrediction = false
requireEnglishPrediction = false
case (false, .direct):
requireJapanesePrediction = true
requireEnglishPrediction = true
case (false, .roman2kana):
requireJapanesePrediction = keyboardLanguage == .ja_JP
requireEnglishPrediction = keyboardLanguage == .en_US
}
@KeyboardSetting(.typographyLetter) var typographyLetterCandidate
@KeyboardSetting(.unicodeCandidate) var unicodeCandidate
@KeyboardSetting(.englishCandidate) var englishCandidateInRoman2KanaInput
@KeyboardSetting(.fullRomanCandidate) var fullWidthRomanCandidate
@KeyboardSetting(.halfKanaCandidate) var halfWidthKanaCandidate
@KeyboardSetting(.learningType) var learningType

return ConvertRequestOptions(
N_best: 10,
requireJapanesePrediction: requireJapanesePrediction,
requireEnglishPrediction: requireEnglishPrediction,
keyboardLanguage: keyboardLanguage,
// KeyboardSettingsを注入
typographyLetterCandidate: typographyLetterCandidate,
unicodeCandidate: unicodeCandidate,
englishCandidateInRoman2KanaInput: englishCandidateInRoman2KanaInput,
fullWidthRomanCandidate: fullWidthRomanCandidate,
halfWidthKanaCandidate: halfWidthKanaCandidate,
learningType: learningType,
maxMemoryCount: 65536,
shouldResetMemory: MemoryResetCondition.shouldReset(),
dictionaryResourceURL: Self.dictionaryResourceURL,
memoryDirectoryURL: Self.memoryDirectoryURL,
sharedContainerURL: Self.sharedContainerURL,
metadata: .init(appVersionString: SharedStore.currentAppVersion?.description ?? "Unknown")
)
}

private func updateLog(candidate: Candidate) {
for data in candidate.data {
// 「感謝する: カンシャスル」→を「感謝: カンシャ」に置き換える
Expand Down Expand Up @@ -179,37 +221,52 @@ import UIKit
let results = self.textReplacer.getSearchResult(query: query, target: target)
return results
}

func updatePredictionCandidates(candidate: Candidate) {

/// 確定直後に呼ぶ
func updatePostCompositionPredictionCandidates(candidate: Candidate) {
let results = self.kanaKanjiConverter.requestPostCompositionPredictionCandidates(leftSideCandidate: candidate, options: getConvertRequestOptions())
predictionManager.updateAfterComplete(candidate: candidate, textChangedCount: self.displayedTextManager.getTextChangedCount())
if let updateResult {
updateResult {
$0.setPredictionResults(predictionManager.update(candidate: candidate, textChangedCount: self.displayedTextManager.getTextChangedCount()))
$0.setPredictionResults(results)
}
}
}

func updatePredictionCandidates(appending candidate: PredictionCandidate) {
// TODO: provide implementation
/// 予測変換を選んだ後に呼ぶ
func postCompositionPredictionCandidateSelected(candidate: PostCompositionPredictionCandidate) {
guard let lastUsedCandidate = predictionManager.getLastCandidate() else {
return
}
self.kanaKanjiConverter.updateLearningData(lastUsedCandidate, with: candidate)
let newCandidate = candidate.join(to: lastUsedCandidate)
let results = self.kanaKanjiConverter.requestPostCompositionPredictionCandidates(leftSideCandidate: newCandidate, options: getConvertRequestOptions())
predictionManager.update(candidate: newCandidate, textChangedCount: self.displayedTextManager.getTextChangedCount())
if let updateResult {
updateResult {
$0.setPredictionResults(results)
}
}
}

func resetPredictionCandidates() {
func resetPostCompositionPredictionCandidates() {
if let updateResult {
updateResult {
$0.setPredictionResults([])
}
}
}

func resetPredictionCandidatesIfNecessary(textChangedCount: Int) {
func resetPostCompositionPredictionCandidatesIfNecessary(textChangedCount: Int) {
if predictionManager.shouldResetPrediction(textChangedCount: textChangedCount) {
self.resetPredictionCandidates()
self.resetPostCompositionPredictionCandidates()
}
}

/// `composingText`に入力されていた全体が変換された後に呼ばれる関数
private func conversionCompleted(candidate: Candidate) {
// 予測変換を更新する
self.updatePredictionCandidates(candidate: candidate)
self.updatePostCompositionPredictionCandidates(candidate: candidate)
}

/// 変換を選択した場合に呼ばれる
Expand Down Expand Up @@ -750,7 +807,7 @@ import UIKit

// トークナイザ
let tokenizer: CFStringTokenizer = CFStringTokenizerCreate(
kCFAllocatorDefault,
kCFAllocatorDefault,
inputText as CFString,
CFRangeMake(0, inputText.length),
kCFStringTokenizerUnitWordBoundary,
Expand Down Expand Up @@ -819,48 +876,8 @@ import UIKit
func setResult() {
let inputData = composingText.prefixToCursorPosition()
debug("InputManager.setResult: value to be input", inputData)

let requireJapanesePrediction: Bool
let requireEnglishPrediction: Bool
switch (isSelected, inputData.input.last?.inputStyle ?? .direct) {
case (true, _):
requireJapanesePrediction = false
requireEnglishPrediction = false
case (false, .direct):
requireJapanesePrediction = true
requireEnglishPrediction = true
case (false, .roman2kana):
requireJapanesePrediction = keyboardLanguage == .ja_JP
requireEnglishPrediction = keyboardLanguage == .en_US
}
@KeyboardSetting(.typographyLetter) var typographyLetterCandidate
@KeyboardSetting(.unicodeCandidate) var unicodeCandidate
@KeyboardSetting(.englishCandidate) var englishCandidateInRoman2KanaInput
@KeyboardSetting(.fullRomanCandidate) var fullWidthRomanCandidate
@KeyboardSetting(.halfKanaCandidate) var halfWidthKanaCandidate
@KeyboardSetting(.learningType) var learningType

let options = ConvertRequestOptions(
N_best: 10,
requireJapanesePrediction: requireJapanesePrediction,
requireEnglishPrediction: requireEnglishPrediction,
keyboardLanguage: keyboardLanguage,
// KeyboardSettingsを注入
typographyLetterCandidate: typographyLetterCandidate,
unicodeCandidate: unicodeCandidate,
englishCandidateInRoman2KanaInput: englishCandidateInRoman2KanaInput,
fullWidthRomanCandidate: fullWidthRomanCandidate,
halfWidthKanaCandidate: halfWidthKanaCandidate,
learningType: learningType,
maxMemoryCount: 65536,
shouldResetMemory: MemoryResetCondition.shouldReset(),
dictionaryResourceURL: Self.dictionaryResourceURL,
memoryDirectoryURL: Self.memoryDirectoryURL,
sharedContainerURL: Self.sharedContainerURL,
metadata: .init(appVersionString: SharedStore.currentAppVersion?.description ?? "Unknown")
)
let options = self.getConvertRequestOptions(inputStylePreference: inputData.input.last?.inputStyle)
debug("InputManager.setResult: options", options)

let results = self.kanaKanjiConverter.requestCandidates(inputData, options: options)

// 表示を更新する
Expand Down
16 changes: 8 additions & 8 deletions Keyboard/Display/KeyboardActionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ import SwiftUtils
}
}
variableStates.lastTabCharacterPreferenceUpdate = .now
} else if let candidate = candidate as? PredictionCandidate {
} else if let candidate = candidate as? PostCompositionPredictionCandidate {
self.inputManager.input(text: candidate.text, simpleInsert: true, inputStyle: .direct)
if !candidate.terminatePrediction {
self.inputManager.updatePredictionCandidates(appending: candidate)
if !candidate.isTerminal {
self.inputManager.postCompositionPredictionCandidateSelected(candidate: candidate)
} else {
self.inputManager.resetPredictionCandidates()
self.inputManager.resetPostCompositionPredictionCandidates()
}
} else {
debug("notifyComplete: 確定できません")
Expand Down Expand Up @@ -285,7 +285,7 @@ import SwiftUtils
// 文字列の変更を適用
variableStates.textChangedCount = self.inputManager.getTextChangedCount()
// 必要に応じて予測変換をリセット
self.inputManager.resetPredictionCandidatesIfNecessary(textChangedCount: variableStates.textChangedCount)
self.inputManager.resetPostCompositionPredictionCandidatesIfNecessary(textChangedCount: variableStates.textChangedCount)

if let undoAction {
variableStates.undoAction = .init(action: undoAction, textChangedCount: variableStates.textChangedCount)
Expand Down Expand Up @@ -347,7 +347,7 @@ import SwiftUtils
self.registerActions(action.start, variableStates: variableStates)
}
self.tasks.append((type: action, task: startTask))

let repeatTask = Task {
try await Task.sleep(nanoseconds: 0_400_000_000)
while !Task.isCancelled {
Expand Down Expand Up @@ -557,7 +557,7 @@ import SwiftUtils
self.inputManager.userMovedCursor(count: offset)
// カーソルが動いているのでtextChangedCountを増やす
variableStates.textChangedCount += 1
self.inputManager.resetPredictionCandidatesIfNecessary(textChangedCount: variableStates.textChangedCount)
self.inputManager.resetPostCompositionPredictionCandidatesIfNecessary(textChangedCount: variableStates.textChangedCount)
return
}

Expand All @@ -579,7 +579,7 @@ import SwiftUtils
defer {
variableStates.textChangedCount += 1
// 予測変換はテキストが変化したら解除する
self.inputManager.resetPredictionCandidatesIfNecessary(textChangedCount: variableStates.textChangedCount)
self.inputManager.resetPostCompositionPredictionCandidatesIfNecessary(textChangedCount: variableStates.textChangedCount)
}

if b_left == "\n" && b_center == a_wholeText {
Expand Down
55 changes: 41 additions & 14 deletions Keyboard/Display/PredictionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,43 @@ final class PredictionManager {

private var lastState: State?

func update(candidate: Candidate, textChangedCount: Int) -> [PredictionCandidate] {
// TODO: `KanaKanjiConverter.mergeCandidates`を呼んだほうが適切
private func mergeCandidates(_ left: Candidate, _ right: Candidate) -> Candidate {
// 厳密なmergeにはleft.lastRcidとright.lastLcidの連接コストの計算が必要だが、予測変換の文脈で厳密なValueの計算は不要なので行わない
var result = left
result.text += right.text
result.data += right.data
result.value += right.value
result.correspondingCount += right.correspondingCount
result.lastMid = right.lastMid == MIDData.EOS.mid ? left.lastMid : right.lastMid
return result
}

/// 部分的に確定した後に更新を行う
func partialUpdate(candidate: Candidate) {
if let lastState {
self.lastState = .init(candidate: self.mergeCandidates(lastState.candidate, candidate), textChangedCount: lastState.textChangedCount)
} else {
self.lastState = .init(candidate: candidate, textChangedCount: -1)
}
}

/// 確定直後にcandidateと合わせて更新する
func updateAfterComplete(candidate: Candidate, textChangedCount: Int) {
if let lastState, lastState.textChangedCount == -1 {
self.lastState = State(candidate: self.mergeCandidates(lastState.candidate, candidate), textChangedCount: textChangedCount)
} else {
self.lastState = State(candidate: candidate, textChangedCount: textChangedCount)
}
}

/// 確定後にcandidateと合わせて更新する
func update(candidate: Candidate, textChangedCount: Int) {
self.lastState = State(candidate: candidate, textChangedCount: textChangedCount)
return [
// TODO: Provide implementation
]
}

func getLastCandidate() -> Candidate? {
lastState?.candidate
}

func shouldResetPrediction(textChangedCount: Int) -> Bool {
Expand All @@ -33,18 +65,13 @@ final class PredictionManager {
}
}

struct PredictionCandidate: ResultViewItemData {
var inputable: Bool {
extension PostCompositionPredictionCandidate: ResultViewItemData {
public var inputable: Bool {
true
}

var text: String

var terminatePrediction: Bool = false

#if DEBUG
func getDebugInformation() -> String {
#if DEBUG
public func getDebugInformation() -> String {
text
}
#endif
#endif
}
4 changes: 0 additions & 4 deletions docs/advice_for_azooKey_based_development.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ azooKeyは英語のなぞり入力(グライド入力)をサポートして

高精度なグライド入力の実現は今のところコストが高いため行えていません。将来的に対応したいですが、現状技術的に困難です。

### 再変換

azooKeyは一般の場合の再変換をサポートしていません。つまり「再変換」から「さいへんかん」という文字列を得るためのモジュール(漢字かな変換モジュール)は実装されていません。

### シフト

azooKeyのデフォルトのUIは英字のシフトに対応していませんが、`ApplicationSpecificKeyboardViewSettingProvider`を実装する際に`useShiftKey``true`にすることでデフォルトのローマ字キーボードでシフトキーを実装することができます。
Expand Down
Loading

0 comments on commit 035792e

Please sign in to comment.