mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-19 06:07:40 +00:00
feat(common/models): reversion in lm-layer
This commit is contained in:
parent
505b5239c1
commit
beb19fdf84
7 changed files with 132 additions and 10 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
common/predictive-text/message.d.ts
vendored
22
common/predictive-text/message.d.ts
vendored
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue