spiegel-keyman/web/source/text/prediction/modelManager.ts
2019-03-14 15:42:38 +07:00

178 lines
No EOL
5.7 KiB
TypeScript

// Defines the text processing KMW core.
///<reference path="../processor.ts" />
// Defines the LMLayer's outer shell
///<reference path="../../includes/lmlayer.ts" />
// Defines the ModelManager and its related types.
///<reference path="../../kmwkeyboards.ts" />
namespace com.keyman.text.prediction {
export interface ModelSpec {
/**
* The model's unique identifier.
*/
id: string;
/**
* The list of supported BCP-47 language codes. Only one language should be supported,
* although multiple variants based on region code (at min) may be specified.
*/
languages: string[];
/**
* The path/URL to the file that defines the model.
*/
path: string;
}
/**
* Corresponds to the 'invalidatesuggestions' ModelManager event.
*/
export type InvalidateSuggestionsHandler = () => boolean;
/**
* Corresponds to the 'suggestionsready' ModelManager event.
*/
export type ReadySuggestionsHandler = (suggestions: Suggestion[]) => boolean;
export type ModelChangeEnum = 'loaded'|'unloaded'|'changed';
/**
* Corresponds to the 'modelchange' ModelManager event.
*/
export type ModelChangeHandler = (state: ModelChangeEnum) => boolean;
export class ModelManager {
private lmEngine: LMLayer;
private currentModel?: ModelSpec;
private currentPromise: Promise<Suggestion[]>;
// Tracks registered models by ID.
private registeredModels: {[id: string]: ModelSpec} = {};
// Allows for easy model lookup by language code; useful when switching keyboards.
private languageModelMap: {[language:string]: ModelSpec} = {};
private static EVENT_PREFIX: string = "kmw.mm.";
init() {
let keyman = com.keyman.singleton;
// Establishes KMW's platform 'capabilities', which limit the range of context a LMLayer
// model may expect.
let capabilities: Capabilities = {
maxLeftContextCodeUnits: 64
}
this.lmEngine = new LMLayer(capabilities);
// Registers this module for keyboard (language) and model change events.
keyman['addEventListener']('keyboardchange', this.onKeyboardChange.bind(this));
}
private unloadModel() {
this.lmEngine.unloadModel();
delete this.currentModel;
}
private loadModel(model: ModelSpec) {
if(!model) {
throw new Error("Null reference not allowed.");
}
let file = model.path;
this.lmEngine.loadModel(file);
this.currentModel = model;
}
onKeyboardChange(kbdInfo: KeyboardChangeData) {
let keyman = com.keyman.singleton;
let lgCode = kbdInfo['languageCode'];
let model = this.languageModelMap[lgCode];
if(this.currentModel !== model) {
let stateChange = 0; // Base value; will not remain the same
if(this.currentModel) {
this.unloadModel();
stateChange += 2;
}
if(model) {
this.loadModel(model);
stateChange += 1;
}
if(stateChange == 0) {
console.warn("Unexpected lack of model state change in ModelManager.onKeyboardChange!");
return;
}
let changeParam: ModelChangeEnum = stateChange == 1 ? 'loaded' : (stateChange == 2 ? 'unloaded' : 'changed');
keyman.util.callEvent(ModelManager.EVENT_PREFIX + 'modelchange', changeParam);
}
}
// Accessible publicly as keyman.modelManager.register(model: ModelSpec)
register(model: ModelSpec): void {
let keyman = com.keyman.singleton;
let activeLanguage = keyman.keyboardManager.getActiveLanguage();
this.registeredModels[model.id] = model;
// Register the model for each targeted language code variant.
let mm = this;
model.languages.forEach(function(code: string) {
mm.languageModelMap[code] = model;
// The model's for our active language! Activate it!
if(code == activeLanguage) {
// Manually trigger our model-update event function.
mm.onKeyboardChange({
['internalName']: keyman.keyboardManager.getActiveKeyboardName(),
['languageCode']: code,
['indirect']: true
});
}
});
}
isRegistered(model: ModelSpec): boolean {
return !! this.registeredModels[model.id];
}
/**
* Function addEventListener
* Scope Public
* @param {string} event event to handle
* @param {function(Event)} func event handler function
* @return {boolean} value returned by util.addEventListener
* Description Wrapper function to add and identify KeymanWeb-specific event handlers
*/
['addEventListener'](event: string, func: ReadySuggestionsHandler | InvalidateSuggestionsHandler): boolean {
let keyman = com.keyman.singleton;
return keyman.util.addEventListener(ModelManager.EVENT_PREFIX + event, func);
}
public predict(transform: Transform, context: Context) {
// If there's no active model, there can be no predictions.
if(!this.currentModel) {
return;
}
let keyman = com.keyman.singleton;
var promise = this.currentPromise = this.lmEngine.predict(transform, context);
// We've already invalidated any suggestions resulting from any previously-existing Promise -
// may as well officially invalidate them via event.
keyman.util.callEvent(ModelManager.EVENT_PREFIX + "invalidatesuggestions", null);
let mm = this;
promise.then(function(suggestions: Suggestion[]) {
if(promise == mm.currentPromise) {
keyman.util.callEvent(ModelManager.EVENT_PREFIX + "suggestionsready", suggestions);
}
})
}
public shutdown() {
this.lmEngine.shutdown();
}
}
}