From 7bda1e5d93f419ef8438dfd1a7706ce6953c8266 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 26 Jun 2024 09:59:29 +0700 Subject: [PATCH] feat(web): determine suggestion to use for auto-correct --- common/models/types/index.d.ts | 5 ++ .../lm-worker/src/main/model-compositor.ts | 62 ++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/common/models/types/index.d.ts b/common/models/types/index.d.ts index 2fc1c92cb3..af1b17c3ae 100644 --- a/common/models/types/index.d.ts +++ b/common/models/types/index.d.ts @@ -333,6 +333,11 @@ declare interface Suggestion { * to the input text. Ex: 'keep', 'emoji', 'correction', etc. */ tag?: SuggestionTag; + + /** + * Set to true if this suggestion is a valid auto-accept target. + */ + autoAccept?: boolean } interface Reversion extends Suggestion { diff --git a/common/web/lm-worker/src/main/model-compositor.ts b/common/web/lm-worker/src/main/model-compositor.ts index befdae1826..f8416d6c35 100644 --- a/common/web/lm-worker/src/main/model-compositor.ts +++ b/common/web/lm-worker/src/main/model-compositor.ts @@ -69,8 +69,7 @@ export default class ModelCompositor { const { sample: correctionTransform, p: correctionProb } = correction; const correctionRoot = this.wordbreak(models.applyTransform(correction.sample, context)); - let predictionSet = predictions.map(function(pair: ProbabilityMass) { - + let predictionSet = predictions.map((pair: ProbabilityMass) => { // Let's not rely on the model to copy transform IDs. // Only bother is there IS an ID to copy. if(correctionTransform.id !== undefined) { @@ -473,6 +472,16 @@ export default class ModelCompositor { return b.totalProb - a.totalProb; // Use descending order - we want the largest probabilty suggestions first! }); + // Section 4: Auto-correction + finalization. + if(keepOption && keepOption.matchesModel) { + // Auto-select it for auto-acceptance; we don't correct away from perfectly-valid + // lexical entries, even if they are comparatively low-frequency. + keepOption.autoAccept = true; + } else { + this.predictionAutoSelect(suggestionDistribution); + } + + // Now that we've marked the suggestion to auto-select, we can finalize the suggestions. let suggestions = suggestionDistribution.splice(0, ModelCompositor.MAX_SUGGESTIONS).map((tuple) => { const prediction = tuple.prediction; @@ -489,7 +498,7 @@ export default class ModelCompositor { } = { ...prediction.sample, p: tuple.totalProb, - "lexical-p": tuple.prediction.p, + "lexical-p": prediction.p, "correction-p": tuple.correction.p } @@ -559,6 +568,53 @@ export default class ModelCompositor { return suggestions; } + private predictionAutoSelect(suggestionDistribution: CorrectionPredictionTuple[]) { + if(suggestionDistribution.length == 0) { + return; + } + + if(suggestionDistribution.length == 1) { + // Mark for auto-acceptance; there are no alternatives. + suggestionDistribution[0].prediction.sample.autoAccept = true; + return; + } + + // Is it reasonable to auto-accept any of our suggestions? + const bestSuggestion = suggestionDistribution[0]; + + const baseCorrection = bestSuggestion.correction.sample; + if(baseCorrection.length == 0) { + // If the correction is rooted on an empty root, there's no basis for + // auto-correcting to this suggestion. + return; + } + + // Find the highest probability for any correction that led to a valid prediction. + // No need to full-on re-sort everything, though. + const bestCorrection = suggestionDistribution.reduce((prev, current) => prev?.correction.p > current.correction.p ? prev : current, null).correction; + if(bestCorrection.p > bestSuggestion.correction.p) { + // Here, the best suggestion didn't come from the best correction. + // Is it actually reasonable to auto-correct? We're probably just very + // biased toward its frequency. (Maybe a threshold should be considered?) + return; + } + + // compare best vs other probabilities. + const probSum = suggestionDistribution.reduce((accum, current) => accum + current.totalProb, 0); + const proportionOfBest = bestSuggestion.totalProb / probSum; + if(proportionOfBest < .66) { + return; + } + + // compare correction-cost aspects? We disable if the base correction is lower than best, + // but should we do other comparisons too? + + // const nextSuggestion = suggestionDistribution[1]; + // baseCorrection + + bestSuggestion.prediction.sample.autoAccept = true; + } + // Responsible for applying casing rules to suggestions. private applySuggestionCasing(suggestion: Suggestion, baseWord: USVString, casingForm: CasingForm) { // Step 1: does the suggestion replace the whole word? If not, we should extend the suggestion to do so.