Merge pull request #1644 from keymanapp/web-lmlayer-model-registration

[Web] [LMLayer] Model registration rework & integration
This commit is contained in:
Joshua Horton 2019-03-12 07:17:48 +07:00 committed by GitHub
commit 2c99cb723f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 758 additions and 308 deletions

View file

@ -9,12 +9,18 @@
LMLAYER_OUTPUT=build
WORKER_OUTPUT=build/intermediate
INCLUDES_OUTPUT=build/includes
NAKED_WORKER=$WORKER_OUTPUT/index.js
EMBEDDED_WORKER=$WORKER_OUTPUT/embedded_worker.js
# Build the worker and the main script.
build ( ) {
# Ensure that the build-product destination for any generated include .d.ts files exists.
if ! [ -d $INCLUDES_OUTPUT ]; then
mkdir -p "$INCLUDES_OUTPUT"
fi
# Build worker first; the main file depends on it.
# Then wrap the worker; Then build the main file.
@ -24,8 +30,8 @@ build ( ) {
# Builds the top-level JavaScript file (the second stage of compilation)
build-main () {
npm run tsc -- -p ./tsconfig.json || fail "Could not build top-level JavaScript file."
cp ./index.d.ts build/index.d.ts
cp ./message.d.ts build/message.d.ts
cp ./index.d.ts $INCLUDES_OUTPUT/LMLayer.d.ts
cp ./message.d.ts $INCLUDES_OUTPUT/message.d.ts
}
# Builds the inner JavaScript worker (the first stage of compilation).
@ -36,6 +42,20 @@ build-worker () {
fi
npm run tsc -- -p ./worker/tsconfig.json || fail "Could not build worker."
get_builder_OS
# macOS has a slightly different sed, which needs an extension to use for a backup file. Thanks, Apple.
BACKUP_EXT=
if [ $os_id == 'mac' ]; then
BACKUP_EXT='.bak'
fi
# Tweak the output index.d.ts to have an updated reference to message.d.ts
sed -i $BACKUP_EXT 's/path="\.\.\/\.\.\/message\.d\.ts"/path="message\.d\.ts"/g' "${WORKER_OUTPUT}/index.d.ts" \
|| fail "Could not update message.d.ts reference"
mv $WORKER_OUTPUT/index.d.ts $INCLUDES_OUTPUT/LMLayerWorker.d.ts
}
# A nice, extensible method for -clean operations. Add to this as necessary.

View file

@ -0,0 +1,18 @@
#!/bin/sh
# The diagram is built using the Graphviz suite.
# You can get it with most package managers, e.g.,
#
# sudo apt installl graphviz # Ubuntu
#
# brew install graphviz # macOS
# Check if Graphviz/dot is installed
if ! hash dot ; then
echo "Cannot (re)build state diagram" 1>&2
echo "Missing the Graphviz suite" 1>&2
echo "Download at $(tput bold)https://www.graphviz.org/$(tput sgr0)"
exit 1
fi
dot -Tpng lmlayer-states.dot -o lmlayer-states.png

View file

@ -0,0 +1,13 @@
digraph LMLayerFSM {
rankdir=LR;
node [shape = point] 0;
edge [fontname="Courier"];
node [shape = circle];
0 -> unconfigured;
unconfigured -> modelless [label="config"];
modelless -> ready [label="load"];
ready -> ready [label="predict"];
ready -> modelless [label="unload"];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -117,77 +117,99 @@ Currently there are four message types:
Message | Direction | Parameters | Expected reply | Uses token
--------------|--------------------|---------------------|---------------------|---------------
`initialize` | keyboard → LMLayer | capabilities, model | Yes — `ready` | No
`config` | LMLayer -> worker | capabilities | No | No
`load` | keyboard → LMLayer | model | Yes — `ready` | No
`unload` | keyboard → LMLayer | none | No | No
`ready` | LMLayer → keyboard | configuration | No | No
`predict` | keyboard → LMLayer | transform, context | Yes — `suggestions` | Yes
`suggestions` | LMLayer → keyboard | suggestions | No | Yes
### Message: `initialize`
### Message: `config`
Must be sent from the keyboard to the LMLayer so that the LMLayer
initializes a model. It will send `initialization` which is a plain
JavaScript object specify the path to the model, as well configurations
and platform restrictions.
may properly configure loaded models. It will send `config`, a plain
JavaScript object specifying platform restrictions.
The keyboard **MUST NOT** send any messages to the LMLayer prior to
sending `initialize`. The keyboard **SHOULD NOT** send another message
to the keyboard until it receives `ready` message from the LMLayer
before sending another message.
The keyboard **MUST NOT** send any messages to the LMLayer prior to sending `config`.
After this, it is safe to assume the `config` was performed successfully and is ready to
`load` a model.
The LMLayer needs to know the platform's abilities and restrictions (capabilities), as well as which concrete language model to instantiate. These properties are passed as `capabilities` and `model`, respectively.
The LMLayer needs to know the platform's abilities and restrictions (capabilities).
```typescript
interface InitializeMessage {
message: 'initialize';
interface LoadMessage {
message: 'load';
/**
* A ModelDescription that describes the language model and its parameters.
* The concrete documentation on is a valid ModelDescription
* can be found elsewhere.
* The path to the model's compiled script file.
*/
model: {
/**
* What kind of model to instantiate. This is subject to availability,
* but common examples are 'wordlist', 'fst', and 'dummy'.
*/
type: string;
/**
* Each model type defines a set of configurable parameters. Please
* see the corresponding model's documentation for an extensive list.
*/
...parameters: any;
};
capabilities: {
/**
* Whether the platform supports deleting to the right.
* The absence of this rule implies false.
*/
supportsDeleteRight?: false,
/**
* The maximum amount of UTF-16 code units that the keyboard will
* provide to the left of the cursor, as an integer.
* The maximum amount of UTF-16 code units that the keyboard will provide to
* the left of the cursor, as an integer.
*/
maxLeftContextCodeUnits: number,
/**
* The maximum amount of code units that the keyboard will provide to
* the right of the cursor, as an integer. The absence of this rule
* implies the platform is incapable of supplying right contexts.
* See also, [[supportsRightContexts]].
* The maximum amount of code units that the keyboard will provide to the
* right of the cursor, as an integer. The value 0 or the absence of this
* rule implies that the right contexts are not supported.
*/
maxRightContextCodeUnits?: number,
/**
* Whether the platform supports deleting to the right. The absence of this
* rule implies false.
*/
supportsDeleteRight?: false,
}
}
```
### Message: `load`
Must be sent from the keyboard to the LMLayer so that the LMLayer
loads a model. It will send `load` which is a plain
JavaScript object specify the path to the model, as well configurations
and platform restrictions.
After a single `config` message, the keyboard **MUST NOT** send any messages
to the LMLayer prior to sending `load`. The keyboard **SHOULD NOT** send another
message to the keyboard until it receives `ready` message from the LMLayer
before sending another message.
The LMLayer needs to know which concrete language model to instantiate. This is provided
by the file at the path specified by the `model` string parameter.
```typescript
interface LoadMessage {
message: 'load';
/**
* The path to the model's compiled script file.
*/
model: string
}
```
### Message: `unload`
Must be sent from the keyboard to the LMLayer so that the LMLayer
resets itself in preparation for loading a new model. It will send `unload`
which is a plain message to trigger release of old model resources.
```typescript
interface UnloadMessage {
message: 'unload';
}
```
### Message: `ready`
Must be sent from the LMLayer to the keyboard when the LMLayer's model
as a response to `initialize`. It will send `configuration`, which is
as a response to `load`. It will send `configuration`, which is
a plain JavaScript object requesting configuration from the keyboard.
There are only two options defined so far:
@ -393,3 +415,17 @@ keyboard to acknowledge late suggestions, or for the LMLayer to avoid
sending late `suggestions` messages. In either case, a `suggestions`
message can be identified as appropriate or "late" via its `token`
property.
## *Informative*: LMLayer worker as a state machine
The LMLayer worker can be seen in the following states:
- `unconfigured`
- `model-less`
- `ready`
The transitions of this diagram correspond to messages as described
above.
![State machine of the LMLayer](./lmlayer-states.png)

View file

@ -13,18 +13,23 @@ declare namespace com.keyman.text.prediction {
/**
* Construct the top-level LMLayer interface. This also starts the underlying Worker.
* Make sure to call .initialize() when using the default Worker.
* Make sure to call .load() when using the default Worker.
*
* @param uri URI of the underlying LMLayer worker code. This will usually be a blob:
* or file: URI. If uri is not provided, this will start the default Worker.
*/
constructor(worker?: Worker);
constructor(capabilities: Capabilities, worker?: Worker);
/**
* Initializes the LMLayer worker with the keyboard/platform's capabilities,
* as well as a description of the model required.
*/
initialize(capabilities: Capabilities, model: ModelDescription): Promise<Configuration>;
loadModel(model: string): Promise<Configuration>;
/**
* Prepares the LMLayer for reinitialization with a different model/capability set.
*/
unloadModel();
predict(transform: Transform, context: Context): Promise<Suggestion[]>;
@ -57,5 +62,11 @@ declare namespace com.keyman.text.prediction {
* }));
*/
static asBlobURI(fn: Function): string;
/**
* Clears out any computational resources in use by the LMLayer, including shutting
* down any internal WebWorkers.
*/
public shutdown(): void;
}
}

