Skip to content

Commit 22edba7

Browse files
committed
Continue patching UOM APIs and Candidate APIs.
1 parent a065aa6 commit 22edba7

5 files changed

Lines changed: 172 additions & 58 deletions

File tree

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ let package = Package(
2424
),
2525
.target(
2626
name: "MegrezTestComponents",
27-
dependencies: []
27+
dependencies: [
28+
"Megrez",
29+
]
2830
),
2931
.testTarget(
3032
name: "MegrezTests",

Sources/Megrez/3_KeyValuePaired.swift

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,14 @@ extension Megrez.Compositor {
169169
/// - candidate: 指定用來覆寫為的候選字(詞音鍵值配對)。
170170
/// - location: 游標位置。
171171
/// - overrideType: 指定覆寫行為。
172+
/// - enforceRetokenization: 是否強制重新分詞,對所有重疊節點施作重置與降權,以避免殘留舊節點狀態。
172173
/// - perceptionHandler: 覆寫成功後用於回傳觀測智慧的回呼。
173174
/// - Returns: 該操作是否成功執行。
174175
@discardableResult
175176
public func overrideCandidate(
176177
_ candidate: Megrez.KeyValuePaired, at location: Int,
177178
overrideType: Megrez.Node.OverrideType = .withHighScore,
179+
enforceRetokenization: Bool = false,
178180
perceptionHandler: ((Megrez.PerceptionIntel) -> ())? = nil
179181
)
180182
-> Bool {
@@ -183,6 +185,7 @@ extension Megrez.Compositor {
183185
at: location,
184186
value: candidate.value,
185187
type: overrideType,
188+
enforceRetokenization: enforceRetokenization,
186189
perceptionHandler: perceptionHandler
187190
)
188191
}
@@ -194,12 +197,14 @@ extension Megrez.Compositor {
194197
/// - candidate: 指定用來覆寫為的候選字(字串)。
195198
/// - location: 游標位置。
196199
/// - overrideType: 指定覆寫行為。
200+
/// - enforceRetokenization: 是否強制重新分詞,對所有重疊節點施作重置與降權,以避免殘留舊節點狀態。
197201
/// - perceptionHandler: 覆寫成功後用於回傳觀測智慧的回呼。
198202
/// - Returns: 該操作是否成功執行。
199203
@discardableResult
200204
public func overrideCandidateLiteral(
201205
_ candidate: String,
202206
at location: Int, overrideType: Megrez.Node.OverrideType = .withHighScore,
207+
enforceRetokenization: Bool = false,
203208
perceptionHandler: ((Megrez.PerceptionIntel) -> ())? = nil
204209
)
205210
-> Bool {
@@ -208,6 +213,7 @@ extension Megrez.Compositor {
208213
at: location,
209214
value: candidate,
210215
type: overrideType,
216+
enforceRetokenization: enforceRetokenization,
211217
perceptionHandler: perceptionHandler
212218
)
213219
}
@@ -220,13 +226,15 @@ extension Megrez.Compositor {
220226
/// - location: 游標位置。
221227
/// - value: 資料值。
222228
/// - type: 指定覆寫行為。
229+
/// - enforceRetokenization: 是否強制重新分詞,對所有重疊節點施作重置與降權,以避免殘留舊節點狀態。
223230
/// - perceptionHandler: 覆寫成功後用於回傳觀測智慧的回呼。
224231
/// - Returns: 該操作是否成功執行。
225232
internal func overrideCandidateAgainst(
226233
keyArray: [String]?,
227234
at location: Int,
228235
value: String,
229236
type: Megrez.Node.OverrideType,
237+
enforceRetokenization: Bool,
230238
perceptionHandler: ((Megrez.PerceptionIntel) -> ())? = nil
231239
)
232240
-> Bool {
@@ -296,20 +304,39 @@ extension Megrez.Compositor {
296304
overridden.location + overridden.node.segLength
297305
)
298306

299-
for i in overriddenRange {
300-
let overlappingNodes = fetchOverlappingNodes(at: i)
301-
302-
for anchor in overlappingNodes where anchor.node != overridden.node {
303-
// 檢查是否需要重設節點
304-
let shouldReset = shouldResetNode(
305-
anchor: anchor.node,
306-
overriddenNode: overridden.node
307-
)
308-
309-
if shouldReset {
310-
anchor.node.reset()
311-
} else {
312-
anchor.node.overridingScore /= 4
307+
if enforceRetokenization {
308+
let overriddenNodeRef = overridden.node
309+
let demotionScore = -Swift.max(1.0, Swift.abs(overriddenNodeRef.overridingScore))
310+
for i in overriddenRange {
311+
let overlappingNodes = fetchOverlappingNodes(at: i)
312+
for anchor in overlappingNodes where anchor.node !== overriddenNodeRef
313+
&& anchor.location <= overridden.location {
314+
if shouldResetNode(anchor: anchor.node, overriddenNode: overriddenNodeRef) {
315+
anchor.node.reset()
316+
}
317+
anchor.node.overrideStatus = .init(
318+
overridingScore: demotionScore,
319+
currentOverrideType: .withHighScore,
320+
currentUnigramIndex: anchor.node.currentUnigramIndex
321+
)
322+
}
323+
}
324+
} else {
325+
for i in overriddenRange {
326+
let overlappingNodes = fetchOverlappingNodes(at: i)
327+
328+
for anchor in overlappingNodes where anchor.node != overridden.node {
329+
// 檢查是否需要重設節點
330+
let shouldReset = shouldResetNode(
331+
anchor: anchor.node,
332+
overriddenNode: overridden.node
333+
)
334+
335+
if shouldReset {
336+
anchor.node.reset()
337+
} else {
338+
anchor.node.overridingScore /= 4
339+
}
313340
}
314341
}
315342
}
@@ -403,7 +430,12 @@ extension Megrez {
403430

404431
// 選擇用來形成觀測 key 的資料源:長→短使用 after,其他使用 before
405432
let keySource = isBreakingUp ? currentAssembled : previouslyAssembled
406-
let keyCursor = afterHit.range.upperBound // 與現有 key 生成方法對齊
433+
let keyCursorRaw = Swift.max(
434+
afterHit.range.lowerBound,
435+
Swift.min(cursor, afterHit.range.upperBound - 1)
436+
)
437+
guard keySource.totalKeyCount > 0 else { return nil }
438+
let keyCursor = Swift.max(0, Swift.min(keyCursorRaw, keySource.totalKeyCount - 1))
407439

408440
guard let keyGen = keySource.generateKeyForPerception(cursor: keyCursor) else { return nil }
409441

Sources/Megrez/7_Unigram.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ extension Megrez {
4444

4545
/// 將當前單元圖列印成一個字串。
4646
public var description: String {
47-
"(" + value.description + "," + String(score) + ")"
47+
"(\(keyArray.joined(separator: "-")),\(value),\(score))"
4848
}
4949

5050
/// 單元圖的淺層複製品(保持相同的索引鍵陣列)。

Sources/Megrez/8_GramInPath.swift

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ extension Array where Element == Megrez.GramInPath {
151151
///
152152
/// - Remark: 除非有專門指定游標,否則身為 `[GramInPath]` 自身的
153153
/// 「陣列最尾端」(也就是打字方向上最前方)的那個 Gram 會被當成 Head。
154+
/// 若游標落在多字詞節點內,Head 會選擇游標右方(文字輸入方向上的下一個)讀音。
154155
/// - Parameters:
155156
/// - cursor: 指定用於當作 head 的游標位置;若為 nil 則取陣列尾端。
156157
/// - maxContext: 最多向前取用的上下文節點數(含 head),預設 3。
@@ -159,41 +160,83 @@ extension Array where Element == Megrez.GramInPath {
159160
maxContext: Int = 3
160161
)
161162
-> (ngramKey: String, candidate: String, headReading: String)? {
162-
let perceptedGIP: Megrez.GramInPath?
163-
if let cursor, (0 ..< self.totalKeyCount).contains(cursor) {
164-
perceptedGIP = findGram(at: cursor)?.gram
163+
guard maxContext > 0 else { return nil }
164+
165+
let headInfo: (pair: Megrez.GramInPath, range: Range<Int>)?
166+
let resolvedCursor: Int
167+
if let cursor, (0 ..< totalKeyCount).contains(cursor), let hit = findGram(at: cursor) {
168+
headInfo = (pair: hit.gram, range: hit.range)
169+
resolvedCursor = Swift.max(
170+
hit.range.lowerBound,
171+
Swift.min(cursor, hit.range.upperBound - 1)
172+
)
173+
} else if let tail = last {
174+
let lowerBound = totalKeyCount - tail.keyArray.count
175+
headInfo = (pair: tail, range: lowerBound ..< totalKeyCount)
176+
resolvedCursor = Swift.max(lowerBound, totalKeyCount - 1)
165177
} else {
166-
perceptedGIP = last
178+
headInfo = nil
179+
resolvedCursor = 0
167180
}
168-
guard let perceptedGIP else { return nil }
169-
var arrGIPs = self
170-
while arrGIPs.last?.gram !== perceptedGIP.gram { arrGIPs.removeLast() }
171-
var isHead = true
172-
var outputCells = [String]()
173-
loopProc: while !arrGIPs.isEmpty, let frontendPair = arrGIPs.last {
174-
defer { arrGIPs = arrGIPs.dropLast() }
175-
176-
func makeNGramKeyCell(isHead: Bool) -> String? {
177-
// 字音數與字數不一致的內容會被拋棄。
178-
guard !frontendPair.isReadingMismatched else { return nil }
179-
guard !frontendPair.value.isEmpty else { return nil }
180-
guard !frontendPair.keyArray.joined().isEmpty else { return nil }
181-
let keyChain = frontendPair.keyArray.joined(separator: "-")
182-
guard !keyChain.contains("_") else { return nil }
183-
// 前置單元只記錄讀音,在其後的單元則同時記錄讀音與字詞
184-
return isHead ? keyChain : "(\(keyChain):\(frontendPair.value))"
181+
182+
guard let headInfo else { return nil }
183+
let headPair = headInfo.pair
184+
let headRange = headInfo.range
185+
let headKeyOffset = Swift.max(0, resolvedCursor - headRange.lowerBound)
186+
guard headPair.keyArray.indices.contains(headKeyOffset) else { return nil }
187+
188+
let placeholder = "()"
189+
let readingSeparator = Megrez.Compositor.theSeparator
190+
191+
func isPunctuation(_ pair: Megrez.GramInPath) -> Bool {
192+
guard let firstReading = pair.keyArray.first else { return false }
193+
return firstReading.first == "_"
194+
}
195+
196+
func combinedString(for pair: Megrez.GramInPath) -> String? {
197+
guard !pair.value.isEmpty else { return nil }
198+
guard !pair.keyArray.isEmpty else { return nil }
199+
let reading = pair.joinedCurrentKey(by: readingSeparator)
200+
guard !reading.isEmpty else { return nil }
201+
return "(\(reading),\(pair.value))"
202+
}
203+
204+
guard let headIndex = lastIndex(where: { $0.gram === headPair.gram })
205+
?? firstIndex(of: headPair)
206+
else { return nil }
207+
208+
var resultCells = [String](repeating: placeholder, count: maxContext)
209+
resultCells[maxContext - 1] = combinedString(for: headPair) ?? placeholder
210+
211+
var contextSlot = maxContext - 2
212+
var currentIndex = headIndex - 1
213+
var encounteredPunctuation = false
214+
215+
while contextSlot >= 0 {
216+
guard currentIndex >= 0 else {
217+
resultCells[contextSlot] = placeholder
218+
contextSlot -= 1
219+
continue
220+
}
221+
let currentPair = self[currentIndex]
222+
currentIndex -= 1
223+
224+
if encounteredPunctuation || isPunctuation(currentPair) {
225+
encounteredPunctuation = true
226+
resultCells[contextSlot] = placeholder
227+
contextSlot -= 1
228+
continue
185229
}
186230

187-
guard let keyCellStr = makeNGramKeyCell(isHead: isHead) else { break loopProc }
188-
outputCells.insert(keyCellStr, at: 0)
189-
if outputCells.count >= maxContext { break loopProc }
190-
if isHead { isHead = false }
231+
resultCells[contextSlot] = combinedString(for: currentPair) ?? placeholder
232+
contextSlot -= 1
191233
}
192-
guard !outputCells.isEmpty else { return nil }
234+
235+
let headReading = headPair.keyArray[headKeyOffset]
193236
return (
194-
"(\(outputCells.joined(separator: ",")))",
195-
perceptedGIP.gram.value,
196-
perceptedGIP.joinedCurrentKey(by: "-")
237+
resultCells.joined(separator: "&"),
238+
headPair.value,
239+
headReading
197240
)
198241
}
199242
}

Tests/MegrezTests/MegrezTests.swift

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -652,39 +652,54 @@ final class MegrezTestsAdvanced: XCTestCase {
652652
func test18_Composer_UOMMarginalCaseTest() throws {
653653
let lm = SimpleLM(input: MegrezTestComponents.strLMSampleData_SaisoukiNoGaika)
654654
let compositor = Megrez.Compositor(with: lm)
655-
["zai4", "chuang4", "shi4", "de5", "kai3", "ge1"].forEach {
655+
let readingKeys = ["zai4", "chuang4", "shi4", "de5", "kai3", "ge1"]
656+
readingKeys.forEach {
656657
_ = compositor.insertKey($0)
657658
}
658659
compositor.assemble()
659660
let assembledBefore = compositor.assembledSentence.map(\.value).joined(separator: " ")
660661
XCTAssertTrue("再 創 是的 凱歌" == assembledBefore)
662+
// 測試此時生成的 keyForQueryingData 是否正確
663+
let cursorShi = 2
664+
let cursorShiDe = 3
665+
let keyForQueryingDataAt2 = compositor.assembledSentence
666+
.generateKeyForPerception(cursor: cursorShi)
667+
XCTAssertEqual(keyForQueryingDataAt2?.ngramKey, "(zai4,再)&(chuang4,創)&(shi4-de5,是的)")
668+
XCTAssertEqual(keyForQueryingDataAt2?.headReading, "shi4")
669+
let keyForQueryingDataAt3 = compositor.assembledSentence
670+
.generateKeyForPerception(cursor: cursorShiDe)
671+
XCTAssertEqual(keyForQueryingDataAt3?.ngramKey, "(zai4,再)&(chuang4,創)&(shi4-de5,是的)")
672+
XCTAssertEqual(keyForQueryingDataAt3?.headReading, "de5")
661673
// 應能提供『是的』『似的』『凱歌』等候選
662674
let pairsAtShiDeEnd = compositor.fetchCandidates(at: 4, filter: .endAt)
663675
XCTAssertTrue(pairsAtShiDeEnd.map(\.value).contains("是的"))
664676
XCTAssertTrue(pairsAtShiDeEnd.map(\.value).contains("似的"))
665677
// 模擬使用者把『是』改為『世』,再合成:觀測應為 shortToLong
666678
var obsCaptured: Megrez.PerceptionIntel?
667-
let cursorShi = 2
668679
_ = compositor.overrideCandidate(
669680
.init(keyArray: ["shi4"], value: ""),
670-
at: cursorShi
671-
) {
672-
obsCaptured = $0
673-
}
681+
at: cursorShi,
682+
overrideType: .withHighScore,
683+
enforceRetokenization: true,
684+
perceptionHandler: { obsCaptured = $0 }
685+
)
686+
XCTAssertEqual(obsCaptured?.ngramKey, "(zai4,再)&(chuang4,創)&(shi4,世)")
674687
// compositor.assemble() <- 已經組句了。
675688
let assembledAfter = compositor.assembledSentence.map(\.value).joined(separator: " ")
676689
XCTAssertTrue("再 創 世 的 凱歌" == assembledAfter)
677-
let cursorShiDe = 3
690+
// ====
678691
let prevAssembly = compositor.assembledSentence
679692
obsCaptured = nil
680693
let overrideSucceeded = compositor.overrideCandidate(
681694
.init(keyArray: ["shi4", "de5"], value: "是的"),
682-
at: cursorShiDe
683-
) {
684-
obsCaptured = $0
685-
}
695+
at: cursorShiDe,
696+
overrideType: .withHighScore,
697+
enforceRetokenization: true,
698+
perceptionHandler: { obsCaptured = $0 }
699+
)
700+
XCTAssertEqual(obsCaptured?.ngramKey, "(chuang4,創)&(shi4,世)&(de5,的)")
686701
XCTAssertTrue(overrideSucceeded)
687-
let currentAssembly = compositor.assemble()
702+
let currentAssembly = compositor.assembledSentence
688703
guard let afterHit = currentAssembly.findGram(at: cursorShiDe) else {
689704
XCTFail("Expected current gram at cursor \(cursorShiDe)")
690705
return
@@ -701,5 +716,27 @@ final class MegrezTestsAdvanced: XCTestCase {
701716
XCTAssertNotNil(obsCaptured)
702717
XCTAssertEqual(obsCaptured?.scenario, .shortToLong)
703718
XCTAssertEqual(obsCaptured?.candidate, "是的")
719+
720+
// 測試 POM 建議的候選覆寫
721+
compositor.clear()
722+
readingKeys.prefix(4).forEach {
723+
_ = compositor.insertKey($0)
724+
}
725+
726+
let pomSuggestedCandidate = Megrez.KeyValuePaired((["shi4"], "", -0.07449307430679043))
727+
let pomSuggestedCandidateOverrideCursor = 2
728+
// let forceHighScoreOverride = false
729+
// overrideType: forceHighScoreOverride ? .withHighScore : .withTopUnigramScore
730+
compositor.overrideCandidate(
731+
pomSuggestedCandidate,
732+
at: pomSuggestedCandidateOverrideCursor,
733+
overrideType: .withTopUnigramScore,
734+
enforceRetokenization: true
735+
)
736+
compositor.assemble()
737+
let assembledByPOM = compositor.assembledSentence
738+
.map(\.value)
739+
.joined(separator: " ")
740+
XCTAssertEqual("再 創 世 的", assembledByPOM)
704741
}
705742
}

0 commit comments

Comments
 (0)