feat(common/models): reversion in lm-layer

This commit is contained in:
jahorton 2020-10-08 14:23:44 +07:00
parent 505b5239c1
commit beb19fdf84
7 changed files with 132 additions and 10 deletions

View file

@ -128,6 +128,8 @@ Message | Direction | Parameters | Expected reply |
`accept` | keyboard → LMLayer | suggestion, | Yes - `postaccept` | Yes
| context, transform | |
`postaccept` | LMLayer → keyboard | reversion | No | Yes
`revert` | keyboard → LMLayer | reversion, context | Yes - `reversion` | Yes
`postrevert` | LMLayer → keyboard | suggestions | No | Yes
### Message: `config`

View file

@ -54,6 +54,7 @@ namespace com.keyman.text.prediction {
private _predictPromises: PromiseStore<Suggestion[]>;
private _wordbreakPromises: PromiseStore<USVString>;
private _acceptPromises: PromiseStore<Reversion>;
private _revertPromises: PromiseStore<Suggestion[]>;
private _nextToken: number;
private capabilities: Capabilities;
@ -71,6 +72,7 @@ namespace com.keyman.text.prediction {
this._predictPromises = new PromiseStore();
this._wordbreakPromises = new PromiseStore();
this._acceptPromises = new PromiseStore();
this._revertPromises = new PromiseStore();
this._nextToken = Number.MIN_SAFE_INTEGER;
this.sendConfig(capabilities);
@ -154,6 +156,19 @@ namespace com.keyman.text.prediction {
});
}
revertSuggestion(reversion: Reversion, context: Context): Promise<Suggestion[]> {
let token = this._nextToken++;
return new Promise((resolve, reject) => {
this._revertPromises.make(token, resolve, reject);
this._worker.postMessage({
message: 'revert',
token: token,
reversion: reversion,
context: context
})
});
}
// TODO: asynchronous close() method.
// Worker code must recognize message and call self.close().
@ -173,6 +188,8 @@ namespace com.keyman.text.prediction {
this._wordbreakPromises.keep(payload.token, payload.word);
} else if (payload.message === 'postaccept') {
this._acceptPromises.keep(payload.token, payload.reversion);
} else if (payload.message === 'postrevert') {
this._revertPromises.keep(payload.token, payload.suggestions);
} else {
// This branch should never execute, but just in case...
//@ts-ignore

View file

@ -30,8 +30,8 @@ type Token = number;
/**
* The valid outgoing message kinds.
*/
type OutgoingMessageKind = 'error' | 'ready' | 'suggestions' | 'currentword' | 'postaccept';
type OutgoingMessage = ErrorMessage | ReadyMessage | SuggestionMessage | CurrentWordMessage | PostAcceptMessage;
type OutgoingMessageKind = 'error' | 'ready' | 'suggestions' | 'currentword' | 'postaccept' | 'postrevert';
type OutgoingMessage = ErrorMessage | ReadyMessage | SuggestionMessage | CurrentWordMessage | PostAcceptMessage | PostRevertMessage;
interface ErrorMessage {
message: 'error';
@ -95,7 +95,7 @@ interface PostAcceptMessage {
/**
* Opaque, unique token that pairs this message
* with the wordbreak message that initiated it.
* with the accept message that initiated it.
*/
token: Token;
@ -105,6 +105,22 @@ interface PostAcceptMessage {
reversion: Reversion;
}
interface PostRevertMessage {
message: 'postrevert';
/**
* Opaque, unique token that pairs this message
* with the revert message that initiated it.
*/
token: Token;
/**
* The original set of Suggestions returned that included the
* reverted Suggestion
*/
suggestions: Suggestion[];
}
/**
* Describes what kind of model to instantiate.
*/

View file

@ -35,7 +35,6 @@ namespace correction {
revert() {
delete this.activeReplacementId;
delete this.replacements;
}
}
@ -188,7 +187,7 @@ namespace correction {
return undefined;
}
return this.item[0];
return this.item(0);
}
get newest(): Item {
@ -196,7 +195,7 @@ namespace correction {
return undefined;
}
return this.item[this.count - 1];
return this.item(this.count - 1);
}
enqueue(item: Item): Item {
@ -224,6 +223,21 @@ namespace correction {
}
}
popNewest(): Item {
if(this.currentTail == this.currentHead) {
return null;
} else {
let item = this.circle[this.currentHead];
this.currentHead = (this.currentHead - 1 + this.maxCount) % this.maxCount;
return item;
}
}
/**
* Returns items contained within the circular array, ordered from 'oldest' to 'newest' -
* the same order in which the items will be dequeued.
* @param index
*/
item(index: number) {
if(index >= this.count) {
throw "Invalid array index";

View file

@ -280,7 +280,7 @@ class LMLayerWorker {
switch(payload.message) {
case 'predict':
var {transform, context} = payload;
let suggestions = compositor.predict(transform, context);
var suggestions = compositor.predict(transform, context);
// Now that the suggestions are ready, send them out!
this.cast('suggestions', {
@ -308,8 +308,17 @@ class LMLayerWorker {
reversion: reversion
});
break;
case 'revert':
var {reversion, context} = payload;
var suggestions: Suggestion[] = compositor.applyReversion(reversion, context);
this.cast('postrevert', {
token: payload.token,
suggestions: suggestions
});
break;
default:
throw new Error(`invalid message; expected one of {'predict', 'wordbreak', 'accept', 'unload'} but got ${payload.message}`);
throw new Error(`invalid message; expected one of {'predict', 'wordbreak', 'accept', 'revert', 'unload'} but got ${payload.message}`);
}
},
compositor: compositor

View file

@ -434,6 +434,49 @@ class ModelCompositor {
return reversion;
}
applyReversion(reversion: Reversion, context: Context): Suggestion[] {
// If we are unable to track context (because the model does not support LexiconTraversal),
// we need a "fallback" strategy.
let compositor = this;
let fallbackSuggestions = function() {
let revertedContext = models.applyTransform(reversion.transform, context);
return compositor.predict({ insert: '', deleteLeft: 0}, revertedContext);
}
if(!this.contextTracker) {
return fallbackSuggestions();
}
// When the context is tracked, we prefer the tracked information.
let contextMatchFound = false;
for(let c = this.contextTracker.count - 1; c >= 0; c--) {
let contextState = this.contextTracker.item(c);
if(contextState.tail.activeReplacementId == -reversion.id) {
contextMatchFound = true;
break;
}
}
if(!contextMatchFound) {
return fallbackSuggestions();
}
// Remove all contexts more recent than the one we're reverting to.
while(this.contextTracker.newest.tail.activeReplacementId != -reversion.id) {
this.contextTracker.popNewest();
}
this.contextTracker.newest.tail.revert();
// Will need to be modified a bit if/when phrase-level suggestions are implemented.
// Those will be tracked on the first token of the phrase, which won't be the tail
// if they cover multiple tokens.
return this.contextTracker.newest.tail.replacements.map(function(trackedSuggestion) {
return trackedSuggestion.suggestion;
});
}
}
/**

View file

@ -38,8 +38,8 @@ type ImportScripts = typeof DedicatedWorkerGlobalScope.prototype.importScripts;
/**
* The valid incoming message kinds.
*/
type IncomingMessageKind = 'config' | 'load' | 'predict' | 'unload' | 'wordbreak' | 'accept';
type IncomingMessage = ConfigMessage | LoadMessage | PredictMessage | UnloadMessage | WordbreakMessage | AcceptMessage;
type IncomingMessageKind = 'config' | 'load' | 'predict' | 'unload' | 'wordbreak' | 'accept' | 'revert';
type IncomingMessage = ConfigMessage | LoadMessage | PredictMessage | UnloadMessage | WordbreakMessage | AcceptMessage | RevertMessage;
/**
* The structure of a config message. It should include the platform's supported
@ -149,6 +149,27 @@ interface AcceptMessage {
postTransform?: Transform;
}
interface RevertMessage {
message: 'revert';
/**
* Opaque, unique token that pairs this accept message with its return message.
*/
token: Token;
/**
* The Reversion being applied. The ID must be assigned and should be the additive inverse
* of the Suggestion being reverted.
*/
reversion: Reversion;
/**
* The Context being reverted, which should be the same context as resulted from applying the
* corresponding Suggestion.
*/
context: Context;
}
/**
* The LMLayer can be in one of the following states. The LMLayer can only produce predictions in the 'ready' state.
*/