Merge branch 'develop' into feature/improve_convert_graph_impls

This commit is contained in:
Miwa / Ensan
2024-03-16 11:56:56 +09:00
17 changed files with 214 additions and 69 deletions

View File

@ -16,7 +16,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: swift-actions/setup-swift@v1
- uses: swift-actions/setup-swift@v2
with:
swift-version: 5.9
- uses: actions/checkout@v4

View File

@ -2,4 +2,7 @@
本ディレクトリはAzooKeyKanaKanjiConverterModuleに関するドキュメントをまとめています。
azooKey本体については[azooKey/docs](https://github.com/ensan-hcl/azooKey/tree/develop/docs/overview.md)をご覧ください。
azooKey本体については[azooKey/docs](https://github.com/ensan-hcl/azooKey/tree/develop/docs/overview.md)をご覧ください。
開発ガイドは[development_guide.md](./development_guide.md)をご覧ください。

25
Docs/cli.md Normal file
View File

@ -0,0 +1,25 @@
# anco (azooKey Cli)
`anco`コマンドにより、AzooKeyKanaKanjiConverterをCliで利用することができます。`anco`はデバッグ用ツールの位置付けです。
`anco`を利用するには、最初にinstallが必要です。
```
sh install_cli.sh
```
例えば以下のように利用できます。
```
your@pc Desktop % anco にほんごにゅうりょく --disable_prediction -n 10
日本語入力
にほんご入力
2本ご入力
2本後入力
2本語入力
日本語
2本
日本
にほんご
2本後
```

38
Docs/converter_api.md Normal file
View File

@ -0,0 +1,38 @@
# KanaKanjiConverter API
KanaKanjiConverterのインスタンスに対して利用できるいくつかのAPIを示します。
## `setKeyboardLanguage`
これから入力しようとしている言語を設定します。このAPIを呼ぶのは必須ではありません。
英語入力の場合、この関数を入力開始前に呼ぶことで事前に必要なデータをロードすることができるため、ユーザ体験が向上する可能性があります。
## `sendToDicdataStore`
辞書データに関する情報を追加します。
### `importDynamicUserDict`
動的ユーザ辞書を登録します。`DicdataElement`構造体の配列を直接渡します。
```Swift
converter.sendToDicdataStore(.importDynamicUserDict([
DicdataElement(word: "anco", ruby: "アンコ", cid: 1288, mid: 501, value: -5),
]))
```
`ruby`には読みを指定します。カタカナで指定してください。 `cid`はIPADIC品詞ID、`mid`は「501」としてください。`value``-5`から`-10`程度の範囲で設定してください。小さい値ほど変換されにくくなります。
### `forgetMemory`
特定の`Candidate`を渡すと、その`Candidate`に含まれている学習データを全てリセットします。
## `setCompletedData`
prefixとして確定された候補を与えてください。
## `updateLearningData`
確定された候補を与えると、学習を更新します。

23
Docs/development_guide.md Normal file
View File

@ -0,0 +1,23 @@
# 開発ガイド
開発にはSwift 5.9以上が必要です。
開発にはレポジトリをクローンしてください。サブモジュールを含むため、`--recursive`オプションが必要です。
```bash
git clone https://github.com/ensan-hcl/AzooKeyKanaKanjiConverter --recursive
```
## Cliツール
デバッグ用のCliツールとして`anco`コマンドがあります。`install_cli.sh`を実行してインストールしてください。場合によっては、`sudo`が必要です。
```bash
sh install_cli.sh
```
詳しくは[cli.md](./cli.md)をお読みください。
## コントリビュート
コントリビュートは歓迎です!

View File

@ -1,8 +1,10 @@
# Failures
azooKeyの開発上、明確に失敗だったと考えている実装や仕様をまとめます。これらは将来的に修正できるかもしれないし、できないかもしれないです。
AzooKeyKanaKanjiConverterの開発上、明確に失敗だったと考えている実装や仕様をまとめます。これらは将来的に修正できるかもしれないし、できないかもしれないです。
このドキュメントの目的はazooKeyの判断ミスを明確にして、今後azooKeyのフォークを作成する方や、新たな日本語入力ソフトウェアを作ろうとする方に向けて知見を残しておくことです。
このドキュメントの目的はAzooKeyKanaKanjiConverterの判断ミスを明確にして、今後AzooKeyKanaKanjiConverterのフォークを作成する方や、新たな日本語入力ソフトウェアを作ろうとする方に向けて知見を残しておくことです。
AzooKeyKanaKanjiConverter本体については[azooKeyをベースとするプロダクトの開発開始時に注意すべき点](https://github.com/ensan-hcl/azooKey/tree/develop/docs/advice_for_azooKey_based_development.md)もご覧ください。
## コストの設計

View File

@ -31,13 +31,14 @@ let package = Package(
.library(
name: "KanaKanjiConverterModule",
targets: ["KanaKanjiConverterModule"]
)
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-collections", from: "1.0.0")
.package(url: "https://github.com/apple/swift-collections", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.0.0")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@ -72,6 +73,13 @@ let package = Package(
],
swiftSettings: swiftSettings
),
.executableTarget(
name: "CliTool",
dependencies: [
"KanaKanjiConverterModuleWithDefaultDictionary",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.testTarget(
name: "SwiftUtilsTests",
dependencies: ["SwiftUtils"],
@ -88,9 +96,10 @@ let package = Package(
),
.testTarget(
name: "KanaKanjiConverterModuleWithDefaultDictionaryTests",
dependencies: ["KanaKanjiConverterModuleWithDefaultDictionary",
.product(name: "Collections", package: "swift-collections")
],
dependencies: [
"KanaKanjiConverterModuleWithDefaultDictionary",
.product(name: "Collections", package: "swift-collections")
],
swiftSettings: swiftSettings
)
]

View File

@ -3,7 +3,9 @@
AzooKeyKanaKanjiConverterは[azooKey](https://github.com/ensan-hcl/azooKey)のために開発したかな漢字変換エンジンです。数行のコードでかな漢字変換をiOS / macOS / visionOSのアプリケーションに組み込むことができます。
## 動作環境
iOS 14以降, macOS 11以降, visionOS 1以降, Ubuntu 22.04以降で動作を確認しています
iOS 14以降, macOS 11以降, visionOS 1以降, Ubuntu 22.04以降で動作を確認しています
AzooKeyKanaKanjiConverterの開発については[開発ガイド](Docs/development_guide.md)をご覧ください。
## KanaKanjiConverterModule
かな漢字変換を受け持つモジュールです。

View File

@ -0,0 +1,13 @@
import KanaKanjiConverterModuleWithDefaultDictionary
import ArgumentParser
@main
public struct Anco: ParsableCommand {
public static var configuration = CommandConfiguration(
abstract: "Anco is A(zooKey) Kana-Ka(n)ji (co)nverter",
subcommands: [Subcommands.Run.self],
defaultSubcommand: Subcommands.Run.self
)
public init() {}
}

View File

@ -0,0 +1,2 @@
/// namespace for subcommands
enum Subcommands {}

View File

@ -0,0 +1,50 @@
import KanaKanjiConverterModuleWithDefaultDictionary
import ArgumentParser
import Foundation
extension Subcommands {
struct Run: ParsableCommand {
@Argument(help: "ひらがなで表記された入力")
var input: String = ""
@Option(name: [.customLong("config_n_best")], help: "The parameter n (n best parameter) for internal viterbi search.")
var configNBest: Int = 10
@Option(name: [.customShort("n"), .customLong("top_n")], help: "Display top n candidates.")
var displayTopN: Int = 1
@Flag(name: [.customLong("disable_prediction")], help: "Disable producing prediction candidates.")
var disablePrediction = false
static var configuration = CommandConfiguration(commandName: "run", abstract: "Show help for this utility.")
@MainActor mutating func run() {
let converter = KanaKanjiConverter()
var composingText = ComposingText()
composingText.insertAtCursorPosition(input, inputStyle: .direct)
let result = converter.requestCandidates(composingText, options: requestOptions())
for candidate in result.mainResults.prefix(self.displayTopN) {
print(candidate.text)
}
}
func requestOptions() -> ConvertRequestOptions {
.withDefaultDictionary(
N_best: configNBest,
requireJapanesePrediction: !disablePrediction,
requireEnglishPrediction: false,
keyboardLanguage: .ja_JP,
typographyLetterCandidate: false,
unicodeCandidate: true,
englishCandidateInRoman2KanaInput: true,
fullWidthRomanCandidate: false,
halfWidthKanaCandidate: false,
learningType: .nothing,
maxMemoryCount: 0,
shouldResetMemory: false,
memoryDirectoryURL: URL(fileURLWithPath: ""),
sharedContainerURL: URL(fileURLWithPath: ""),
metadata: .init(appVersionString: "anco")
)
}
}
}

View File

@ -48,7 +48,7 @@ extension KanaKanjiConverter {
}
var string = string[...]
// drop
guard "ネン" == string.suffix(2) else {
guard string.hasSuffix("ネン") else {
return nil
}
string = string.dropLast(2)

View File

@ -30,7 +30,7 @@ public final class DicdataStore {
private var charsID: [Character: UInt8] = [:]
private var learningManager = LearningManager()
private var osUserDict: [DicdataElement] = []
private var dynamicUserDict: [DicdataElement] = []
///
/// - TODO: make this value as an option
@ -71,6 +71,10 @@ public final class DicdataStore {
}
public enum Notification {
/// use `importDynamicUserDict` for data that cannot be obtained statically.
/// - warning: Too many dynamic user dictionary will damage conversion performance, as dynamic user dictionary uses inefficent algorithms for looking up. If your entries can be listed up statically, then use normal user dictionaries.
case importDynamicUserDict([DicdataElement])
@available(*, deprecated, renamed: "importDynamicUserDict", message: "it will be removed in AzooKeyKanaKanjiConverter v1.0")
case importOSUserDict([DicdataElement])
case setRequestOptions(ConvertRequestOptions)
case forgetMemory(Candidate)
@ -81,8 +85,8 @@ public final class DicdataStore {
switch data {
case .closeKeyboard:
self.closeKeyboard()
case let .importOSUserDict(osUserDict):
self.osUserDict = osUserDict
case .importOSUserDict(let dicdata), .importDynamicUserDict(let dicdata):
self.dynamicUserDict = dicdata
case let .forgetMemory(candidate):
self.learningManager.forgetMemory(data: candidate.data)
// louds
@ -420,57 +424,29 @@ public final class DicdataStore {
if count == .zero {
return []
}
// 1
if count == 1 {
do {
let csvString = try String(contentsOf: requestOptions.dictionaryResourceURL.appendingPathComponent("p/p_\(key).csv", isDirectory: false), encoding: String.Encoding.utf8)
let csvLines = csvString.split(separator: "\n")
let csvData = csvLines.map {$0.split(separator: ",", omittingEmptySubsequences: false)}
let dicdata: [DicdataElement] = csvData
.map {self.parseLoudstxt2FormattedEntry(from: $0)}
.filter { Self.predictionUsable[$0.rcid] }
return dicdata
} catch {
debug("ファイルが存在しません: \(error)")
return []
}
} else if count == 2 {
var result: [DicdataElement] = []
let first = String(key.first!)
let charIDs = key.map(self.character2charId)
// 700
let prefixIndices = self.prefixMatchLOUDS(identifier: first, charIDs: charIDs, depth: 5).prefix(700)
result.append(
contentsOf: self.getDicdataFromLoudstxt3(identifier: first, indices: Set(prefixIndices))
.filter { Self.predictionUsable[$0.rcid] }
)
let userDictIndices = self.prefixMatchLOUDS(identifier: "user", charIDs: charIDs, depth: 5).prefix(700)
result.append(contentsOf: self.getDicdataFromLoudstxt3(identifier: "user", indices: Set(userDictIndices)))
if learningManager.enabled {
let memoryDictIndices = self.prefixMatchLOUDS(identifier: "memory", charIDs: charIDs, depth: 5).prefix(700)
result.append(contentsOf: self.getDicdataFromLoudstxt3(identifier: "memory", indices: Set(memoryDictIndices)))
result.append(contentsOf: self.learningManager.temporaryPrefixMatch(charIDs: charIDs))
}
return result
// 700
var result: [DicdataElement] = []
let first = String(key.first!)
let charIDs = key.map(self.character2charId)
// 1, 2depth
let depth = if count == 1 || count == 2 {
5
} else {
var result: [DicdataElement] = []
let first = String(key.first!)
let charIDs = key.map(self.character2charId)
// 700
let prefixIndices = self.prefixMatchLOUDS(identifier: first, charIDs: charIDs).prefix(700)
result.append(
contentsOf: self.getDicdataFromLoudstxt3(identifier: first, indices: Set(prefixIndices))
.filter { Self.predictionUsable[$0.rcid] }
)
let userDictIndices = self.prefixMatchLOUDS(identifier: "user", charIDs: charIDs).prefix(700)
result.append(contentsOf: self.getDicdataFromLoudstxt3(identifier: "user", indices: Set(userDictIndices)))
if learningManager.enabled {
let memoryDictIndices = self.prefixMatchLOUDS(identifier: "memory", charIDs: charIDs).prefix(700)
result.append(contentsOf: self.getDicdataFromLoudstxt3(identifier: "memory", indices: Set(memoryDictIndices)))
result.append(contentsOf: self.learningManager.temporaryPrefixMatch(charIDs: charIDs))
}
return result
Int.max
}
let prefixIndices = self.prefixMatchLOUDS(identifier: first, charIDs: charIDs, depth: depth).prefix(700)
result.append(
contentsOf: self.getDicdataFromLoudstxt3(identifier: first, indices: Set(consume prefixIndices))
.filter { Self.predictionUsable[$0.rcid] }
)
let userDictIndices = self.prefixMatchLOUDS(identifier: "user", charIDs: charIDs, depth: depth).prefix(700)
result.append(contentsOf: self.getDicdataFromLoudstxt3(identifier: "user", indices: Set(consume userDictIndices)))
if learningManager.enabled {
let memoryDictIndices = self.prefixMatchLOUDS(identifier: "memory", charIDs: charIDs).prefix(700)
result.append(contentsOf: self.getDicdataFromLoudstxt3(identifier: "memory", indices: Set(consume memoryDictIndices)))
result.append(contentsOf: self.learningManager.temporaryPrefixMatch(charIDs: charIDs))
}
return result
}
private func parseLoudstxt2FormattedEntry(from dataString: [some StringProtocol]) -> DicdataElement {
@ -634,12 +610,12 @@ public final class DicdataStore {
/// OSruby
func getMatchOSUserDict(_ ruby: some StringProtocol) -> [DicdataElement] {
self.osUserDict.filter {$0.ruby == ruby}
self.dynamicUserDict.filter {$0.ruby == ruby}
}
/// OSruby
func getPrefixMatchOSUserDict(_ ruby: some StringProtocol) -> [DicdataElement] {
self.osUserDict.filter {$0.ruby.hasPrefix(ruby)}
self.dynamicUserDict.filter {$0.ruby.hasPrefix(ruby)}
}
//

View File

@ -54,7 +54,7 @@ struct LOUDS: Sendable {
}
return flatChar2nodeIndices
}
self.flatChar2nodeIndicesIndex = consume flatChar2nodeIndicesIndex
self.flatChar2nodeIndicesIndex = flatChar2nodeIndicesIndex
var rankLarge: [UInt32] = .init(repeating: 0, count: bytes.count + 1)
rankLarge.withUnsafeMutableBufferPointer { buffer in
@ -62,7 +62,7 @@ struct LOUDS: Sendable {
buffer[i + 1] = buffer[i] &+ UInt32(Self.unit &- byte.nonzeroBitCount)
}
}
self.rankLarge = consume rankLarge
self.rankLarge = rankLarge
}
/// parentNodeIndex01Index

View File

@ -53,7 +53,7 @@ public struct TextReplacer: Sendable {
}
}
@available(*, deprecated, renamed: "init(emojiDataProvider:)", message: "init() is depreacted and will be removed in v1.0. Use init(emojiDataProvider:) instead")
@available(*, deprecated, renamed: "init(emojiDataProvider:)", message: "it be removed in AzooKeyKanaKanjiConverter v1.0")
public init() {
self.init {
if #available(iOS 16.4, *) {

View File

@ -55,7 +55,7 @@ extension InputGraphProtocol {
mutating func insert(_ node: Node, connection: InputGraphStructure.Connection = .none) -> Int {
var nodes = self.nodes
let index = self.structure.insert(node, nodes: &nodes, displayedTextRange: node.displayedTextRange, inputElementsRange: node.inputElementsRange, connection: connection)
self.nodes = consume nodes
self.nodes = nodes
return index
}
}

2
install_cli.sh Normal file
View File

@ -0,0 +1,2 @@
swift build -c release
cp -f .build/release/CliTool /usr/local/bin/anco