View file

@ -34,9 +34,11 @@
* Since the Worker runs in a different thread, the public methods of this class are
* asynchronous. Methods of note include:
*
* - #initialize() -- initialize the LMLayer with a configuration and language model
* - #loadModel() -- loads a specified model file
* - #predict() -- ask the LMLayer to offer suggestions (predictions or corrections) for
* the input event
* - #unloadModel() -- unloads the LMLayer's currently loaded model, preparing it to
* receive (load) a new model
*
* The top-level LMLayer will automatically starts up its own Web Worker.
*/
@ -51,33 +53,46 @@ namespace com.keyman.text.prediction {
private _declareLMLayerReady: (conf: Configuration) => void;
private _promises: PromiseStore<Suggestion[]>;
private _nextToken: number;
private capabilities: Capabilities;
/**
* Construct the top-level LMLayer interface. This also starts the underlying Worker.
* Make sure to call .initialize() when using the default Worker.
*
* @param uri URI of the underlying LMLayer worker code. This will usually be a blob:
* or file: URI. If uri is not provided, this will start the default Worker.
*/
constructor(worker?: Worker) {
constructor(capabilities: Capabilities, worker?: Worker) {
// Either use the given worker, or instantiate the default worker.
this._worker = worker || new Worker(LMLayer.asBlobURI(LMLayerWorkerCode));
this._worker.onmessage = this.onMessage.bind(this)
this._declareLMLayerReady = null;
this._promises = new PromiseStore;
this._nextToken = Number.MIN_SAFE_INTEGER;
this.sendConfig(capabilities);
}
/**
* Initializes the LMLayer worker with the keyboard/platform's capabilities,
* as well as a description of the model required.
* Initializes the LMLayer worker with the host platform's capability set.
*
* @param capabilities The host platform's capability spec - a model cannot assume access to more context
* than specified by this parameter.
*/
initialize(capabilities: Capabilities, model: ModelDescription): Promise<Configuration> {
private sendConfig(capabilities: Capabilities) {
this._worker.postMessage({
message: 'config',
capabilities: capabilities
});
}
/**
* Initializes the LMLayer worker with a path to the desired model file.
*/
loadModel(modelFilePath: string): Promise<Configuration> {
return new Promise((resolve, _reject) => {
this._worker.postMessage({
message: 'initialize',
capabilities,
model
message: 'load',
model: modelFilePath
});
// Sets up so the promise is resolved in the onMessage() callback, when it receives
@ -86,6 +101,16 @@ namespace com.keyman.text.prediction {
});
}
/**
* Unloads the previously-active model from memory, resetting the LMLayer to prep
* for transition to use of a new model.
*/
public unloadModel() {
this._worker.postMessage({
message: 'unload'
});
}
predict(transform: Transform, context: Context): Promise<Suggestion[]> {
let token = this._nextToken++;
return new Promise((resolve, reject) => {
@ -115,6 +140,14 @@ namespace com.keyman.text.prediction {
}
}
/**
* Clears out any computational resources in use by the LMLayer, including shutting
* down any internal WebWorkers.
*/
public shutdown() {
this._worker.terminate();
}
/**
* Given a function, this utility returns the source code within it, as a string.
* This is intended to unwrap the "wrapped" source code created in the LMLayerWorker

View file

@ -8,48 +8,48 @@ let LMLayer = require('../../build');
describe('LMLayer', function() {
describe('[[constructor]]', function () {
it('should accept a Worker to instantiate', function () {
new LMLayer(createFakeWorker());
new LMLayer(capabilities(), createFakeWorker());
});
it('should send the `config` message to the LMLayer', async function () {
let fakeWorker = createFakeWorker(fakePostMessage);
let lmLayer = new LMLayer(capabilities(), fakeWorker);
assert.propertyVal(fakeWorker.postMessage, 'callCount', 1);
// In the "Worker", assert the message looks right
function fakePostMessage(data) {
assert.propertyVal(data, 'message', 'config');
assert.isObject(data.capabilities);
}
});
});
describe('#initialize()', function () {
describe('#loadModel()', function () {
it('should accept capabilities and model description', function () {
let fakeWorker = createFakeWorker();
let lmLayer = new LMLayer(fakeWorker);
lmLayer.initialize(
{
maxLeftContextCodeUnits: 32,
},
{
kind: 'wordlist',
words: ['foo', 'bar', 'baz', 'quux']
}
);
let lmLayer = new LMLayer(capabilities(), fakeWorker);
lmLayer.loadModel("./unit_tests/in_browser/resources/models/simple-dummy.js");
assert.isFunction(fakeWorker.onmessage, 'LMLayer failed to set a callback!');
});
it('should send the `initialize` message to the LMLayer', async function () {
it('should send the `load` message to the LMLayer', async function () {
let fakeWorker = createFakeWorker(fakePostMessage);
let lmLayer = new LMLayer(fakeWorker);
let configuration = await lmLayer.initialize(
{
maxLeftContextCodeUnits: 32,
},
{
kind: 'wordlist',
words: ['foo', 'bar', 'baz', 'quux']
}
);
let lmLayer = new LMLayer(capabilities(), fakeWorker);
let configuration = await lmLayer.loadModel("./unit_tests/in_browser/resources/models/simple-dummy.js");
assert.propertyVal(fakeWorker.postMessage, 'callCount', 1);
assert.propertyVal(fakeWorker.postMessage, 'callCount', 2);
// In the "Worker", assert the message looks right and
// ASYNCHRONOUSLY reply with ready message.
function fakePostMessage(data) {
assert.propertyVal(data, 'message', 'initialize')
assert.isObject(data.capabilities);
assert.isObject(data.model);
// Expected first call: config. Ignore it.
if(data.message == 'config') {
return;
}
assert.propertyVal(data, 'message', 'load');
assert.isString(data.model);
callAsynchronously(() => fakeWorker.onmessage({
data: {
@ -75,8 +75,8 @@ describe('LMLayer', function() {
}));
});
let lmLayer = new LMLayer(fakeWorker);
let actualConfiguration = await lmLayer.initialize(
let lmLayer = new LMLayer(capabilities, fakeWorker);
let actualConfiguration = await lmLayer.loadModel(
{
maxLeftContextCodeUnits: 32,
},
@ -86,7 +86,7 @@ describe('LMLayer', function() {
}
);
// This SHOULD be called by initialize().
// This SHOULD be called by loadModel().
assert.deepEqual(actualConfiguration, expectedConfiguration);
})
});

View file

@ -12,13 +12,20 @@ describe('LMLayerWorker', function() {
describe('#constructor()', function() {
it('should allow for the mocking of postMessage()', function () {
var fakePostMessage = sinon.fake();
var worker = new LMLayerWorker({ postMessage: fakePostMessage });
var context = {
postMessage: fakePostMessage
};
context.importScripts = importScriptsWith(context);
var worker = LMLayerWorker.install(context);
// Sending it the initialize it should notify us that it's initialized!
// First the worker must receive config data...
configWorker(worker);
// Sending it the `load` message should notify us that it's loaded!
worker.onMessage(createMessageEventWithData({
message: 'initialize',
model: dummyModel(),
capabilities: defaultCapabilities()
message: 'load',
model: "./unit_tests/in_browser/resources/models/simple-dummy.js"
}));
assert(fakePostMessage.calledOnce);
});
@ -26,9 +33,11 @@ describe('LMLayerWorker', function() {
describe('#onMessage()', function() {
it('should fail if not given the `message` attribute', function () {
var worker = new LMLayerWorker({
postMessage: sinon.fake(), // required, but ignored in this test case
});
var context = {
postMessage: sinon.fake()
};
context.importScripts = importScriptsWith(context);
var worker = LMLayerWorker.install(context);
// Every message is a discriminated union with the tag being `message`.
// If it doesn't see 'message', something is deeply wrong,
// and it should loudly let us know.
@ -46,17 +55,21 @@ describe('LMLayerWorker', function() {
onmessage: undefined,
postMessage: new sinon.fake(),
};
fakeWorkerGlobal.importScripts = importScriptsWith(fakeWorkerGlobal);
// Instantiate and install a worker on our global object.
var worker = LMLayerWorker.install(fakeWorkerGlobal);
assert.instanceOf(worker, LMLayerWorker);
// It should have installed a callback.
assert.isFunction(fakeWorkerGlobal.onmessage);
// First the worker must receive config data...
configWorker(worker);
// Send a message; we should get something back.
worker.onMessage(createMessageEventWithData({
message: 'initialize',
model: dummyModel(),
capabilities: defaultCapabilities()
message: 'load',
model: "./unit_tests/in_browser/resources/models/simple-dummy.js"
}));
// It called the postMessage() in its global scope.
@ -64,11 +77,48 @@ describe('LMLayerWorker', function() {
});
});
describe('Message: initialize', function () {
describe('Message: config', function () {
it('should disallow any other message', function () {
var worker = new LMLayerWorker({
postMessage: sinon.fake(), // required, but ignored
});
var context = {
postMessage: sinon.fake()
};
context.importScripts = importScriptsWith(context);
var worker = LMLayerWorker.install(context);
// It should not respond to 'predict'
assert.throws(function () {
worker.onMessage(createMessageEventWithData({
message: 'predict',
}));
}, /invalid message/i);
});
it('accepts a capability set and transitions to the "modelless" state', function () {
var context = {
postMessage: sinon.fake()
};
context.importScripts = importScriptsWith(context);
var worker = LMLayerWorker.install(context);
// Trigger the config message
configWorker(worker);
assert.equal(worker.state.name, 'modelless');
});
});
describe('Message: load', function () {
it('should disallow any other message', function () {
var context = {
postMessage: sinon.fake()
};
context.importScripts = importScriptsWith(context);
var worker = LMLayerWorker.install(context);
configWorker(worker);
// It should not respond to 'predict'
assert.throws(function () {
@ -80,11 +130,17 @@ describe('LMLayerWorker', function() {
it('should send back a "ready" message', function () {
var fakePostMessage = sinon.fake();
var worker = new LMLayerWorker({ postMessage: fakePostMessage });
var context = {
postMessage: fakePostMessage
};
context.importScripts = importScriptsWith(context);
var worker = LMLayerWorker.install(context);
configWorker(worker);
worker.onMessage(createMessageEventWithData({
message: 'initialize',
model: dummyModel(),
capabilities: defaultCapabilities()
message: 'load',
model: "./unit_tests/in_browser/resources/models/simple-dummy.js"
}));
assert(fakePostMessage.calledOnceWith(sinon.match({
@ -94,14 +150,19 @@ describe('LMLayerWorker', function() {
it('should send back configuration', function () {
var fakePostMessage = sinon.fake();
var worker = new LMLayerWorker({ postMessage: fakePostMessage });
var context = {
postMessage: fakePostMessage
};
context.importScripts = importScriptsWith(context);
var worker = LMLayerWorker.install(context);
configWorker(worker);
// simple-dummy.js is set with the following.
var maxCodeUnits = 64;
worker.onMessage(createMessageEventWithData({
message: 'initialize',
model: dummyModel(),
capabilities: {
maxLeftContextCodeUnits: maxCodeUnits,
}
message: 'load',
model: "./unit_tests/in_browser/resources/models/simple-dummy.js"
}));
sinon.assert.calledWithMatch(fakePostMessage, {

View file

@ -3,31 +3,14 @@
*/
var assert = require('chai').assert;
var DummyModel = require('../../build/intermediate').models.DummyModel;
describe('LMLayerWorker dummy model', function() {
describe('instantiation', function () {
it('can be instantiated with capabilities', function () {
var model = new DummyModel(defaultCapabilities);
it('can be instantiated with no arguments', function () {
var model = new DummyModel();
assert.isObject(model);
});
it('supports dependency-injected configuration', function () {
let configuration = {
leftContextCodeUnits: 64,
rightContextCodeUnits: 0
};
var model = new DummyModel({
maxLeftContextCodeUnits: 64,
},
{
configuration: configuration,
});
assert.deepEqual(model.configuration, configuration);
});
});
describe('prediction', function () {
@ -66,7 +49,7 @@ describe('LMLayerWorker dummy model', function() {
},
];
var model = new DummyModel(defaultCapabilities());
var model = new DummyModel();
// Type a 't'
var suggestions = model.predict({
@ -90,9 +73,7 @@ describe('LMLayerWorker dummy model', function() {
assert.isDefined(futureSuggestions[2]);
assert.isDefined(futureSuggestions[3]);
var model = new DummyModel(defaultCapabilities, {
futureSuggestions: futureSuggestions
});
var model = new DummyModel({futureSuggestions: futureSuggestions});
// The dummy model should give suggestions in order,
// regardless of the provided transform and context.

View file

@ -3,13 +3,12 @@
*/
var assert = require('chai').assert;
var WordListModel = require('../../build/intermediate').models.WordListModel;
describe('LMLayerWorker word list model', function() {
describe('instantiation', function () {
it('can be instantiated with an empty word list', function () {
var model = new WordListModel(defaultCapabilities(), []);
var model = new WordListModel([]);
assert.isObject(model);
});
@ -26,7 +25,6 @@ describe('LMLayerWorker word list model', function() {
// «t| » [Send]
// [ to ] [ the ] [ this ]
var model = new WordListModel(
defaultCapabilities(),
jsonFixture('wordlists/english-1000')
);
@ -52,7 +50,6 @@ describe('LMLayerWorker word list model', function() {
// «th| » [Send]
// [ this ] [ the ] [ there ]
var model = new WordListModel(
defaultCapabilities(),
jsonFixture('wordlists/english-1000')
);
@ -93,7 +90,6 @@ describe('LMLayerWorker word list model', function() {
// «| » [Send]
// [ I'm ] [ I ] [ Hey ]
var model = new WordListModel(
defaultCapabilities(),
jsonFixture('wordlists/english-1000')
);
@ -116,7 +112,6 @@ describe('LMLayerWorker word list model', function() {
// «I g| » [Send]
// [ gave ] [ got ] [ got the ]
var model = new WordListModel(
defaultCapabilities(),
jsonFixture('wordlists/english-1000')
);

View file

@ -17,13 +17,17 @@ describe('LMLayerWorker', function () {
// Initialize the worker with a model that will produce one suggestion.
var fakePostMessage = sinon.fake();
var worker = new LMLayerWorker({ postMessage: fakePostMessage });
var context = {
postMessage: fakePostMessage
};
context.importScripts = importScriptsWith(context);
var worker = LMLayerWorker.install(context);
configWorker(worker);
worker.onMessage(createMessageEventWithData({
message: 'initialize',
model: dummyModel([
[suggestion]
]),
capabilities: defaultCapabilities()
message: 'load',
model: "./unit_tests/in_browser/resources/models/simple-dummy.js"
}));
sinon.assert.calledWithMatch(fakePostMessage.lastCall, {
message: 'ready',
@ -38,10 +42,14 @@ describe('LMLayerWorker', function () {
transform: zeroTransform(),
context: emptyContext()
}));
// Retrieve the internal 'dummy' suggestions for comparison.
var hazel = iGotDistractedByHazel();
sinon.assert.calledWithMatch(fakePostMessage.lastCall, {
message: 'suggestions',
token: token,
suggestions: sinon.match.array.deepEquals([suggestion])
suggestions: hazel[0]
});
});

View file

@ -4,6 +4,9 @@
* Globally-defined helper functions for use in in Mocha tests.
*/
var fs = require("fs");
var vm = require("vm");
// Choose the appropriate global object. Either `global` in
// Node, or `window` in browsers.
var _ = global || window;
@ -17,6 +20,26 @@ _.createMessageEventWithData = function createMessageEventWithData(data) {
return { data };
}
/**
* Creates a simple, default capabilities object for standard-case LMLayer init.
*/
_.capabilities = function capabilities() {
return {
maxLeftContextCodeUnits: 64
}
}
/**
* Mimics a message from the outer LMLayer shell with a simple, default config object.
* Used for Worker tests.
*/
_.configWorker = function configWorker(worker) {
worker.onMessage(createMessageEventWithData({
message: 'config',
capabilities: _.capabilities()
}));
}
/**
* A valid model that suggests exactly what you want it to suggest.
*
@ -97,4 +120,20 @@ if (typeof require === 'function') {
// └── ...
return require('./in_browser/json/' + name);
}
// This worker-global function does not exist by default in Node!
_.importScriptsWith = function(context) {
return function() { // the constructed context's importScripts method.
/* Use of vm.createContext and script.runInContext allow us to avoid
* polluting the global scope with imports. When we throw away the
* context object, imported scripts will be automatically GC'd.
*/
for(var i=0; i < arguments.length; i++) {
context = vm.createContext(context);
var script = new vm.Script(fs.readFileSync(arguments[i]));
script.runInContext(context);
}
}
}
}

View file

@ -33,7 +33,7 @@ module.exports = {
files: [
// Include the generated worker code. Make sure it's linked before any of the test cases.
'../../build/index.js',
'helpers.js', // Provides utility helpers and objects for tests.
'cases/**/*.js', // Where the tests actually reside.
// We don't have anything in these locations... yet. But they'll be useful for test resources.
@ -59,6 +59,10 @@ module.exports = {
variableName: '__json__'
},
proxies: {
"/resources/": "/base/resources/"
},
// web server port
port: 9876,

View file

@ -3,9 +3,10 @@ var LMLayer = com.keyman.text.prediction.LMLayer;
describe('LMLayer', function () {
describe('[[constructor]]', function () {
it('should construct with zero arguments', function () {
let lmLayer = new LMLayer();
it('should construct with a single argument', function () {
let lmLayer = new LMLayer(helpers.defaultCapabilities);
assert.instanceOf(lmLayer, LMLayer);
lmLayer.shutdown();
});
});
@ -25,6 +26,7 @@ describe('LMLayer', function () {
let worker = new Worker(uri);
worker.onmessage = function thisShouldBeCalled(event) {
assert.propertyVal(event, 'data', 'fhqwhgads');
worker.terminate();
done();
};
})

View file

@ -7,25 +7,19 @@ var LMLayer = com.keyman.text.prediction.LMLayer;
* **injectable** suggestions: that is, you, as the tester, have
* to provide the predictions. The dummy model does not create any
* suggestions on its own. The dummy model can take in a series
* of suggestions when initialized and return them sequentially.
* of suggestions when loaded and return them sequentially.
*/
describe('LMLayer using dummy model', function () {
describe('Prediction', function () {
it('will predict future suggestions', function () {
var lmLayer = new LMLayer();
var capabilities = {
maxLeftContextCodeUnits: 32 + ~~Math.random() * 32
};
var lmLayer = new LMLayer(helpers.defaultCapabilities);
// We're testing many as asynchronous messages in a row.
// this would be cleaner using async/await syntax, but
// alas some of our browsers don't support it.
return lmLayer.initialize(
capabilities,
{
type: 'dummy',
futureSuggestions: iGotDistractedByHazel()
}
return lmLayer.loadModel(
// We need to provide an absolute path since the worker is based within a blob.
document.location.protocol + '//' + document.location.host + "/resources/models/simple-dummy.js"
).then(function (actualConfiguration) {
return Promise.resolve();
}).then(function () {
@ -41,6 +35,7 @@ describe('LMLayer using dummy model', function () {
return lmLayer.predict(zeroTransform(), emptyContext());
}).then(function (suggestions) {
assert.deepEqual(suggestions, iGotDistractedByHazel()[3]);
lmLayer.shutdown();
return Promise.resolve();
});
});

View file

@ -8,20 +8,14 @@ describe('LMLayer using the word list model', function () {
describe('Prediction', function () {
var EXPECTED_SUGGESTIONS = 3;
it('will predict an empty buffer', function () {
var lmLayer = new LMLayer();
var capabilities = {
maxLeftContextCodeUnits: 32 + ~~Math.random() * 32
};
var lmLayer = new LMLayer(helpers.defaultCapabilities);
// We're testing many as asynchronous messages in a row.
// this would be cleaner using async/await syntax, but
// alas some of our browsers don't support it.
return lmLayer.initialize(
capabilities,
{
type: 'wordlist',
wordlist: __json__['wordlists/english-1000']
}
return lmLayer.loadModel(
// We need to provide an absolute path since the worker is based within a blob.
document.location.protocol + '//' + document.location.host + "/resources/models/simple-wordlist.js"
).then(function (_actualConfiguration) {
return Promise.resolve();
}).then(function () {
@ -38,6 +32,7 @@ describe('LMLayer using the word list model', function () {
return lmLayer.predict(type('q'), atEndOfBuffer('the '));
}).then(function (suggestions) {
assert.isAtLeast(suggestions.length, EXPECTED_SUGGESTIONS);
lmLayer.shutdown();
return Promise.resolve();
});
});

View file

@ -2,6 +2,7 @@ var assert = chai.assert;
var LMLayer = com.keyman.text.prediction.LMLayer;
describe('LMLayerWorker', function () {
this.timeout(5000);
describe('LMLayerWorkerCode', function() {
it('should exist!', function() {
assert.isFunction(LMLayerWorkerCode,
@ -16,11 +17,17 @@ describe('LMLayerWorker', function () {
let worker = new Worker(uri);
worker.onmessage = function thisShouldBeCalled(message) {
done();
worker.terminate();
};
// While the config message doesn't trigger a reply message, we have to send it a configuration message first.
worker.postMessage({
message: 'initialize',
model: { type: 'dummy' },
capabilities: { maxLeftContextCodeUnits: 64 }
message: 'config',
capabilities: helpers.defaultCapabilities
})
worker.postMessage({
message: 'load',
// Since the worker's based in a blob, it's not on the 'same domain'. We need to absolute-path the model file.
model: document.location.protocol + '//' + document.location.host + "/resources/models/simple-dummy.js"
});
});
});

View file

@ -0,0 +1,8 @@
var helpers;
// Establishes the equivalent of a TS namespace.
(function(helpers){
helpers.defaultCapabilities = {
maxLeftContextCodeUnits: 64
};
})(helpers || (helpers = {}));

View file

@ -0,0 +1,111 @@
/**
* While handwritten, this class is designed to mirror the potential results of TypeScript compilation of
* model TS source files.
*/
(function(){
var Model = /** @class */ (function() {
function Model() { // implements Model
}
// A direct import/copy from i_got_distracted_by_hazel.json.
Model.futureSuggestions = [
[
{
"transform": {
"insert": "I ",
"deleteLeft": 0
},
"displayAs": "I"
},
{
"transform": {
"insert": "I'm ",
"deleteLeft": 0
},
"displayAs": "I'm"
},
{
"transform": {
"insert": "Oh ",
"deleteLeft": 0
},
"displayAs": "Oh "
}
],
[
{
"transform": {
"insert": "love ",
"deleteLeft": 0
},
"displayAs": "love"
},
{
"transform": {
"insert": "am ",
"deleteLeft": 0
},
"displayAs": "am"
},
{
"transform": {
"insert": "got ",
"deleteLeft": 0
},
"displayAs": "got"
}
],
[
{
"transform": {
"insert": "distracted ",
"deleteLeft": 0
},
"displayAs": "distracted by"
},
{
"transform": {
"insert": "distracted ",
"deleteLeft": 0
},
"displayAs": "distracted"
},
{
"transform": {
"insert": "a ",
"deleteLeft": 0
},
"displayAs": "a"
}
],
[
{
"transform": {
"insert": "Hazel ",
"deleteLeft": 0
},
"displayAs": "Hazel"
},
{
"transform": {
"insert": "the ",
"deleteLeft": 0
},
"displayAs": "the"
},
{
"transform": {
"insert": "a ",
"deleteLeft": 0
},
"displayAs": "a"
}
]
];
return Model;
}());
// It's a 'dummy' model, so there's no need for extra methods and such within the Model's class definition.
LMLayerWorker.loadModel(new models.DummyModel({futureSuggestions: Model.futureSuggestions}));
})();

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
class DefaultWordBreaker implements WorkerInternalWordBreaker {
// TODO: Specify the data needed to implement a 'default' word breaker.
constructor(obj) {
}
break(text: string): string[] {
throw "Not yet implemented.";
}
}

View file

@ -30,40 +30,33 @@
*/
/// <reference path="../message.d.ts" />
/// <reference path="models/dummy-model.ts" />
/// <reference path="models/wordlist-model.ts" />
/**
* Encapsulates all the state required for the LMLayer's worker thread.
*
* Implements the state pattern. There are two states:
* Implements the state pattern. There are three states:
*
* - `uninitialized` (initial state)
* - `ready` (accepting state)
* - `unconfigured` (initial state before configuration)
* - `modelless` (state without model loaded)
* - `ready` (state with model loaded, accepts prediction requests)
*
* Transitions are initiated by valid messages. Invalid
* messages are errors, and do not lead to transitions.
*
* +-----------------+ +---------+
* | | initialize | |
* +------> uninitialized +----------->+ ready +---+
* | | | | |
* +-----------------+ +----^----+ | predict
* | |
* +--------+
* +-------------+ load +---------+
* config | |----------->| |
* +-------> modelless + + ready +---+
* | |<-----------| | |
* +-------------+ unload +----^----+ | predict
* | |
* +--------+
*
* The model and the configuration are ONLY relevant in the `ready` state;
* as such, they are NOT direct properties of the LMLayerWorker.
*/
class LMLayerWorker {
/**
* All of the bundled model implementations will add themselves here:
* Note: the models will add themselves by including this
* file using a triple-slash directive:
* /// <reference path="path/to/this/file.ts" />
* then adding the constructor to this static object:
* LMLayerWorker.models.MyModelImplementation = class {}
*/
static models: {[key: string]: WorkerInternalModelConstructor} = {};
/**
* State pattern. This object handles onMessage().
* handleMessage() can transition to a different state, if
@ -77,11 +70,26 @@ class LMLayerWorker {
*/
private _postMessage: PostMessage;
/**
* By default, it's self.importScripts(), but can be overridden
* so that this can be tested **outside of a Worker**.
*
* To function properly, self.importScripts() must be bound to self
* before being stored here, else it will fail.
*/
private _importScripts: ImportScripts;
private _platformCapabilities: Capabilities;
private _hostURL: string;
constructor(options = {
postMessage: null,
importScripts: null,
postMessage: null
}) {
this._postMessage = options.postMessage || postMessage;
this.setupInitialState();
this._importScripts = options.importScripts || importScripts;
this.setupConfigState();
}
/**
@ -134,58 +142,79 @@ class LMLayerWorker {
* @param desc Type of the model to instantiate and its parameters.
* @param capabilities Capabilities on offer from the keyboard.
*/
private loadModel(desc: ModelDescription, capabilities: Capabilities) {
let model: WorkerInternalModel;
let configuration: Configuration = {
leftContextCodeUnits: 0,
rightContextCodeUnits: 0
};
if (desc.type === 'dummy') {
model = new LMLayerWorker.models.DummyModel(capabilities, {
futureSuggestions: desc.futureSuggestions
});
} else if (desc.type === 'wordlist') {
model = new LMLayerWorker.models.WordListModel(capabilities, desc.wordlist);
} else {
throw new Error('Invalid model');
}
// TODO: when model is object with kind 'wordlist' or 'fst'
public loadModel(model: WorkerInternalModel) {
// TODO: pass _platformConfig to model so that it can self-configure to the platform,
// returning a Configuration.
let configuration = model.configure(this._platformCapabilities);
// Set reasonable defaults for the configuration.
if (!configuration.leftContextCodeUnits) {
configuration.leftContextCodeUnits = capabilities.maxLeftContextCodeUnits;
configuration.leftContextCodeUnits = this._platformCapabilities.maxLeftContextCodeUnits;
}
if (!configuration.rightContextCodeUnits) {
configuration.rightContextCodeUnits = capabilities.maxRightContextCodeUnits || 0;
configuration.rightContextCodeUnits = this._platformCapabilities.maxRightContextCodeUnits || 0;
}
return {model, configuration};
this.transitionToReadyState(model);
this.cast('ready', { configuration });
}
private loadModelFile(url: string) {
// The self/global WebWorker method, allowing us to directly import another script file into WebWorker scope.
// If built correctly, the model's script file will auto-register the model with loadModel() above.
this._importScripts(url);
}
public unloadModel() {
// Right now, this seems sufficient to clear out the old model.
// The only existing reference to a loaded model is held by
// transitionToReadyState's `handleMessage` closure. (The `model` var)
this.transitionToLoadingState();
}
/**
* Sets the initial state, i.e., `uninitialized`.
* This state only handles `initialized` messages, and will
* Sets the initial state, i.e., `unconfigured`.
* This state only handles `config` messages, and will
* transition to the `modelless` state once it receives
* the config data from the host platform.
*/
private setupConfigState() {
this.state = {
name: 'unconfigured',
handleMessage: (payload) => {
// ... that message must have been 'config'!
if (payload.message !== 'config') {
throw new Error(`invalid message; expected 'config' but got ${payload.message}`);
}
this._platformCapabilities = payload.capabilities;
this.transitionToLoadingState();
}
}
}
public loadWordBreaker(breaker: WorkerInternalWordBreaker) {
// TODO: Actually store it somewhere for future use. Make sure we can forget it with `unloadModel` as well.
}
/**
* Sets the model-loading state, i.e., `modelless`.
* This state only handles `load` messages, and will
* transition to the `ready` state once it receives a model
* description and capabilities.
*/
private setupInitialState() {
private transitionToLoadingState() {
this.state = {
name: 'uninitialized',
name: 'modelless',
handleMessage: (payload) => {
// ...that message must have been 'initialize'!
if (payload.message !== 'initialize') {
throw new Error(`invalid message; expected 'initialize' but got ${payload.message}`);
// ...that message must have been 'load'!
if (payload.message !== 'load') {
throw new Error(`invalid message; expected 'load' but got ${payload.message}`);
}
// TODO: validate configuration?
let {model, configuration} = this.loadModel(
// TODO: validate configuration, and provide valid configuration in tests.
payload.model, payload.capabilities
);
this.transitionToReadyState(model);
this.cast('ready', { configuration });
this.loadModelFile(payload.model);
}
};
}
@ -195,21 +224,26 @@ class LMLayerWorker {
* fully-instantiated model. The `ready` state only responds
* to `predict` message, and is an accepting state.
*
* @param model The initialized language model.
* @param model The loaded language model.
*/
private transitionToReadyState(model: WorkerInternalModel) {
this.state = {
name: 'ready',
handleMessage: (payload) => {
if (payload.message !== 'predict') {
throw new Error(`invalid message; expected 'predict' but got ${payload.message}`);
switch(payload.message) {
case 'predict':
let {transform, context} = payload;
this.cast('suggestions', {
token: payload.token,
suggestions: model.predict(transform, context)
});
break;
case 'unload':
this.unloadModel();
break;
default:
throw new Error(`invalid message; expected one of {'predict', 'unload'} but got ${payload.message}`);
}
let {transform, context} = payload;
this.cast('suggestions', {
token: payload.token,
suggestions: model.predict(transform, context)
});
}
};
}
@ -233,9 +267,14 @@ class LMLayerWorker {
* @param scope A global scope to install upon.
*/
static install(scope: DedicatedWorkerGlobalScope): LMLayerWorker {
let worker = new LMLayerWorker({ postMessage: scope.postMessage });
let worker = new LMLayerWorker({ postMessage: scope.postMessage, importScripts: scope.importScripts.bind(scope) });
scope.onmessage = worker.onMessage.bind(worker);
// Ensures that the worker instance is accessible for loaded model scripts.
// Assists unit-testing.
scope['LMLayerWorker'] = worker;
scope['models'] = models;
return worker;
}
}
@ -243,6 +282,7 @@ class LMLayerWorker {
// Let LMLayerWorker be available both in the browser and in Node.
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = LMLayerWorker;
module.exports['models'] = models;
} else if (typeof self !== 'undefined' && 'postMessage' in self) {
// Automatically install if we're in a Web Worker.
LMLayerWorker.install(self as DedicatedWorkerGlobalScope);

View file

@ -20,35 +20,43 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/// <reference path="./index.ts" />
namespace models {
/**
* @file dummy-model.ts
*
* Defines the Dummy model, which is used for testing the
* prediction API exclusively.
*/
/**
* @file dummy-model.ts
*
* Defines the Dummy model, which is used for testing the
* prediction API exclusively.
*/
/**
* The Dummy Model that returns nonsensical, but predictable results.
*/
export class DummyModel implements WorkerInternalModel {
configuration: Configuration;
private _futureSuggestions: Suggestion[][];
/**
* The Dummy Model that returns nonsensical, but predictable results.
*/
LMLayerWorker.models.DummyModel = class DummyModel implements WorkerInternalModel {
configuration: Configuration;
private _futureSuggestions: Suggestion[][];
constructor(capabilities: Capabilities, options?: any) {
options = options || {};
this.configuration = options.configuration || {};
// Create a shallow copy of the suggestions;
// this class mutates the array.
this._futureSuggestions = options.futureSuggestions
? options.futureSuggestions.slice() : [];
}
predict(transform: Transform, context: Context, injectedSuggestions?: Suggestion[]): Suggestion[] {
if (injectedSuggestions) {
return injectedSuggestions;
constructor(options?: any) {
options = options || {};
// Create a shallow copy of the suggestions;
// this class mutates the array.
this._futureSuggestions = options.futureSuggestions
? options.futureSuggestions.slice() : [];
}
return this._futureSuggestions.shift();
}
};
configure(capabilities: Capabilities): Configuration {
this.configuration = {
leftContextCodeUnits: capabilities.maxLeftContextCodeUnits,
rightContextCodeUnits: capabilities.maxRightContextCodeUnits
};
return this.configuration;
}
predict(transform: Transform, context: Context, injectedSuggestions?: Suggestion[]): Suggestion[] {
if (injectedSuggestions) {
return injectedSuggestions;
}
return this._futureSuggestions.shift();
}
};
}

View file

@ -20,34 +20,41 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/// <reference path="./index.ts" />
/**
* @file wordlist-model.ts
*
* Defines a simple word list (unigram) model.
*/
/**
* @class WordListModel
*
* Defines the word list model, or the unigram model.
* Unigram models throw away all preceding words, and search
* for the next word exclusively. As such, they can perform simple
* prefix searches within words, however they are not very good
* at predicting the next word.
*/
LMLayerWorker.models.WordListModel = (function () {
namespace models {
/**
* @class WordListModel
*
* Defines the word list model, or the unigram model.
* Unigram models throw away all preceding words, and search
* for the next word exclusively. As such, they can perform simple
* prefix searches within words, however they are not very good
* at predicting the next word.
*/
/** Upper bound on the amount of suggestions to generate. */
const MAX_SUGGESTIONS = 3;
return class WordListModel implements WorkerInternalModel {
export class WordListModel implements WorkerInternalModel {
configuration: Configuration;
private _wordlist: string[];
constructor(_capabilities: Capabilities, wordlist: string[]) {
constructor(wordlist: string[]) {
this._wordlist = wordlist;
}
configure(capabilities: Capabilities): Configuration {
return this.configuration = {
leftContextCodeUnits: capabilities.maxLeftContextCodeUnits,
rightContextCodeUnits: capabilities.maxRightContextCodeUnits
};
}
predict(transform: Transform, context: Context): Suggestion[] {
// EVERYTHING to the left of the cursor:
let fullLeftContext = context.left || '';
@ -95,4 +102,4 @@ LMLayerWorker.models.WordListModel = (function () {
return suggestions;
}
};
}());
}

View file

@ -1,6 +1,7 @@
{
"compilerOptions": {
"allowJs": false,
"declaration": true,
"module": "none",
"outFile": "../build/intermediate/index.js",
"inlineSources": true,

View file

@ -32,29 +32,39 @@
* The signature of self.postMessage(), so that unit tests can mock it.
*/
type PostMessage = typeof DedicatedWorkerGlobalScope.prototype.postMessage;
type ImportScripts = typeof DedicatedWorkerGlobalScope.prototype.importScripts;
/**
* The valid incoming message kinds.
*/
type IncomingMessageKind = 'initialize' | 'predict';
type IncomingMessage = InitializeMessage | PredictMessage;
type IncomingMessageKind = 'config' | 'load' | 'predict' | 'unload';
type IncomingMessage = ConfigMessage | LoadMessage | PredictMessage | UnloadMessage;
/**
* The structure of a config message. It should include the platform's supported
* capabilities.
*/
interface ConfigMessage {
message: 'config';
/**
* The platform's supported capabilities.
*/
capabilities: Capabilities;
}
/**
* The structure of an initialization message. It should include the model (either in
* source code or parameter form), as well as the keyboard's capabilities.
*/
interface InitializeMessage {
message: 'initialize';
interface LoadMessage {
message: 'load';
/**
* The model type, and all of its parameters.
* The model's compiled JS file.
*/
model: ModelDescription;
/**
* The configuration that the keyboard can offer to the model.
*/
capabilities: Capabilities;
model: string;
}
/**
@ -86,6 +96,9 @@ interface PredictMessage {
context: Context;
}
interface UnloadMessage {
message: 'unload'
}
/**
@ -96,7 +109,7 @@ interface LMLayerWorkerState {
* Informative property. Name of the state. Currently, the LMLayerWorker can only
* be the following states:
*/
name: 'uninitialized' | 'ready';
name: 'unconfigured' | 'modelless' | 'ready';
handleMessage(payload: IncomingMessage): void;
}
@ -104,6 +117,7 @@ interface LMLayerWorkerState {
* The model implementation, within the Worker.
*/
interface WorkerInternalModel {
configure(capabilities: Capabilities): Configuration;
predict(transform: Transform, context: Context): Suggestion[];
}
@ -115,5 +129,9 @@ interface WorkerInternalModelConstructor {
* WorkerInternalModel instances are all given the keyboard's
* capabilities, plus any parameters they require.
*/
new(capabilities: Capabilities, ...modelParameters: any[]): WorkerInternalModel;
new(...modelParameters: any[]): WorkerInternalModel;
}
interface WorkerInternalWordBreaker {
break(text: string): string[]; //
}

View file

@ -7,6 +7,7 @@
* Attachment to all supported element types has now been abstracted, splitting keystroke processing from related DOM management.
* Centralizes code paths to improve parity between hardware and OSK-based keystrokes.
* In both cases above, significant unit testing was facilitated and has been added as a result, further improving code maintainability into the future.
* Began adding support for our common LMLayer interface for predictive modeling.
## 2019-02-25 11.0.220 stable
* 11.0 Stable release

View file

@ -1 +1 @@
///<reference path="../../../common/predictive-text/message.d.ts" />
///<reference path="../../../common/predictive-text/build/includes/message.d.ts" />

View file

@ -5,3 +5,6 @@
// Defines the main interface of the Language Modeling Layer (LMLayer) and its original typing information.
///<reference path="../../../common/predictive-text/embedded_worker.d.ts" />
///<reference path="../../../common/predictive-text/index.ts" />
// We DO need the embedded_worker.d.ts file - since we're directly linking into the LMLayer's code,
// we need the typedef for the embedded worker to be linked.

View file

@ -159,6 +159,7 @@ namespace com.keyman {
this.osk.shutdown();
this.util.shutdown();
this.keyboardManager.shutdown();
this.modelManager.shutdown();
if(this.ui && this.ui.shutdown) {
this.ui.shutdown();

View file

@ -36,28 +36,29 @@ namespace com.keyman.text.prediction {
init() {
let keyman = com.keyman.singleton;
this.lmEngine = new LMLayer();
// 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 (and thus, language) change events.
keyman['addEventListener']('keyboardchange', this.onKeyboardChange.bind(this));
}
private deactivateModel() {
// TODO: Call a LMLayer method for model deactivation.
private unloadModel() {
this.lmEngine.unloadModel();
this.currentModel = null;
}
private activateModel(model: ModelSpec) {
private loadModel(model: ModelSpec) {
if(!model) {
throw new Error("Null reference not allowed.");
}
// TODO: Activate this model within the LMLayer!
let file = model.path;
//this.lmEngine.initialize(file) // Currently unsupported.
console.log("Model detected!");
this.lmEngine.loadModel(file);
this.currentModel = model;
}
@ -67,10 +68,10 @@ namespace com.keyman.text.prediction {
let model = this.languageModelMap[lgCode];
if(this.currentModel !== model) {
this.deactivateModel();
this.unloadModel();
if(model) {
this.activateModel(model);
this.loadModel(model);
}
}
}
@ -102,5 +103,11 @@ namespace com.keyman.text.prediction {
isRegistered(model: ModelSpec): boolean {
return !! this.registeredModels[model.id];
}
// TODO: actually calling this.lmEngine.predict. Will need its own method(s).
public shutdown() {
this.lmEngine.shutdown();
}
}
}