Skip to content

Commit 2a9c6f2

Browse files
committed
change(web): support multi-token suggestion similarity
Build-bot: skip build:web Test-bot: skip
1 parent dbbe633 commit 2a9c6f2

3 files changed

Lines changed: 36 additions & 42 deletions

File tree

web/src/engine/predictive-text/worker-thread/src/main/model-compositor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class ModelCompositor {
174174
const deduplicatedSuggestionTuples = dedupeSuggestions(this.lexicalModel, rawPredictions, context);
175175

176176
// Needs "casing" to be applied first.
177-
const hasExistingKeep = processSimilarity(this.lexicalModel, deduplicatedSuggestionTuples, context, transformDistribution[0]);
177+
const hasExistingKeep = processSimilarity(this.lexicalModel, deduplicatedSuggestionTuples, context, postContext);
178178

179179
// If no existing suggestion directly matches the user-visible version of
180180
// the token, also add a 'keep' suggestion (with `.matchesModel = false`)

web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -954,55 +954,49 @@ export function dedupeSuggestions(
954954
export function processSimilarity(
955955
lexicalModel: LexicalModel,
956956
suggestionDistribution: IntermediateCompositedPrediction[],
957-
context: Context,
958-
trueInput: ProbabilityMass<Transform>
957+
baseContext: Context,
958+
finalContext: Context
959959
): boolean {
960-
const { sample: inputTransform } = trueInput;
961960
const wordbreak = determineModelWordbreaker(lexicalModel);
962961

963-
const postContext = models.applyTransform(inputTransform, context);
964-
const truePrefix = wordbreak(postContext);
965-
966962
const keyed = (text: string) => lexicalModel.toKey ? lexicalModel.toKey(text) : text;
967963
const keyCased = (text: string) => lexicalModel.applyCasing ? lexicalModel.applyCasing('lower', text) : text;
968-
const keyedPrefix = keyed(truePrefix);
969-
const lowercasedPrefix = keyCased(truePrefix);
964+
const keyedTarget = keyed(finalContext.left);
965+
const lowercasedTarget = keyCased(finalContext.left);
970966

971967
let keepOption: Outcome<Keep>;
972968

973-
for(let tuple of suggestionDistribution) {
974-
// Don't set it unnecessarily; this can have side-effects in some automated tests.
975-
if(inputTransform.id !== undefined) {
976-
tuple.components.prediction.transformId = inputTransform.id;
977-
}
969+
// If there are no suggestions found, we can't validate that the underlying
970+
// correction was an empty token.
971+
let allCorrectionsEmpty: boolean = suggestionDistribution.length > 0
972+
? true
973+
: wordbreak(finalContext) == '';
978974

979-
const predictedWord = wordbreak(models.applyTransform(tuple.components.prediction.transform, context));
975+
for(let tuple of suggestionDistribution) {
976+
const appliedContext = models.applyTransform(tuple.components.prediction.transform, baseContext);
977+
allCorrectionsEmpty &&= tuple.components.correction == '';
980978

981979
// Is the suggestion an exact match (or, "similar enough") to the
982980
// actually-typed context? If so, we wish to note this fact and to
983981
// prioritize such a suggestion over suggestions that are not.
984-
if(keyed(tuple.components.correction) == keyedPrefix) {
985-
if(predictedWord == truePrefix) {
986-
// Exact match: it's a perfect 'keep' suggestion.
987-
tuple.metadata.matchLevel = SuggestionSimilarity.exact;
988-
keepOption = toAnnotatedSuggestion(lexicalModel, tuple.components.prediction, 'keep', models.QuoteBehavior.noQuotes);
989-
990-
// Indicates that this suggestion exists directly within the lexical
991-
// model as a valid suggestion. (We actively display it if it's an
992-
// exact match, but hide it if not, only preserving it for reversions
993-
// if/when needed.)
994-
keepOption.matchesModel = true;
995-
Object.assign(tuple.components.prediction, keepOption);
996-
keepOption = tuple.components.prediction as Outcome<Keep>;
997-
} else if(keyCased(predictedWord) == lowercasedPrefix) {
998-
// Case-insensitive match. No diacritic differences; the ONLY difference is casing.
999-
tuple.metadata.matchLevel = SuggestionSimilarity.sameText;
1000-
} else if(keyed(predictedWord) == keyedPrefix) {
1001-
// Diacritic-insensitive / exact-key match.
1002-
tuple.metadata.matchLevel = SuggestionSimilarity.sameKey;
1003-
} else {
1004-
tuple.metadata.matchLevel = SuggestionSimilarity.none;
1005-
}
982+
if(appliedContext.left == finalContext.left) {
983+
// Exact match: it's a perfect 'keep' suggestion.
984+
tuple.metadata.matchLevel = SuggestionSimilarity.exact;
985+
keepOption = toAnnotatedSuggestion(lexicalModel, tuple.components.prediction, 'keep', models.QuoteBehavior.noQuotes);
986+
987+
// Indicates that this suggestion exists directly within the lexical
988+
// model as a valid suggestion. (We actively display it if it's an
989+
// exact match, but hide it if not, only preserving it for reversions
990+
// if/when needed.)
991+
keepOption.matchesModel = true;
992+
Object.assign(tuple.components.prediction, keepOption);
993+
keepOption = tuple.components.prediction as Outcome<Keep>;
994+
} else if(keyCased(appliedContext.left) == lowercasedTarget) {
995+
// Case-insensitive match. No diacritic differences; the ONLY difference is casing.
996+
tuple.metadata.matchLevel = SuggestionSimilarity.sameText;
997+
} else if(keyed(appliedContext.left) == keyedTarget) {
998+
// Diacritic-insensitive / exact-key match.
999+
tuple.metadata.matchLevel = SuggestionSimilarity.sameKey;
10061000
} else {
10071001
tuple.metadata.matchLevel = SuggestionSimilarity.none;
10081002
}
@@ -1012,7 +1006,7 @@ export function processSimilarity(
10121006
//
10131007
// No actual 'keep' needed if the current context token is empty, so we say we
10141008
// have a 'keep' for that case, even though there isn't really one.
1015-
return !!(keepOption || truePrefix == '');
1009+
return !!(keepOption || allCorrectionsEmpty);
10161010
}
10171011

10181012
/**

web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-similarity.tests.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ describe('processSimilarity', () => {
233233
const keep_its = toAnnotatedSuggestion(testModelWithCasing, original_its.components.prediction, 'keep', QuoteBehavior.noQuotes);
234234
keep_its.matchesModel = true;
235235

236-
processSimilarity(testModelWithCasing, distribution, context, trueInput);
236+
processSimilarity(testModelWithCasing, distribution, context, models.applyTransform(trueInput.sample, context));
237237

238238
assert.sameDeepMembers(distribution, expectation);
239239
assert.equal(its.components.prediction.tag, 'keep');
@@ -270,7 +270,7 @@ describe('processSimilarity', () => {
270270
const keep_it_is = toAnnotatedSuggestion(testModelWithCasing, original_it_is.components.prediction, 'keep', QuoteBehavior.noQuotes);
271271
keep_it_is.matchesModel = true;
272272

273-
processSimilarity(testModelWithCasing, distribution, context, trueInput);
273+
processSimilarity(testModelWithCasing, distribution, context, models.applyTransform(trueInput.sample, context));
274274

275275
assert.sameDeepMembers(distribution, expectation);
276276
assert.equal(it_is.components.prediction.tag, 'keep');
@@ -318,7 +318,7 @@ describe('processSimilarity', () => {
318318
expectation[1].metadata.matchLevel = SuggestionSimilarity.sameText; // it_is
319319
expectation[2].metadata.matchLevel = SuggestionSimilarity.none; // is
320320
expectation[3].metadata.matchLevel = SuggestionSimilarity.none; // is_not
321-
processSimilarity(testModelWithCasing, distribution, context, trueInput);
321+
processSimilarity(testModelWithCasing, distribution, context, models.applyTransform(trueInput.sample, context));
322322

323323
// Because we mucked with the casing here, there is no perfect 'keep' match.
324324
const keep = distribution.find((entry) => entry.components.prediction.tag == 'keep');
@@ -358,7 +358,7 @@ describe('processSimilarity', () => {
358358
const expectation: IntermediateCompositedPrediction[] = [...Object.values(testSet)];
359359

360360
expectation.forEach((entry) => entry.metadata.matchLevel = SuggestionSimilarity.none);
361-
processSimilarity(testModelWithoutCasing, distribution, context, trueInput);
361+
processSimilarity(testModelWithoutCasing, distribution, context, models.applyTransform(trueInput.sample, context));
362362

363363
// Because we mucked with the casing here, there is no perfect 'keep' match.
364364
const keep = distribution.find((entry) => entry.components.prediction.tag == 'keep');

0 commit comments

Comments
 (0)