feat(typing): wire PersonalDictionary into Chinese Pinyin via Rime sidecar
Redeploy an osg_personal import table on dictionary add/delete/sync so Chinese, English typing, and ASR share one curated lexicon (next keyboard open).
This commit is contained in:
@@ -175,4 +175,93 @@ final class LibrimeIntegrationTests: XCTestCase {
|
||||
)
|
||||
fuzzyBridge.finalizeRuntime()
|
||||
}
|
||||
|
||||
func testPersonalDictionarySidecarPinsSameCodeCandidates() throws {
|
||||
let fileManager = FileManager.default
|
||||
let root = fileManager.temporaryDirectory
|
||||
.appendingPathComponent("OSGRimePersonal-\(UUID().uuidString)", isDirectory: true)
|
||||
let shared = root.appendingPathComponent("SharedSupport", isDirectory: true)
|
||||
let user = root.appendingPathComponent("UserData", isDirectory: true)
|
||||
defer { try? fileManager.removeItem(at: root) }
|
||||
try fileManager.createDirectory(at: shared, withIntermediateDirectories: true)
|
||||
try fileManager.createDirectory(at: user, withIntermediateDirectories: true)
|
||||
|
||||
let bundle = Bundle(for: LibrimeIntegrationTests.self)
|
||||
let dictionaryURL = try XCTUnwrap(
|
||||
bundle.url(forResource: "osg_pinyin.dict", withExtension: "yaml")
|
||||
)
|
||||
let baseline = try String(contentsOf: dictionaryURL, encoding: .utf8)
|
||||
let patched = RimePersonalDictionaryExporter.injectingImportTables(into: baseline)
|
||||
try patched.write(
|
||||
to: shared.appendingPathComponent("osg_pinyin.dict.yaml"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
// Unique personal phrase on a common code — must outrank baseline “你好”.
|
||||
let personalYAML = RimePersonalDictionaryExporter.yaml(
|
||||
entries: [
|
||||
.init(text: "尼好专名", code: "ni hao"),
|
||||
.init(text: "ChatGPT", code: "chatgpt")
|
||||
]
|
||||
)
|
||||
try personalYAML.write(
|
||||
to: shared.appendingPathComponent("osg_personal.dict.yaml"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
|
||||
try RimeSchemaGenerator.defaultConfiguration().write(
|
||||
to: shared.appendingPathComponent("default.yaml"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
for schema in TypingInputSchema.allCases {
|
||||
try RimeSchemaGenerator.schema(for: schema, fuzzyPairs: []).write(
|
||||
to: shared.appendingPathComponent("\(schema.rawValue).schema.yaml"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
}
|
||||
|
||||
let deployer = OSGRimeBridge(
|
||||
sharedDataDirectory: shared.path,
|
||||
userDataDirectory: user.path,
|
||||
distributionVersion: "tests-personal"
|
||||
)
|
||||
try deployer.deploy(withFullCheck: true)
|
||||
deployer.finalizeRuntime()
|
||||
|
||||
let bridge = OSGRimeBridge(
|
||||
sharedDataDirectory: shared.path,
|
||||
userDataDirectory: user.path,
|
||||
distributionVersion: "tests-personal"
|
||||
)
|
||||
try bridge.start()
|
||||
XCTAssertTrue(bridge.selectSchema(TypingInputSchema.fullPinyin.rawValue))
|
||||
|
||||
for scalar in "nihao".utf8 {
|
||||
XCTAssertTrue(bridge.processKeyCode(Int32(scalar), modifiers: 0))
|
||||
}
|
||||
let chinese = bridge.snapshot(withCandidateLimit: 40)
|
||||
XCTAssertTrue(
|
||||
chinese.candidates.contains(where: { $0.text == "尼好专名" }),
|
||||
"personal Chinese missing: \(chinese.candidates.map(\.text).prefix(20))"
|
||||
)
|
||||
if let personal = chinese.candidates.firstIndex(where: { $0.text == "尼好专名" }),
|
||||
let baselineHello = chinese.candidates.firstIndex(where: { $0.text == "你好" }) {
|
||||
XCTAssertLessThan(personal, baselineHello, "personal same-code should pin above baseline")
|
||||
}
|
||||
|
||||
bridge.clearComposition()
|
||||
for scalar in "chatgpt".utf8 {
|
||||
XCTAssertTrue(bridge.processKeyCode(Int32(scalar), modifiers: 0))
|
||||
}
|
||||
let english = bridge.snapshot(withCandidateLimit: 40)
|
||||
XCTAssertTrue(
|
||||
english.candidates.contains(where: { $0.text == "ChatGPT" }),
|
||||
"personal English missing: \(english.candidates.map(\.text).prefix(20))"
|
||||
)
|
||||
bridge.finalizeRuntime()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// RimePersonalDictionaryExporterTests.swift
|
||||
// OSGKeyboard · Ext unit tests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class RimePersonalDictionaryExporterTests: XCTestCase {
|
||||
private let annotator = RimePinyinAnnotator(
|
||||
phraseCodes: [
|
||||
"你好": "ni hao",
|
||||
"阿坝": "a ba",
|
||||
"官网": "guan wang"
|
||||
],
|
||||
characterCodes: [
|
||||
"你": "ni",
|
||||
"好": "hao",
|
||||
"阿": "a",
|
||||
"坝": "ba",
|
||||
"官": "guan",
|
||||
"网": "wang",
|
||||
"欧": "ou",
|
||||
"斯": "si",
|
||||
"吉": "ji",
|
||||
"键": "jian"
|
||||
]
|
||||
)
|
||||
|
||||
func testAnnotatesPhraseExactMatch() {
|
||||
XCTAssertEqual(annotator.code(for: "阿坝"), "a ba")
|
||||
}
|
||||
|
||||
func testAnnotatesCharacterFallback() {
|
||||
XCTAssertEqual(annotator.code(for: "欧斯吉键"), "ou si ji jian")
|
||||
}
|
||||
|
||||
func testAnnotatesLatinProductName() {
|
||||
XCTAssertEqual(annotator.code(for: "ChatGPT"), "chatgpt")
|
||||
XCTAssertEqual(annotator.code(for: "GPT-4"), "gpt")
|
||||
}
|
||||
|
||||
func testAnnotatesMixedChineseAndLatin() {
|
||||
XCTAssertEqual(annotator.code(for: "ChatGPT官网"), "chatgpt guan wang")
|
||||
}
|
||||
|
||||
func testExporterPinsChineseEnglishAndLatinAlias() {
|
||||
var dictionary = PersonalDictionary.empty
|
||||
dictionary.entries = [
|
||||
PersonalDictionary.Entry(
|
||||
term: "阿坝",
|
||||
aliases: ["Aba"],
|
||||
category: .properNoun,
|
||||
source: .manual
|
||||
),
|
||||
PersonalDictionary.Entry(
|
||||
term: "ChatGPT",
|
||||
aliases: ["聊天GP"],
|
||||
category: .productName,
|
||||
source: .manual
|
||||
)
|
||||
]
|
||||
|
||||
let rows = RimePersonalDictionaryExporter.entries(from: dictionary, annotator: annotator)
|
||||
XCTAssertTrue(rows.contains { $0.text == "阿坝" && $0.code == "a ba" })
|
||||
XCTAssertTrue(rows.contains { $0.text == "阿坝" && $0.code == "aba" })
|
||||
XCTAssertTrue(rows.contains { $0.text == "ChatGPT" && $0.code == "chatgpt" })
|
||||
// Chinese ASR alias must not become a Rime code for the English term.
|
||||
XCTAssertFalse(rows.contains { $0.text == "ChatGPT" && $0.code.contains(" ") })
|
||||
XCTAssertTrue(rows.allSatisfy { $0.weight == RimePersonalDictionaryExporter.pinWeight })
|
||||
}
|
||||
|
||||
func testYamlContainsImportReadyHeaderAndRows() {
|
||||
let yaml = RimePersonalDictionaryExporter.yaml(
|
||||
entries: [
|
||||
.init(text: "阿坝", code: "a ba"),
|
||||
.init(text: "ChatGPT", code: "chatgpt")
|
||||
]
|
||||
)
|
||||
XCTAssertTrue(yaml.contains("name: osg_personal"))
|
||||
XCTAssertTrue(yaml.contains("阿坝\ta ba\t\(RimePersonalDictionaryExporter.pinWeight)"))
|
||||
XCTAssertTrue(yaml.contains("ChatGPT\tchatgpt\t\(RimePersonalDictionaryExporter.pinWeight)"))
|
||||
}
|
||||
|
||||
func testInjectImportTablesIsIdempotent() {
|
||||
let baseline = """
|
||||
---
|
||||
name: osg_pinyin
|
||||
version: "1.0"
|
||||
sort: by_weight
|
||||
use_preset_vocabulary: false
|
||||
columns:
|
||||
- text
|
||||
...
|
||||
啊\ta\t1
|
||||
"""
|
||||
let once = RimePersonalDictionaryExporter.injectingImportTables(into: baseline)
|
||||
XCTAssertTrue(once.contains("import_tables:"))
|
||||
XCTAssertTrue(once.contains("- osg_personal"))
|
||||
let twice = RimePersonalDictionaryExporter.injectingImportTables(into: once)
|
||||
XCTAssertEqual(once, twice)
|
||||
}
|
||||
|
||||
func testFingerprintChangesWhenEntriesChange() {
|
||||
let a = RimePersonalDictionaryExporter.yaml(entries: [.init(text: "阿坝", code: "a ba")])
|
||||
let b = RimePersonalDictionaryExporter.yaml(entries: [])
|
||||
XCTAssertNotEqual(
|
||||
RimePersonalDictionaryExporter.fingerprint(of: a),
|
||||
RimePersonalDictionaryExporter.fingerprint(of: b)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user