diff --git a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts index 92691c8d20b..cd547cf6578 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts @@ -72,11 +72,30 @@ export const CORRECTION_SEARCH_THRESHOLDS = { REPLACEMENT_SEARCH_THRESHOLD: 4 as const // e^-4 = 0.0183156388. Allows "80%" of an extra edit. } +/** + * Tracks common intermediate prediction data, such as its underlying probabilities and its similarity to the actual context. + */ +export interface PredictionMetadata { + /** + * How directly the prediction matches the current token in the context. + * + * This is determined later in the suggestion-analysis project and is not + * available upon initial construction of this type. + */ + matchLevel: SuggestionSimilarity; + + /** + * Text from the triggering input that should _not_ be affected by the + * prediction. + */ + preservationTransform: Transform; +} + /** * Collates information related to suggestions during the suggestion generation * process. */ -export type CorrectionPredictionTuple = { +export interface CorrectionPredictionTupleCore { /** * The potential Suggestion (or Keep) */ @@ -90,19 +109,18 @@ export type CorrectionPredictionTuple = { * by the keystroke-sequence + correction likelihood. */ totalProb: number; +}; + +export interface CorrectionPredictionTuple extends CorrectionPredictionTupleCore { /** - * How directly the prediction matches the current token in the context. + * Contains additional metadata about the prediction and its properties. * - * This is determined later in the suggestion-analysis project and is not - * available upon initial construction of this type. + * This object will generally remain unset by the `predictFromCorrections` + * method, with its values set afterward a layer or two removed from that + * specific call. */ - matchLevel?: SuggestionSimilarity; - /** - * Text from the triggering input that should _not_ be affected by the - * prediction. - */ - preservationTransform?: Transform; -}; + metadata: PredictionMetadata; +} /** * An enum to be used when categorizing the level of similarity between @@ -140,9 +158,15 @@ export enum SuggestionSimilarity { exact = 3 } -export function tupleDisplayOrderSort(a: CorrectionPredictionTuple, b: CorrectionPredictionTuple) { +export function tupleDisplayOrderSort( + a: CorrectionPredictionTuple, + b: CorrectionPredictionTuple +) { + const matchLevelA = a.metadata.matchLevel ?? 0; + const matchLevelB = b.metadata.matchLevel ?? 0; + // Similarity distance - const simDist = (b.matchLevel ?? 0) - (a.matchLevel ?? 0); + const simDist = matchLevelB - matchLevelA; if(simDist != 0) { return simDist; } @@ -177,7 +201,7 @@ export async function correctAndEnumerateWithoutTraversals( revertableTransitionId?: number }> { const inputTransform = transformDistribution[0].sample; - let rawPredictions: CorrectionPredictionTuple[] = []; + let rawPredictions: CorrectionPredictionTupleCore[] = []; let predictionRoots: ProbabilityMass[]; @@ -212,13 +236,20 @@ export async function correctAndEnumerateWithoutTraversals( // Running in bulk over all suggestions, duplicate entries may be possible. rawPredictions = predictFromCorrections(lexicalModel, predictionRoots, context); - if(allowSpace) { - rawPredictions.forEach((entry) => entry.preservationTransform = inputTransform); - } + const predictions = rawPredictions.map((entry) => { + const preservationTransform = allowSpace ? inputTransform : null; + return { + ...entry, + metadata: { + preservationTransform, + matchLevel: SuggestionSimilarity.none // will be overwritten later + } + }; + }); return { postContextState: null, - rawPredictions: rawPredictions + rawPredictions: predictions }; } @@ -499,10 +530,17 @@ export function buildAndMapPredictions( // Worth considering: extend Traversal to allow direct prediction lookups? // let traversal = match.finalTraversal; // ... - let predictions = predictFromCorrections(model, [predictionRoot], predictionContext); - predictions.forEach((entry) => { - entry.preservationTransform = tokenization.taillessTrueKeystroke; + let rawPredictions = predictFromCorrections(model, [predictionRoot], predictionContext); + const predictions = rawPredictions.map((entry) => { entry.prediction.sample.transform.deleteLeft += committedDeleteLeft; + + return { + ...entry, + metadata: { + preservationTransform: tokenization.taillessTrueKeystroke, + matchLevel: SuggestionSimilarity.none // will be overwritten later + } + }; }); return predictions; @@ -649,7 +687,7 @@ export async function correctAndEnumerate( export function shouldStopSearchingEarly( bestCorrectionCost: number, currentCorrectionCost: number, - rawPredictions: CorrectionPredictionTuple[] + rawPredictions: CorrectionPredictionTupleCore[] ) { if(currentCorrectionCost >= bestCorrectionCost + CORRECTION_SEARCH_THRESHOLDS.MAX_SEARCH_THRESHOLD) { return true; @@ -659,10 +697,11 @@ export function shouldStopSearchingEarly( // Very useful for stopping 'sooner' when words reach a sufficient length. return true; } else { - // Sort the prediction list; we need them in descending order for the next check. - rawPredictions.sort(tupleDisplayOrderSort); + // Sort the prediction list; we need them in descending probability order + // for the next check. + rawPredictions.sort((a, b) => b.totalProb - a.totalProb); - // If the best suggestion from the search's current tier fails to beat the worst + // If the best result at the current state of the search fails to beat the worst // pending suggestion from previous tiers, assume all further corrections will // similarly fail to win; terminate the search-loop. if(rawPredictions[ModelCompositor.MAX_SUGGESTIONS-1].totalProb > Math.exp(-currentCorrectionCost)) { @@ -690,8 +729,8 @@ export function predictFromCorrections( lexicalModel: LexicalModel, corrections: ProbabilityMass[], context: Context -): CorrectionPredictionTuple[] { - let returnedPredictions: CorrectionPredictionTuple[] = []; +): CorrectionPredictionTupleCore[] { + let returnedPredictions: CorrectionPredictionTupleCore[] = []; const wordbreak = determineModelWordbreaker(lexicalModel); for(let correction of corrections) { @@ -707,14 +746,13 @@ export function predictFromCorrections( pair.sample.transform.id = correctionTransform.id; } - let tuple: CorrectionPredictionTuple = { + let tuple: CorrectionPredictionTupleCore = { prediction: pair, correction: { sample: correctionRoot, p: correctionProb }, - totalProb: pair.p * correctionProb, - matchLevel: SuggestionSimilarity.none + totalProb: pair.p * correctionProb }; return tuple; }); @@ -847,7 +885,7 @@ export function processSimilarity( if(keyed(tuple.correction.sample) == keyedPrefix) { if(predictedWord == truePrefix) { // Exact match: it's a perfect 'keep' suggestion. - tuple.matchLevel = SuggestionSimilarity.exact; + tuple.metadata.matchLevel = SuggestionSimilarity.exact; keepOption = toAnnotatedSuggestion(lexicalModel, tuple.prediction.sample, 'keep', models.QuoteBehavior.noQuotes); // Indicates that this suggestion exists directly within the lexical @@ -859,15 +897,15 @@ export function processSimilarity( keepOption = tuple.prediction.sample as Outcome; } else if(keyCased(predictedWord) == lowercasedPrefix) { // Case-insensitive match. No diacritic differences; the ONLY difference is casing. - tuple.matchLevel = SuggestionSimilarity.sameText; + tuple.metadata.matchLevel = SuggestionSimilarity.sameText; } else if(keyed(predictedWord) == keyedPrefix) { // Diacritic-insensitive / exact-key match. - tuple.matchLevel = SuggestionSimilarity.sameKey; + tuple.metadata.matchLevel = SuggestionSimilarity.sameKey; } else { - tuple.matchLevel = SuggestionSimilarity.none; + tuple.metadata.matchLevel = SuggestionSimilarity.none; } } else { - tuple.matchLevel = SuggestionSimilarity.none; + tuple.metadata.matchLevel = SuggestionSimilarity.none; } } @@ -932,7 +970,10 @@ export function createDefaultKeep( sample: truePrefix, p: inputTransformProb * MAX_PROB }, - matchLevel: SuggestionSimilarity.exact + metadata: { + preservationTransform: null, + matchLevel: SuggestionSimilarity.exact + } }; } @@ -1019,14 +1060,14 @@ export function predictionAutoSelect(suggestionDistribution: CorrectionPredictio // - such as replacing `cant` with `can't` if the latter is much more frequent - // we may wish to group matchLevel values below by 'mapping' them with an appropriate // function. (Both on the next line and within the reduce functor.) - const bestSuggestionTier = bestSuggestion.matchLevel; + const bestSuggestionTier = bestSuggestion.metadata.matchLevel; // compare best vs other probabilities of compatible tier. const probSum = suggestionDistribution.reduce((accum, current) => { // If the suggestion is from a different similarity tier, do not count it against // the required auto-select probability ratio threshold. That threshold should // only apply within the suggestion's tier. - return accum + (current.matchLevel == bestSuggestionTier ? current.totalProb : 0) + return accum + (current.metadata.matchLevel == bestSuggestionTier ? current.totalProb : 0) }, 0); const proportionOfBest = bestSuggestion.totalProb / probSum; if(proportionOfBest < AUTOSELECT_PROPORTION_THRESHOLD) { @@ -1076,9 +1117,9 @@ export function finalizeSuggestions( // from prior tokens! // // Note: may need adjustment if/when supporting phrase-level correction. - if(tuple.preservationTransform) { - const presDL = tuple.preservationTransform.deleteLeft; - const mergedTransform = models.buildMergedTransform(tuple.preservationTransform, prediction.sample.transform); + if(tuple.metadata.preservationTransform) { + const presDL = tuple.metadata.preservationTransform.deleteLeft; + const mergedTransform = models.buildMergedTransform(tuple.metadata.preservationTransform, prediction.sample.transform); // Any preserved delete-left is applied early because it directly affects the suggestion // root; we need to remove that preserved delete-left here. if(presDL > 0) { diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/early-correction-search-stopping.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/early-correction-search-stopping.tests.ts index bf45e3b94e1..2afd5f5290a 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/early-correction-search-stopping.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/early-correction-search-stopping.tests.ts @@ -1,6 +1,6 @@ import { assert } from 'chai'; -import { CORRECTION_SEARCH_THRESHOLDS, CorrectionPredictionTuple, ModelCompositor, shouldStopSearchingEarly } from "@keymanapp/lm-worker/test-index"; +import { CORRECTION_SEARCH_THRESHOLDS, CorrectionPredictionTupleCore, ModelCompositor, shouldStopSearchingEarly } from "@keymanapp/lm-worker/test-index"; describe('correction-search: shouldStopSearchingEarly', () => { it('stops early once new corrections are less likely than currently discovered predictions', () => { @@ -16,7 +16,7 @@ describe('correction-search: shouldStopSearchingEarly', () => { const predictions = predictionProbs.map((entry) => { return { totalProb: entry - } as CorrectionPredictionTuple + } as CorrectionPredictionTupleCore }); // Thresholding is performed in log-space. @@ -33,8 +33,8 @@ describe('correction-search: shouldStopSearchingEarly', () => { // // Can technically run the method with an empty array, but the actual scenario would have // at least one prediction present in the "found predictions" array. - assert.isFalse(shouldStopSearchingEarly(baseCost, baseCost + expectedThreshold - 0.01, [{ totalProb: Math.exp(-1) } as CorrectionPredictionTuple])); - assert.isTrue(shouldStopSearchingEarly( baseCost, baseCost + expectedThreshold + 0.01, [{ totalProb: Math.exp(-1) } as CorrectionPredictionTuple])); + assert.isFalse(shouldStopSearchingEarly(baseCost, baseCost + expectedThreshold - 0.01, [{ totalProb: Math.exp(-1) } as CorrectionPredictionTupleCore])); + assert.isTrue(shouldStopSearchingEarly( baseCost, baseCost + expectedThreshold + 0.01, [{ totalProb: Math.exp(-1) } as CorrectionPredictionTupleCore])); }); it('stops checking corrections earlier when enough predictions have been found', () => { @@ -46,7 +46,7 @@ describe('correction-search: shouldStopSearchingEarly', () => { const predictions = predictionProbs.map((entry) => { return { totalProb: entry - } as CorrectionPredictionTuple + } as CorrectionPredictionTupleCore }); const baseCost = 1; diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/auto-correct.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/auto-correct.tests.ts index 882390591a9..354324210ef 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/auto-correct.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/auto-correct.tests.ts @@ -1,6 +1,19 @@ import { assert } from 'chai'; -import { AUTOSELECT_PROPORTION_THRESHOLD, CorrectionPredictionTuple, predictionAutoSelect, SuggestionSimilarity, tupleDisplayOrderSort } from "@keymanapp/lm-worker/test-index"; +import { + AUTOSELECT_PROPORTION_THRESHOLD, + CorrectionPredictionTuple, + predictionAutoSelect, + PredictionMetadata, + SuggestionSimilarity, + tupleDisplayOrderSort +} from "@keymanapp/lm-worker/test-index"; + +const defaultMetadata: PredictionMetadata = { + matchLevel: SuggestionSimilarity.none, + preservationTransform: undefined +} + /* * Preconditions: * - there should always be a 'keep' option. Now, whether or not that option @@ -35,7 +48,8 @@ describe('predictionAutoSelect', () => { }, p: 1 }, - totalProb: 1 + totalProb: 1, + metadata: defaultMetadata } ]; @@ -66,7 +80,8 @@ describe('predictionAutoSelect', () => { }, p: 0.01 }, - totalProb: 0.01 + totalProb: 0.01, + metadata: defaultMetadata }, { correction: { @@ -84,7 +99,8 @@ describe('predictionAutoSelect', () => { }, p: 0.8 }, - totalProb: 0.8 + totalProb: 0.8, + metadata: defaultMetadata } ]; @@ -115,7 +131,8 @@ describe('predictionAutoSelect', () => { }, p: 1 }, - totalProb: 1 + totalProb: 1, + metadata: defaultMetadata } ]; @@ -145,10 +162,11 @@ describe('predictionAutoSelect', () => { }, p: .05 }, - totalProb: .04 + totalProb: .04, + metadata: defaultMetadata } - const highestNonKeepSuggestion: CorrectionPredictionTuple = { + const highestNonKeepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thin', p: .8 @@ -163,7 +181,8 @@ describe('predictionAutoSelect', () => { }, p: .55 }, - totalProb: .44 + totalProb: .44, + metadata: defaultMetadata }; const predictions: CorrectionPredictionTuple[] = [ @@ -184,7 +203,8 @@ describe('predictionAutoSelect', () => { }, p: .4 }, - totalProb: .32 + totalProb: .32, + metadata: defaultMetadata }, { correction: { @@ -201,7 +221,8 @@ describe('predictionAutoSelect', () => { }, p: 1 }, - totalProb: .2 + totalProb: .2, + metadata: defaultMetadata } ]; @@ -214,7 +235,7 @@ describe('predictionAutoSelect', () => { }); it(`selects solitary non-'keep' suggestion when 'keep' does not match model`, () => { - const keepSuggestion: CorrectionPredictionTuple = { + const keepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thin', p: .8 @@ -231,14 +252,15 @@ describe('predictionAutoSelect', () => { }, p: .05 }, - totalProb: .04 + totalProb: .04, + metadata: defaultMetadata } // To 'win', a suggestion (currently) needs at least twice the probability of the sum of all alternatives. // This threshold may be subject to change. // // Refer to AUTOSELECT_PROPORTION_THRESHOLD in predict-helpers.ts. - const onlyNonKeepSuggestion: CorrectionPredictionTuple = { + const onlyNonKeepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thin', p: .8 @@ -253,7 +275,8 @@ describe('predictionAutoSelect', () => { }, p: .01 }, - totalProb: .008 + totalProb: .008, + metadata: defaultMetadata }; const predictions: CorrectionPredictionTuple[] = [ @@ -275,7 +298,7 @@ describe('predictionAutoSelect', () => { }); it(`does not select non-'keep' without sufficient winning probability`, () => { - const keepSuggestion: CorrectionPredictionTuple = { + const keepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thin', p: .8 @@ -292,14 +315,15 @@ describe('predictionAutoSelect', () => { }, p: .05 }, - totalProb: .04 + totalProb: .04, + metadata: defaultMetadata } // To 'win', a suggestion (currently) needs at least twice the probability of the sum of all alternatives. // This threshold may be subject to change. // // Refer to AUTOSELECT_PROPORTION_THRESHOLD in predict-helpers.ts. - const highestNonKeepSuggestion: CorrectionPredictionTuple = { + const highestNonKeepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thin', p: .8 @@ -314,7 +338,8 @@ describe('predictionAutoSelect', () => { }, p: .55 }, - totalProb: .44 + totalProb: .44, + metadata: defaultMetadata }; const predictions: CorrectionPredictionTuple[] = [ @@ -335,7 +360,8 @@ describe('predictionAutoSelect', () => { }, p: .4 }, - totalProb: .32 + totalProb: .32, + metadata: defaultMetadata }, { correction: { @@ -352,7 +378,8 @@ describe('predictionAutoSelect', () => { }, p: 1 }, - totalProb: .2 + totalProb: .2, + metadata: defaultMetadata } ]; @@ -370,7 +397,7 @@ describe('predictionAutoSelect', () => { }); it(`does select non-'keep' with sufficient winning probability`, () => { - const keepSuggestion: CorrectionPredictionTuple = { + const keepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thin', p: .8 @@ -387,10 +414,11 @@ describe('predictionAutoSelect', () => { }, p: .05 }, - totalProb: .04 + totalProb: .04, + metadata: defaultMetadata } - const highestNonKeepSuggestion: CorrectionPredictionTuple = { + const highestNonKeepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thin', p: .9 @@ -405,7 +433,8 @@ describe('predictionAutoSelect', () => { }, p: .75 }, - totalProb: .675 + totalProb: .675, + metadata: defaultMetadata }; const predictions: CorrectionPredictionTuple[] = [ @@ -426,7 +455,8 @@ describe('predictionAutoSelect', () => { }, p: .2 }, - totalProb: .18 + totalProb: .18, + metadata: defaultMetadata }, { correction: { @@ -443,7 +473,8 @@ describe('predictionAutoSelect', () => { }, p: 1 }, - totalProb: .1 + totalProb: .1, + metadata: defaultMetadata } ]; @@ -459,7 +490,7 @@ describe('predictionAutoSelect', () => { }); it('ignores non key-matched suggestions when key-matched suggestions exist', () => { - const keepSuggestion: CorrectionPredictionTuple = { + const keepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'cant', p: 1 @@ -477,10 +508,10 @@ describe('predictionAutoSelect', () => { p: 1 }, totalProb: 1, - matchLevel: SuggestionSimilarity.exact + metadata: { matchLevel: SuggestionSimilarity.exact, preservationTransform: null } } - const expectedSuggestion: CorrectionPredictionTuple = { + const expectedSuggestion: CorrectionPredictionTuple= { correction: { sample: 'cant', p: 1 @@ -496,7 +527,7 @@ describe('predictionAutoSelect', () => { p: .2 }, totalProb: .2, - matchLevel: SuggestionSimilarity.sameKey + metadata: { matchLevel: SuggestionSimilarity.sameKey, preservationTransform: null } }; const predictions: CorrectionPredictionTuple[] = [ @@ -518,7 +549,7 @@ describe('predictionAutoSelect', () => { p: .8 }, totalProb: .8, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: null } } ]; @@ -534,7 +565,7 @@ describe('predictionAutoSelect', () => { // The idea: avoid "over-correcting" when a potential correction has a // super-high-frequency word. it('does not auto-select suggestion if its root correction is not most likely', () => { - const keepSuggestion: CorrectionPredictionTuple = { + const keepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thi', p: .7 @@ -551,10 +582,11 @@ describe('predictionAutoSelect', () => { }, p: .05 }, - totalProb: .035 + totalProb: .035, + metadata: defaultMetadata } - const highestCorrectionSuggestion: CorrectionPredictionTuple = { + const highestCorrectionSuggestion: CorrectionPredictionTuple= { correction: { sample: 'thi', p: .7 @@ -569,10 +601,11 @@ describe('predictionAutoSelect', () => { }, p: .1 }, - totalProb: .07 + totalProb: .07, + metadata: defaultMetadata }; - const highestNonKeepSuggestion: CorrectionPredictionTuple = { + const highestNonKeepSuggestion: CorrectionPredictionTuple= { correction: { sample: 'the', p: .3 @@ -587,7 +620,8 @@ describe('predictionAutoSelect', () => { }, p: 1 }, - totalProb: .3 + totalProb: .3, + metadata: defaultMetadata }; const predictions: CorrectionPredictionTuple[] = [ diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts index 73d8276e132..a2729d36334 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts @@ -46,7 +46,6 @@ describe('buildAndMapPredictions', () => { ]; const basePredictions = predictFromCorrections(plainModel, correctionDistribution, context); - basePredictions.forEach((entry) => assert.isNotOk(entry.preservationTransform)); // must construct the taillessTrueKeystroke appropriately. const tailless = { insert: 'TEST', deleteLeft: 0 }; @@ -64,8 +63,8 @@ describe('buildAndMapPredictions', () => { ); assert.deepEqual(mappedPredictions.map((tuple) => tuple.prediction), basePredictions.map((tuple) => tuple.prediction)); - mappedPredictions.forEach((tuple) => assert.isOk(tuple.preservationTransform)); - mappedPredictions.forEach((tuple) => tuple.preservationTransform == tailless); + mappedPredictions.forEach((tuple) => assert.isOk(tuple.metadata?.preservationTransform)); + mappedPredictions.forEach((tuple) => tuple.metadata?.preservationTransform == tailless); }); it('properly handles empty prediction roots from deleted same-token codepoints', () => { diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/create-default-keep.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/create-default-keep.tests.ts index 6b5981c8343..3c093a1af99 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/create-default-keep.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/create-default-keep.tests.ts @@ -12,7 +12,12 @@ import { assert } from 'chai'; import { LexicalModelTypes } from "@keymanapp/common-types"; import * as wordBreakers from '@keymanapp/models-wordbreakers'; -import { CorrectionPredictionTuple, createDefaultKeep, models, SuggestionSimilarity } from "@keymanapp/lm-worker/test-index"; +import { + CorrectionPredictionTuple, + createDefaultKeep, + models, + SuggestionSimilarity +} from "@keymanapp/lm-worker/test-index"; import CasingFunction = LexicalModelTypes.CasingFunction; import Context = LexicalModelTypes.Context; @@ -126,7 +131,7 @@ describe('produceKeep', () => { p: 1 }, totalProb: 1, - matchLevel: SuggestionSimilarity.exact + metadata: { matchLevel: SuggestionSimilarity.exact, preservationTransform: null } }; const tuple = createDefaultKeep(testModelWithCasing, context, trueInput); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/predict-from-corrections.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/predict-from-corrections.tests.ts index 31f5063c15c..d18f2878418 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/predict-from-corrections.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/predict-from-corrections.tests.ts @@ -4,7 +4,7 @@ import { assert } from 'chai'; import { deepCopy } from "keyman/common/web-utils"; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { models, predictFromCorrections, tupleDisplayOrderSort } from "@keymanapp/lm-worker/test-index"; +import { models, predictFromCorrections } from "@keymanapp/lm-worker/test-index"; import CasingFunction = LexicalModelTypes.CasingFunction; import Context = LexicalModelTypes.Context; @@ -115,7 +115,7 @@ describe('predictFromCorrections', () => { const predictions = predictFromCorrections(model, correctionDistribution, context); predictions.forEach((entry) => assert.equal(entry.correction.sample, 'Its')); predictions.forEach((entry) => assert.equal(entry.correction.p, 0.6)); - predictions.sort(tupleDisplayOrderSort); + predictions.sort((a, b) => b.totalProb - a.totalProb); assert.sameDeepOrderedMembers(predictions.map((entry) => entry.prediction.sample), dummied_suggestions); @@ -167,7 +167,7 @@ describe('predictFromCorrections', () => { const predictions = predictFromCorrections(model, correctionDistribution, context); predictions.forEach((entry) => assert.equal(entry.correction.sample, 'Its')); predictions.forEach((entry) => assert.equal(entry.correction.p, 0.6)); - predictions.sort(tupleDisplayOrderSort); + predictions.sort((a, b) => b.totalProb - a.totalProb); assert.sameOrderedMembers(predictions.map((entry) => entry.prediction.sample.displayAs), ["it's", "its"]); assert.sameDeepOrderedMembers(predictions.map((entry) => entry.prediction.sample), dummied_suggestions.map((entry) => { @@ -247,7 +247,7 @@ describe('predictFromCorrections', () => { }); const predictions = predictFromCorrections(model, correctionDistribution, context); - predictions.sort(tupleDisplayOrderSort); + predictions.sort((a, b) => b.totalProb - a.totalProb); assert.sameOrderedMembers(predictions.map((entry) => entry.prediction.sample.displayAs), ["is", "it's", "isn't", "its"]); assert.sameDeepMembers(predictions.map((entry) => entry.prediction.sample), dummied_suggestions.flatMap((entry) => entry)); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-deduplication.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-deduplication.tests.ts index bfd2eeefafc..6a78ef1fd8e 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-deduplication.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-deduplication.tests.ts @@ -4,7 +4,13 @@ import * as wordBreakers from '@keymanapp/models-wordbreakers'; import { deepCopy } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CorrectionPredictionTuple, dedupeSuggestions, models } from "@keymanapp/lm-worker/test-index"; +import { + CorrectionPredictionTuple, + CorrectionPredictionTupleCore, + SuggestionSimilarity, + dedupeSuggestions, + models +} from "@keymanapp/lm-worker/test-index"; import Context = LexicalModelTypes.Context; import DummyModel = models.DummyModel; @@ -18,13 +24,23 @@ const testModel = new DummyModel({ // No suggestions needed here, so we don't define any. }); +const mockMetadata: (tc: CorrectionPredictionTupleCore) => CorrectionPredictionTuple = (t: CorrectionPredictionTupleCore) => { + return { + ...t, + metadata: { + preservationTransform: null, + matchLevel: SuggestionSimilarity.none + } + } +}; + /** * Builds a fresh copy of test values useful for suggestion-similarity * testing. * @returns */ const build_its_is_set = () => { - const its: CorrectionPredictionTuple = { + const its: CorrectionPredictionTupleCore = { correction: { sample: 'its', p: 0.8 @@ -43,7 +59,7 @@ const build_its_is_set = () => { // matchLevel does not yet exist. }; - const it_is: CorrectionPredictionTuple = { + const it_is: CorrectionPredictionTupleCore = { correction: { sample: 'its', p: 0.8 @@ -61,7 +77,7 @@ const build_its_is_set = () => { totalProb: 0.64 }; - const is: CorrectionPredictionTuple = { + const is: CorrectionPredictionTupleCore = { correction: { sample: 'is', p: 0.2 @@ -79,7 +95,7 @@ const build_its_is_set = () => { totalProb: 0.1 }; - const is_not: CorrectionPredictionTuple = { + const is_not: CorrectionPredictionTupleCore = { correction: { sample: 'is', p: 0.2 @@ -115,7 +131,7 @@ describe('dedupeSuggestions', () => { }; const testSet = build_its_is_set(); - const predictions = [...Object.values(testSet)]; + const predictions: CorrectionPredictionTuple[] = [...Object.values(testSet)].map(mockMetadata) ; const deduplicated = dedupeSuggestions(testModel, predictions, context); @@ -136,10 +152,10 @@ describe('dedupeSuggestions', () => { ...Object.values(testSet).map((entry) => deepCopy(entry)), ...Object.values(testSet).map((entry) => deepCopy(entry)), deepCopy(testSet.it_is) // as in, `it's`, the contraction. - ]; + ].map(mockMetadata); const deduplicated = dedupeSuggestions(testModel, predictions, context); - const expected = [...Object.values(testSet)]; + const expected = [...Object.values(testSet)].map(mockMetadata); // Note: only changes the _total_ probability. // // There's no mathematically safe way to combine the components if the diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-finalization.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-finalization.tests.ts index 387bf6dab93..01b017e5fee 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-finalization.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-finalization.tests.ts @@ -5,7 +5,7 @@ import { deepCopy } from 'keyman/common/web-utils'; import * as wordBreakers from '@keymanapp/models-wordbreakers'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CorrectionPredictionTuple, finalizeSuggestions, models } from "@keymanapp/lm-worker/test-index"; +import { CorrectionPredictionTuple, PredictionMetadata, SuggestionSimilarity, finalizeSuggestions, models } from "@keymanapp/lm-worker/test-index"; import DummyModel = models.DummyModel; import Outcome = LexicalModelTypes.Outcome; @@ -48,6 +48,11 @@ const testModelWithoutSpacing = new DummyModel({ const build_its_is_set = (verbose?: string) => { const verboseFlag = (verbose == 'verbose' ? true : false); + const metadata: PredictionMetadata = { + matchLevel: SuggestionSimilarity.none, + preservationTransform: undefined + }; + const its: CorrectionPredictionTuple = { correction: { sample: 'its', @@ -63,8 +68,8 @@ const build_its_is_set = (verbose?: string) => { }, p: 0.2 }, - totalProb: 0.16 - // matchLevel does not yet exist. + totalProb: 0.16, + metadata: {...metadata} }; const it_is: CorrectionPredictionTuple = { @@ -82,7 +87,8 @@ const build_its_is_set = (verbose?: string) => { }, p: 0.8 }, - totalProb: 0.64 + totalProb: 0.64, + metadata: {...metadata} }; const is: CorrectionPredictionTuple = { @@ -100,7 +106,8 @@ const build_its_is_set = (verbose?: string) => { }, p: 0.5 }, - totalProb: 0.1 + totalProb: 0.1, + metadata: {...metadata} }; const is_not: CorrectionPredictionTuple = { @@ -118,7 +125,8 @@ const build_its_is_set = (verbose?: string) => { }, p: 0.5 }, - totalProb: 0.1 + totalProb: 0.1, + metadata: {...metadata} }; const baseDefinitions = { diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-similarity.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-similarity.tests.ts index 6cfa731429d..9672b102dff 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-similarity.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/suggestion-similarity.tests.ts @@ -5,7 +5,14 @@ import * as wordBreakers from '@keymanapp/models-wordbreakers'; import { deepCopy } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CorrectionPredictionTuple, models, processSimilarity, SuggestionSimilarity, toAnnotatedSuggestion } from "@keymanapp/lm-worker/test-index"; +import { + CorrectionPredictionTuple, + models, + PredictionMetadata, + processSimilarity, + SuggestionSimilarity, + toAnnotatedSuggestion +} from "@keymanapp/lm-worker/test-index"; import CasingFunction = LexicalModelTypes.CasingFunction; import Context = LexicalModelTypes.Context; @@ -109,6 +116,11 @@ const testModelWithCasing = new DummyModel({ * @returns */ const build_its_is_set = () => { + const metadata: PredictionMetadata = { + matchLevel: SuggestionSimilarity.none, + preservationTransform: undefined + }; + const its: CorrectionPredictionTuple = { correction: { sample: 'its', @@ -124,8 +136,8 @@ const build_its_is_set = () => { }, p: 0.2 }, - totalProb: 0.16 - // matchLevel does not yet exist. + totalProb: 0.16, + metadata: {...metadata} }; const it_is: CorrectionPredictionTuple = { @@ -143,7 +155,8 @@ const build_its_is_set = () => { }, p: 0.8 }, - totalProb: 0.64 + totalProb: 0.64, + metadata: {...metadata} }; const is: CorrectionPredictionTuple = { @@ -161,7 +174,8 @@ const build_its_is_set = () => { }, p: 0.5 }, - totalProb: 0.1 + totalProb: 0.1, + metadata: {...metadata} }; const is_not: CorrectionPredictionTuple = { @@ -179,7 +193,8 @@ const build_its_is_set = () => { }, p: 0.5 }, - totalProb: 0.1 + totalProb: 0.1, + metadata: {...metadata} }; return { @@ -213,16 +228,16 @@ describe('processSimilarity', () => { const expectation: CorrectionPredictionTuple[] = [ { ...testSet.its, - matchLevel: SuggestionSimilarity.exact + metadata: { matchLevel: SuggestionSimilarity.exact, preservationTransform: undefined } }, { ...testSet.it_is, - matchLevel: SuggestionSimilarity.sameKey + metadata: { matchLevel: SuggestionSimilarity.sameKey, preservationTransform: undefined } }, { ...testSet.is, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } }, { ...testSet.is_not, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } } ]; @@ -260,16 +275,16 @@ describe('processSimilarity', () => { const expectation: CorrectionPredictionTuple[] = [ { ...testSet.its, - matchLevel: SuggestionSimilarity.sameKey + metadata: { matchLevel: SuggestionSimilarity.sameKey, preservationTransform: undefined } }, { ...testSet.it_is, - matchLevel: SuggestionSimilarity.exact + metadata: { matchLevel: SuggestionSimilarity.exact, preservationTransform: undefined } }, { ...testSet.is, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } }, { ...testSet.is_not, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } } ]; @@ -324,17 +339,17 @@ describe('processSimilarity', () => { const expectation: CorrectionPredictionTuple[] = [ { ...testSet.its, - matchLevel: SuggestionSimilarity.sameKey + metadata: { matchLevel: SuggestionSimilarity.sameKey, preservationTransform: undefined } }, { ...testSet.it_is, // case mismatch, detectable because we have access to a lowercasing/uppercasing function. - matchLevel: SuggestionSimilarity.sameText + metadata: { matchLevel: SuggestionSimilarity.sameText, preservationTransform: undefined } }, { ...testSet.is, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } }, { ...testSet.is_not, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } } ]; @@ -378,17 +393,17 @@ describe('processSimilarity', () => { const expectation: CorrectionPredictionTuple[] = [ { ...testSet.its, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } }, { ...testSet.it_is, // case mismatch, detectable because we have access to a lowercasing/uppercasing function. - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } }, { ...testSet.is, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } }, { ...testSet.is_not, - matchLevel: SuggestionSimilarity.none + metadata: { matchLevel: SuggestionSimilarity.none, preservationTransform: undefined } } ];