Skip to content

Commit 5f9397e

Browse files
tianjianjiangclaude
andcommitted
feat(keyhandler): integrate contextual user model into KeyHandler
- Wire ContextualUserModel into KeyHandler via LanguageModelManager - Single walk per keystroke (remove post-walk suggest + re-walk) - Selection flow: overrideCandidate → fixSpan → observe → save - Global ContextualUserModel with aliasing shared_ptr - Load from contextual-user-model.txt on startup, save on selection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c0b2e10 commit 5f9397e

4 files changed

Lines changed: 97 additions & 17 deletions

File tree

Source/Engine/gramambular2/reading_grid_test.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,38 @@ TEST(FixedSpanTest, ChainedOverrides) {
970970
<< " us\n";
971971
}
972972

973+
TEST(FixedSpanTest, ClearAlsoResetsFixedSpans) {
974+
ReadingGrid grid(std::make_shared<SimpleLM>(kSampleData));
975+
grid.setReadingSeparator("");
976+
std::vector<std::string> readings = {"ㄍㄠ", "ㄎㄜ", "ㄐㄧˋ",
977+
"ㄍㄨㄥ", "", "ㄉㄜ˙",
978+
"ㄋㄧㄢˊ", "ㄓㄨㄥ", "ㄐㄧㄤˇ",
979+
"ㄐㄧㄣ"};
980+
for (const auto& r : readings) {
981+
grid.insertReading(r);
982+
}
983+
984+
auto nzNode = grid.spans()[6].nodeOf(2);
985+
nzNode->selectOverrideUnigram(
986+
"年終", ReadingGrid::Node::OverrideType::kOverrideValueWithHighScore);
987+
grid.fixSpan(6, nzNode);
988+
auto result = grid.walk();
989+
bool hasNianzhong = false;
990+
for (const auto& v : result.valuesAsStrings()) {
991+
if (v == "年終") hasNianzhong = true;
992+
}
993+
ASSERT_TRUE(hasNianzhong);
994+
995+
grid.clear();
996+
997+
for (const auto& r : readings) {
998+
grid.insertReading(r);
999+
}
1000+
result = grid.walk();
1001+
ASSERT_EQ(result.valuesAsStrings(),
1002+
(std::vector<std::string>{"高科技", "公司", "", "年中", "獎金"}));
1003+
}
1004+
9731005
// Phase 0B Tests: Algorithm Comparison
9741006

9751007
TEST(AlgorithmComparisonTest, Basic10Syllables) {

Source/KeyHandler.mm

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#import "McBopomofoLM.h"
2929
#import "UTF8Helper.h"
3030
#import "UserOverrideModel.h"
31+
#import "contextual_user_model.h"
3132
#import "reading_grid.h"
3233

3334
#import <algorithm>
@@ -60,6 +61,9 @@ @implementation KeyHandler {
6061
// user override model
6162
McBopomofo::UserOverrideModel *_userOverrideModel;
6263

64+
// contextual user model (KN backoff)
65+
Formosa::Gramambular2::ContextualUserModel *_contextualUserModel;
66+
6367
Formosa::Gramambular2::ReadingGrid *_grid;
6468
Formosa::Gramambular2::ReadingGrid::WalkResult _latestWalk;
6569

@@ -104,6 +108,7 @@ - (void)setInputMode:(NSString *)value
104108
std::shared_ptr<Formosa::Gramambular2::LanguageModel> lm(_emptySharedPtr, _languageModel);
105109
_grid = new Formosa::Gramambular2::ReadingGrid(lm);
106110
_grid->setReadingSeparator("-");
111+
_grid->setUserModel(_contextualUserModel);
107112
}
108113

109114
if (!_bpmfReadingBuffer->isEmpty()) {
@@ -128,11 +133,13 @@ - (instancetype)init
128133
_languageModel = [LanguageModelManager languageModelMcBopomofo];
129134
_languageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled);
130135
_userOverrideModel = [LanguageModelManager userOverrideModel];
136+
_contextualUserModel = [LanguageModelManager contextualUserModel];
131137

132138
// This returns a shared_ptr that in turn points to an unmanaged object.
133139
std::shared_ptr<Formosa::Gramambular2::LanguageModel> lm(_emptySharedPtr, _languageModel);
134140
_grid = new Formosa::Gramambular2::ReadingGrid(lm);
135141
_grid->setReadingSeparator("-");
142+
_grid->setUserModel(_contextualUserModel);
136143

137144
_inputMode = InputModeBopomofo;
138145
}
@@ -171,26 +178,48 @@ - (void)syncWithPreferences
171178
- (void)fixNodeWithReading:(NSString *)reading value:(NSString *)value originalCursorIndex:(size_t)originalCursorIndex useMoveCursorAfterSelectionSetting:(BOOL)flag
172179
{
173180
size_t actualCursor = self.actualCandidateCursorIndex;
174-
Formosa::Gramambular2::ReadingGrid::Candidate candidate(reading.UTF8String, value.UTF8String);
175-
if (!_grid->overrideCandidate(actualCursor, candidate)) {
181+
std::string readingStr(reading.UTF8String);
182+
std::string valueStr(value.UTF8String);
183+
Formosa::Gramambular2::ReadingGrid::Candidate candidate(readingStr, valueStr);
184+
if (!_grid->overrideCandidate(actualCursor, candidate,
185+
Formosa::Gramambular2::ReadingGrid::Node::OverrideType::kOverrideValueWithScoreFromTopUnigram)) {
176186
return;
177187
}
178188

179-
Formosa::Gramambular2::ReadingGrid::WalkResult prevWalk = _latestWalk;
189+
// Structurally fix the overridden node so the walk is constrained to it.
190+
auto nodeOpt = _grid->findInSpan(actualCursor, [&readingStr](const auto& node) {
191+
return node->reading() == readingStr && node->isOverridden();
192+
});
193+
if (nodeOpt) {
194+
_grid->fixSpan(actualCursor, *nodeOpt);
195+
}
196+
180197
[self _walk];
181198

182-
// Update the user override model if warranted.
183199
size_t accumulatedCursor = 0;
184200
auto nodeIter = _latestWalk.findNodeAt(actualCursor, &accumulatedCursor);
185201
if (nodeIter == _latestWalk.nodes.cend()) {
186202
return;
187203
}
188204
Formosa::Gramambular2::ReadingGrid::NodePtr currentNode = *nodeIter;
189-
if (currentNode != nullptr && currentNode->currentUnigram().score() > -8) {
190-
_userOverrideModel->observe(prevWalk, _latestWalk, self.actualCandidateCursorIndex, [NSDate date].timeIntervalSince1970);
205+
if (currentNode == nullptr) {
206+
_grid->setCursor(originalCursorIndex);
207+
return;
191208
}
192209

193-
if (currentNode != nullptr && flag && Preferences.moveCursorAfterSelectingCandidate) {
210+
std::string leftReading = Formosa::Gramambular2::ContextualUserModel::kStartSentinel;
211+
std::string leftValue;
212+
if (nodeIter != _latestWalk.nodes.cbegin()) {
213+
auto prevNode = *(nodeIter - 1);
214+
leftReading = prevNode->reading();
215+
leftValue = prevNode->value();
216+
}
217+
_contextualUserModel->observe(leftReading, leftValue,
218+
currentNode->reading(), currentNode->value(),
219+
[NSDate date].timeIntervalSince1970);
220+
[LanguageModelManager saveContextualUserModel];
221+
222+
if (flag && Preferences.moveCursorAfterSelectingCandidate) {
194223
_grid->setCursor(accumulatedCursor);
195224
} else {
196225
_grid->setCursor(originalCursorIndex);
@@ -525,16 +554,6 @@ - (BOOL)handleInput:(KeyHandlerInput *)input state:(InputState *)inState stateCa
525554
_grid->insertReading(reading);
526555
[self _walk];
527556

528-
// get user override model suggestion
529-
if (_inputMode != InputModePlainBopomofo) {
530-
McBopomofo::UserOverrideModel::Suggestion suggestion = _userOverrideModel->suggest(_latestWalk, self.actualCandidateCursorIndex, [NSDate date].timeIntervalSince1970);
531-
if (!suggestion.empty()) {
532-
Formosa::Gramambular2::ReadingGrid::Node::OverrideType type = suggestion.forceHighScoreOverride ? Formosa::Gramambular2::ReadingGrid::Node::OverrideType::kOverrideValueWithHighScore : Formosa::Gramambular2::ReadingGrid::Node::OverrideType::kOverrideValueWithScoreFromTopUnigram;
533-
_grid->overrideCandidate(self.actualCandidateCursorIndex, suggestion.candidate, type);
534-
[self _walk];
535-
}
536-
}
537-
538557
// then update the text
539558
_bpmfReadingBuffer->clear();
540559

Source/LanguageModelManager+Privates.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@
2424
#import "LanguageModelManager.h"
2525
#import "McBopomofoLM.h"
2626
#import "UserOverrideModel.h"
27+
#import "contextual_user_model.h"
2728

2829
NS_ASSUME_NONNULL_BEGIN
2930

3031
@interface LanguageModelManager ()
3132
@property (class, readonly, nonatomic) McBopomofo::McBopomofoLM *languageModelMcBopomofo;
3233
@property (class, readonly, nonatomic) McBopomofo::McBopomofoLM *languageModelPlainBopomofo;
3334
@property (class, readonly, nonatomic) McBopomofo::UserOverrideModel *userOverrideModel;
35+
@property (class, readonly, nonatomic) Formosa::Gramambular2::ContextualUserModel *contextualUserModel;
36+
@property (class, readonly, nonatomic) NSString *contextualUserModelDataPath;
37+
+ (void)saveContextualUserModel;
3438
@end
3539

3640
NS_ASSUME_NONNULL_END

Source/LanguageModelManager.mm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
static McBopomofo::McBopomofoLM gLanguageModelPlainBopomofo;
3535
static McBopomofo::UserOverrideModel gUserOverrideModel(kUserOverrideModelCapacity, kObservedOverrideHalflife);
3636

37+
// Aliasing shared_ptr: wraps the stack-allocated LM for ContextualUserModel.
38+
static std::shared_ptr<Formosa::Gramambular2::LanguageModel> gEmptySharedPtr;
39+
static std::shared_ptr<Formosa::Gramambular2::LanguageModel> gLmPtr(gEmptySharedPtr, &gLanguageModelMcBopomofo);
40+
static Formosa::Gramambular2::ContextualUserModel gContextualUserModel(gLmPtr);
41+
3742
static NSString *const kUserDataTemplateName = @"template-data";
3843
static NSString *const kUserDataPlainBopomofoTemplateName = @"template-data-plain-bpmf";
3944
static NSString *const kExcludedPhrasesMcBopomofoTemplateName = @"template-exclude-phrases";
@@ -72,6 +77,11 @@ + (void)loadDataModels
7277
if (!gLanguageModelPlainBopomofo.isAssociatedPhrasesV2Loaded()) {
7378
LTLoadAssociatedPhrases(gLanguageModelPlainBopomofo);
7479
}
80+
81+
NSString *cumPath = [self contextualUserModelDataPath];
82+
if ([[NSFileManager defaultManager] fileExistsAtPath:cumPath]) {
83+
gContextualUserModel.loadFromFile(cumPath.UTF8String);
84+
}
7585
}
7686

7787
+ (void)loadDataModel:(InputMode)mode
@@ -430,6 +440,21 @@ + (NSString *)phraseReplacementDataPathMcBopomofo
430440
return &gUserOverrideModel;
431441
}
432442

443+
+ (Formosa::Gramambular2::ContextualUserModel *)contextualUserModel
444+
{
445+
return &gContextualUserModel;
446+
}
447+
448+
+ (NSString *)contextualUserModelDataPath
449+
{
450+
return [[self dataFolderPath] stringByAppendingPathComponent:@"contextual-user-model.txt"];
451+
}
452+
453+
+ (void)saveContextualUserModel
454+
{
455+
gContextualUserModel.saveToFile([self contextualUserModelDataPath].UTF8String);
456+
}
457+
433458
+ (BOOL)phraseReplacementEnabled
434459
{
435460
return gLanguageModelMcBopomofo.phraseReplacementEnabled();

0 commit comments

Comments
 (0)