Merge branch 'lmlayer-init' into lmlayer-build-reorg

This commit is contained in:
Joshua A. Horton 2018-11-29 23:35:37 +07:00
commit cf13caa98e
11 changed files with 521 additions and 488 deletions

View file

@ -1,408 +1,61 @@
Keyman/Language Modelling layer
================================
Language Modelling Layer (LMLayer)
==================================
This document introduces the protocol for communicating between
KeymanWeb and the language modeling layer (i.e., the configurable
prediction and suggestion engine, (henceforth referred to as the
"LMLayer").
Provide predictions and corrections while you type!
Note: I'm using the term **keyboard** as a synonym for **KeymanWeb**.
See [Worker Communication Protocol](./docs/worker-communication-protocol.md) for a
semi-formal specification on how the Worker and the main thread communicate.
> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
> NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
> "OPTIONAL" in this document are to be interpreted as described in
> [RFC 2119].
System dependencies
-------------------
[RFC 2119]: https://www.ietf.org/rfc/rfc2119.txt
You will need Bash and Node.js >= 6.0.
Build
-----
Communication protocol between keyboard and asynchronous worker
---------------------------------------------------------------
Run `build.sh`. This will also automatically install dependencies with `npm`.
![Sequence diagram of obtaining a prediction](./docs/predictive-text-sequence.png)
We have decided that everything to the right of the `KeymanWeb` will be
in a Web Worker. However, communication can happen only through
[`postMessage(data)`][postMessage] commands,
where `data` is a serializable object (via the [structured clone][]
algorithm).
What serializable object can we send that will adhere to the [open-closed principle]?
### Messages
The idea is to use a [discriminated union][]. The protocol involves
plain JavaScript objects with one property called `message` that takes
a finite set of `string` values.
These string values indicate what message should be sent. The rest of
the properties in the object are the parameters send with the message.
```javascript
{
message: 'predict',
// message-specific properties here
}
```sh
./build.sh
```
See also: [XML-RPC][]
### Two-stage compilation process
Messages are **not** methods. That is, there is no assumption that
a client will receive a reply when a message is sent. However, some
pairs of messages, such as `predict` and `suggestions`, lightly assume
request/response semantics.
Since the primary LMLayer code runs within a [Web Worker][], `build.sh` compiles the
LMLayer in two stages:
[discriminated union]: http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
[open-closed principle]: https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle
[XML-RPC]: https://en.wikipedia.org/wiki/XML-RPC
[structured clone]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[postMessage]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
1. Compile the inner worker code
1. Compile the TypeScript sources for _only_ the Worker code.
2. Wrap the Worker code as `embedded_worker.js`
2. Compile the top-level code
1. Include `embedded_worker.js` verbatim using a TypeScript directive.
2. Compile the top-level TypeScript code.
3. Unwrap the Worker code at runtime.
### Tokens
[Web Worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
Tokens uniquely identify an input event, such as a key press. Since
Keyman should ask for an asynchronous prediction on most key presses, the
token is intended to associate a prediction and its response with
a particular input; Keyman is free to ignore prediction responses if
they are for outdated input events.
Test
----
The `Token` type is opaque to LMLayer. That is, LMLayer does not inspect
its contents; it simply uses it to identify a request and pass it back
to Keyman. There are a few requirements on the concrete type of the
`Token`:
This will run both headless unit tests, and in-browser unit tests and integration
tests:
1. Tokens **MUST** be serializable via the [structured clone][] algorithm;
2. Tokens **MUST** be usable as a key in a [`Map`][Map object] object.
3. Tokens **MUST** be unique across messages. That is, tokens **MUST
NOT** be duplicated between different messages.
It is up to the keyboard to create unambiguous tokens that can be uniquely
identified through the round-trip process.
In the following examples, I'll use the subset of `number` values that
are interpretable as [31-bit signed integers][Smi].
[Map object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality
[Smi]: https://github.com/thlorenz/v8-perf/blob/master/data-types.md#efficiently-representing-values-and-tagging
[Tokens]: #tokens
### Example
An asynchronous message to predict after typing 'D':
```javascript
{
message: 'predict',
token: 1,
transform: {
insert: 'D',
deleteLeft: 0,
deleteRight: 0
},
context: {
left: '',
right: '',
startOfBuffer: true,
endOfBuffer: true
},
}
```sh
./build.sh -test
```
Message types
-------------
### Test-Driven Development
Currently there are four message types:
I like to use [entr]() to automatically build and re-run the unit tests anytime I
change a source code file. Here's the command I run in separate window:
Message | Direction | Parameters | Expected reply | Uses token
--------------|--------------------|---------------------|---------------------|---------------
`initialize` | keyboard → LMLayer | initialization | Yes — `ready` | No
`ready` | LMLayer → keyboard | configuration | No | No
`predict` | keyboard → LMLayer | transform, context | Yes — `suggestions` | Yes
`suggestions` | LMLayer → keyboard | suggestions | No | Yes
### Message: `initialize`
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.
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.
These are the configurations, and platform restrictions sent to
initialize the LMLayer and its model.
```typescript
interface InitializeMessage {
message: 'initialize';
/**
* Path to the model. There are no concrete restrictions on the path
* to the model, so long as the LMLayer can successfully use it to
* initialize the model.
*/
model: string;
configuration: {
/**
* Whether the platform supports right contexts.
* The absence of this rule implies false.
*/
supportsRightContexts?: false,
/**
* 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.
*/
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 0.
* See also, [[supportsRightContexts]].
*/
maxRightContextCodeUnits?: number,
}
}
```sh
git ls-files | entr -c ./build.sh -tdd
```
Importantly, `./build.sh -tdd` skips running the in-browser tests, and skips
downloading/updating `npm` dependencies.
### 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
a plain JavaScript object requesting configuration from the keyboard.
There are only two options defined so far:
```typescript
interface ReadyMessage {
message: 'ready';
configuration: {
/**
* How many UTF-16 code units maximum to send as the context to the
* left of the cursor ("left" in the Unicode character stream).
*
* Affects the `context` property sent in `predict` messages.
*
* While the left context MUST NOT bisect surrogate pairs, they MAY
* bisect graphical clusters.
*/
leftContextCodeUnits: number,
/**
* How many UTF-16 code units maximum to send as the context to the
* right of the cursor ("right" in the Unicode character stream).
*
* Affects the `context` property sent in `predict` messages.
*
* While the left context MUST NOT bisect surrogate pairs, they MAY
* bisect graphical clusters.
*/
rightContextCodeUnits: number,
};
}
```
### Message: `predict`
Sent from the keyboard to the LMLayer whenever a new prediction should
be generated. This is typically initiated by a key press event. The
keyboard **SHOULD** track each `predict` message using a [token][Tokens]. The
token **MUST** be unique across all prediction events. The LMLayer
**SHOULD** respond to each `predict` message with a `suggestions`
message. The `suggestions` message **MUST** contain the corresponding
token as sent in the initial `predict` message.
The keyboard **MUST** send the `context` parameter. The keyboard
**SHOULD** send the `transform` parameter. The keyboard **MUST** send
a unique token.
The semantics of the `predict` message **MUST** be from the
perspective of this sequence of events:
1. After the input event is received by the keyboard.
2. Before the keyboard applies the associated `transform` to the buffer.
**NOTE**: The keyboard **MAY** apply the `transform` associated with the
input event before receiving the corresponding `suggestions` message
from the LMLayer. The intention is that once the suggestions are displayed,
the typist may select one of the suggestions in the place of the effects
of their original input.
**NOTE**: The keyboard **MAY** send the `predict` message after it has
applied the `transform` associated with the input event to the buffer.
Regardless of the actual sequence, the semantics **MUST** remain the
same:the prediction happens from the perspective before the `transform`
has been applied. Therefore, if the keyboard eagerly transforms the
buffer before it has sent `predict` message, it must anticipate _undoing_
the `transform` it has already applied if it is to apply a `transfrom`
send in the `suggestions` message. The keyboard must act as if it has
never applied the `transform` associated with the input event in the
first place.
The context is the text surrounding the insertion point, _before_ the
transform is applied to the buffer.
```typescript
interface Context {
/**
* Up to maxLeftContextCodeUnits code units of Unicode scalar value
* (i. e., characters) to the left of the insertion point in the
* buffer. If there is nothing to the left of the buffer, this returns
* an empty string.
*/
left: USVString;
/**
* Up to maxRightContextCodeUnits code units of Unicode scalar value
* (i. e., characters) to the right of the insertion point in the
* buffer. If there is nothing to the right of the buffer, this returns
* an empty string.
*/
right?: USVString;
/**
* Whether the insertion point is at the start of the buffer.
*/
startOfBuffer: boolean;
/**
* Whether the insertion point is at the end of the buffer.
*/
endOfBuffer: boolean;
}
```
The transform parameter describes how the input event will change the
buffer.
```typescript
interface Transform {
/**
* The Unicode scalar values (i.e., characters) to be inserted at the
* cursor position.
*
* Corresponds to `s` in com.keyman.KeyboardInterface.output.
*/
insert: USVString;
/**
* The number of code units to delete to the left of the cursor.
*
* Corresponds to `dn` in com.keyman.KeyboardInterface.output.
*/
delete: number;
/**
* The number of code units to delete to the right of the cursor.
* Not available on all platforms.
*/
deleteRight?: number;
}
```
### Message: `suggestions`
The `suggestions` message is sent from the LMLayer to the keyboard. This
message sends a ranked array of suggestions, in descending order of
probability (i.e., entry `0` is most likely, followed by entry `1`,
etc.). This message **MUST** be in response to a `predict` message, and
it **MUST** respond with the corresponding [token].
```typescript
/**
* `suggestions` is an ordered array of suggestion objects.
* Each suggestion is a transform bundled with a `displayAs` property.
*/
let suggestions = Suggestion[];
interface Suggestion {
/**
* Same object as an input event transform.
* Note that the transform is applied AFTER the input event
* transform.
*/
transform: Transform;
/**
* A string to display the suggestion to the typist.
* This should aid the typist understand what the transform
* will do to their text.
*/
displayAs: string;
}
```
```javascript
let suggestions = [
{
transform: {
insert: 'teapot',
deleteLeft: 1,
deleteRight: 0
},
displayAs: '🍵'
}
];
```
#### Timing
Each suggestion provides a `transform`. This transform is applied
_after_ the transform associated with the input event that initiated
this prediction. That is, the suggested transform applies to the buffer
after the transform associated with the input event.
Rephrased in somewhat mathematical terms:
Let 𝑥𝐵 be the input text buffer. Let 𝑇<sub>𝑖</sub> be the transform
associated with an input event that maps a text buffer 𝐵𝐵. Let
𝑇<sub>𝑠</sub> be a transform suggested through the LMLayer. 𝑦𝐵 is the
text buffer after the suggestion transform has been applied. The
correct sequence of applications should be as follows:
> 𝑦 = 𝑇<sub>𝑠</sub>(𝑇<sub>𝑖</sub>(𝑥))
#### Late suggestions
Sometimes, a `suggestions` message may arrive after an input event has
already invalidated its request. This is called a **"late" suggestion**.
The LMLayer **MAY** send late suggestions. Consequently, the keyboard
**MAY** discard the late suggestions. There is no requirement for the
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.
TODO
====
- [x] Document `suggestions`
- [x] TypeScript!
- [ ] make simple `index.html` that demos a dummy model
- [x] Update class definitions in README from TypeScript sources.
- [ ] Do word segmentation
- [ ] Determine the exact arguments given to the model's `predict()`
method.
- [ ] Make an `error` initialization message.
- [ ] Make a `cancel` message.
- [ ] LOGLIKEIHOOD IN THE TRANSFORM!
- [ ] `possibleTransforms` where `transform` is an alias for `possibleTransforms[0]`
- [ ] Use [puppeteer](https://github.com/GoogleChrome/puppeteer)?
[entr]: http://eradman.com/entrproject/

View file

@ -4,11 +4,15 @@
# Designed for optimal compatibility with the Keyman Suite.
#
# Include some helper functions from resources
. ../../resources/shellHelperFunctions.sh
LMLAYER_OUTPUT=build
WORKER_OUTPUT=build/intermediate
NAKED_WORKER=$WORKER_OUTPUT/index.js
EMBEDDED_WORKER=$WORKER_OUTPUT/embedded_worker.js
# Build the worker and the main script.
build ( ) {
# Build worker first; the main file depends on it.
@ -53,21 +57,6 @@ display_usage ( ) {
echo " -test runs unit and integration tests after building"
}
# Prints a nice, common error message.
fail ( ) {
# TODO: source shellHelperFunctions.sh
local ERROR_RED
local NORMAL
ERROR_RED="$(tput setaf 1)"
NORMAL="$(tput sgr0)"
FAILURE_MSG="$1"
if [[ "$FAILURE_MSG" == "" ]]; then
FAILURE_MSG="Unknown failure."
fi
echo "$0: ${ERROR_RED}$FAILURE_MSG${NORMAL}"
exit 1
}
# Creates embedded_worker.js. Must be run after the worker is built for the
# first time
wrap-worker ( ) {

View file

@ -0,0 +1,391 @@
Keyman/Language Modelling layer
================================
This document introduces the protocol for communicating between
KeymanWeb and the language modeling layer (i.e., the configurable
prediction and suggestion engine, (henceforth referred to as the
"LMLayer").
Note: I'm using the term **keyboard** as a synonym for **KeymanWeb**.
> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
> NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
> "OPTIONAL" in this document are to be interpreted as described in
> [RFC 2119].
[RFC 2119]: https://www.ietf.org/rfc/rfc2119.txt
Communication protocol between keyboard and asynchronous worker
---------------------------------------------------------------
![Sequence diagram of obtaining a prediction](./predictive-text-sequence.png)
We have decided that everything to the right of the `KeymanWeb` will be
in a Web Worker. However, communication can happen only through
[`postMessage(data)`][postMessage] commands,
where `data` is a serializable object (via the [structured clone][]
algorithm).
What serializable object can we send that will adhere to the [open-closed principle]?
### Messages
The idea is to use a [discriminated union][]. The protocol involves
plain JavaScript objects with one property called `message` that takes
a finite set of `string` values.
These string values indicate what message should be sent. The rest of
the properties in the object are the parameters send with the message.
```javascript
{
message: 'predict',
// message-specific properties here
}
```
See also: [XML-RPC][]
Messages are **not** methods. That is, there is no assumption that
a client will receive a reply when a message is sent. However, some
pairs of messages, such as `predict` and `suggestions`, lightly assume
request/response semantics.
[discriminated union]: http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
[open-closed principle]: https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle
[XML-RPC]: https://en.wikipedia.org/wiki/XML-RPC
[structured clone]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[postMessage]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
### Tokens
Tokens uniquely identify an input event, such as a key press. Since
Keyman should ask for an asynchronous prediction on most key presses, the
token is intended to associate a prediction and its response with
a particular input; Keyman is free to ignore prediction responses if
they are for outdated input events.
The `Token` type is opaque to LMLayer. That is, LMLayer does not inspect
its contents; it simply uses it to identify a request and pass it back
to Keyman. There are a few requirements on the concrete type of the
`Token`:
1. Tokens **MUST** be serializable via the [structured clone][] algorithm;
2. Tokens **MUST** be usable as a key in a [`Map`][Map object] object.
3. Tokens **MUST** be unique across messages. That is, tokens **MUST
NOT** be duplicated between different messages.
It is up to the keyboard to create unambiguous tokens that can be uniquely
identified through the round-trip process.
In the following examples, I'll use the subset of `number` values that
are interpretable as [31-bit signed integers][Smi].
[Map object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality
[Smi]: https://github.com/thlorenz/v8-perf/blob/master/data-types.md#efficiently-representing-values-and-tagging
[Tokens]: #tokens
### Example
An asynchronous message to predict after typing 'D':
```javascript
{
message: 'predict',
token: 1,
transform: {
insert: 'D',
deleteLeft: 0,
deleteRight: 0
},
context: {
left: '',
right: '',
startOfBuffer: true,
endOfBuffer: true
},
}
```
Message types
-------------
Currently there are four message types:
Message | Direction | Parameters | Expected reply | Uses token
--------------|--------------------|---------------------|---------------------|---------------
`initialize` | keyboard → LMLayer | initialization | Yes — `ready` | No
`ready` | LMLayer → keyboard | configuration | No | No
`predict` | keyboard → LMLayer | transform, context | Yes — `suggestions` | Yes
`suggestions` | LMLayer → keyboard | suggestions | No | Yes
### Message: `initialize`
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.
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.
These are the configurations, and platform restrictions sent to
initialize the LMLayer and its model.
```typescript
interface InitializeMessage {
message: 'initialize';
/**
* Path to the model. There are no concrete restrictions on the path
* to the model, so long as the LMLayer can successfully use it to
* initialize the model.
*/
model: string;
configuration: {
/**
* Whether the platform supports right contexts.
* The absence of this rule implies false.
*/
supportsRightContexts?: false,
/**
* 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.
*/
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 0.
* See also, [[supportsRightContexts]].
*/
maxRightContextCodeUnits?: number,
}
}
```
### 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
a plain JavaScript object requesting configuration from the keyboard.
There are only two options defined so far:
```typescript
interface ReadyMessage {
message: 'ready';
configuration: {
/**
* How many UTF-16 code units maximum to send as the context to the
* left of the cursor ("left" in the Unicode character stream).
*
* Affects the `context` property sent in `predict` messages.
*
* While the left context MUST NOT bisect surrogate pairs, they MAY
* bisect graphical clusters.
*/
leftContextCodeUnits: number,
/**
* How many UTF-16 code units maximum to send as the context to the
* right of the cursor ("right" in the Unicode character stream).
*
* Affects the `context` property sent in `predict` messages.
*
* While the left context MUST NOT bisect surrogate pairs, they MAY
* bisect graphical clusters.
*/
rightContextCodeUnits: number,
};
}
```
### Message: `predict`
Sent from the keyboard to the LMLayer whenever a new prediction should
be generated. This is typically initiated by a key press event. The
keyboard **SHOULD** track each `predict` message using a [token][Tokens]. The
token **MUST** be unique across all prediction events. The LMLayer
**SHOULD** respond to each `predict` message with a `suggestions`
message. The `suggestions` message **MUST** contain the corresponding
token as sent in the initial `predict` message.
The keyboard **MUST** send the `context` parameter. The keyboard
**SHOULD** send the `transform` parameter. The keyboard **MUST** send
a unique token.
The semantics of the `predict` message **MUST** be from the
perspective of this sequence of events:
1. After the input event is received by the keyboard.
2. Before the keyboard applies the associated `transform` to the buffer.
**NOTE**: The keyboard **MAY** apply the `transform` associated with the
input event before receiving the corresponding `suggestions` message
from the LMLayer. The intention is that once the suggestions are displayed,
the typist may select one of the suggestions in the place of the effects
of their original input.
**NOTE**: The keyboard **MAY** send the `predict` message after it has
applied the `transform` associated with the input event to the buffer.
Regardless of the actual sequence, the semantics **MUST** remain the
same:the prediction happens from the perspective before the `transform`
has been applied. Therefore, if the keyboard eagerly transforms the
buffer before it has sent `predict` message, it must anticipate _undoing_
the `transform` it has already applied if it is to apply a `transfrom`
send in the `suggestions` message. The keyboard must act as if it has
never applied the `transform` associated with the input event in the
first place.
The context is the text surrounding the insertion point, _before_ the
transform is applied to the buffer.
```typescript
interface Context {
/**
* Up to maxLeftContextCodeUnits code units of Unicode scalar value
* (i. e., characters) to the left of the insertion point in the
* buffer. If there is nothing to the left of the buffer, this returns
* an empty string.
*/
left: USVString;
/**
* Up to maxRightContextCodeUnits code units of Unicode scalar value
* (i. e., characters) to the right of the insertion point in the
* buffer. If there is nothing to the right of the buffer, this returns
* an empty string.
*/
right?: USVString;
/**
* Whether the insertion point is at the start of the buffer.
*/
startOfBuffer: boolean;
/**
* Whether the insertion point is at the end of the buffer.
*/
endOfBuffer: boolean;
}
```
The transform parameter describes how the input event will change the
buffer.
```typescript
interface Transform {
/**
* The Unicode scalar values (i.e., characters) to be inserted at the
* cursor position.
*
* Corresponds to `s` in com.keyman.KeyboardInterface.output.
*/
insert: USVString;
/**
* The number of code units to delete to the left of the cursor.
*
* Corresponds to `dn` in com.keyman.KeyboardInterface.output.
*/
delete: number;
/**
* The number of code units to delete to the right of the cursor.
* Not available on all platforms.
*/
deleteRight?: number;
}
```
### Message: `suggestions`
The `suggestions` message is sent from the LMLayer to the keyboard. This
message sends a ranked array of suggestions, in descending order of
probability (i.e., entry `0` is most likely, followed by entry `1`,
etc.). This message **MUST** be in response to a `predict` message, and
it **MUST** respond with the corresponding [token].
```typescript
/**
* `suggestions` is an ordered array of suggestion objects.
* Each suggestion is a transform bundled with a `displayAs` property.
*/
let suggestions = Suggestion[];
interface Suggestion {
/**
* Same object as an input event transform.
* Note that the transform is applied AFTER the input event
* transform.
*/
transform: Transform;
/**
* A string to display the suggestion to the typist.
* This should aid the typist understand what the transform
* will do to their text.
*/
displayAs: string;
}
```
```javascript
let suggestions = [
{
transform: {
insert: 'teapot',
deleteLeft: 1,
deleteRight: 0
},
displayAs: '🍵'
}
];
```
#### Timing
Each suggestion provides a `transform`. This transform is applied
_after_ the transform associated with the input event that initiated
this prediction. That is, the suggested transform applies to the buffer
after the transform associated with the input event.
Rephrased in somewhat mathematical terms:
Let 𝑥𝐵 be the input text buffer. Let 𝑇<sub>𝑖</sub> be the transform
associated with an input event that maps a text buffer 𝐵𝐵. Let
𝑇<sub>𝑠</sub> be a transform suggested through the LMLayer. 𝑦𝐵 is the
text buffer after the suggestion transform has been applied. The
correct sequence of applications should be as follows:
> 𝑦 = 𝑇<sub>𝑠</sub>(𝑇<sub>𝑖</sub>(𝑥))
#### Late suggestions
Sometimes, a `suggestions` message may arrive after an input event has
already invalidated its request. This is called a **"late" suggestion**.
The LMLayer **MAY** send late suggestions. Consequently, the keyboard
**MAY** discard the late suggestions. There is no requirement for the
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.

View file

@ -21,7 +21,9 @@
*/
// Include the code intended to run WITHIN the Web Worker.
// The worker code must be compiled before this file is compiled.
// The worker code MUST be compiled before this file is compiled.
// If you see a 'File: embedded_worker.js not found.' error message, please
// compile the worker first (stage one).
/// <reference path="build/intermediate/embedded_worker.js" />
/**

View file

@ -32,20 +32,55 @@
*/
type USVString = string;
// TODO: document
/**
* Top-level interface to the Language Modelling layer, or "LMLayer" for short.
*
* The Language Modelling layer provides a way for keyboards to offer prediction and
* correction functionalities. The LMLayer proper runs within a Web Worker, however,
* this class is intended to run in the main thread, and automatically spawn a Web
* Worker, capable of offering predictions.
*
* 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
* - #predict() -- ask the LMLayer to offer suggestions (predictions or corrections) for
* the input event
*
* The top-level LMLayer will automatically starts up its own Web Worker.
*/
class LMLayer {
/**
* The underlying worker instance. By default, this is the LMLayerWorker.
*/
private _worker: Worker;
/**
* 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(uri?: string) {
this._worker = new Worker(uri || LMLayer.asBlobURI(LMLayerWorkerCode));
}
// TODO: asynchronous initialize() method, based on
// https://github.com/eddieantonio/keyman-lmlayer-prototype/blob/f8e6268b03190d08cf5d35f9428cf9150d6d219e/index.ts#L42-L62
// TODO: asynchronous predict() method, based on
// https://github.com/eddieantonio/keyman-lmlayer-prototype/blob/f8e6268b03190d08cf5d35f9428cf9150d6d219e/index.ts#L64-L80
// TODO: asynchronous close() method.
// Worker code must recognize message and call self.close().
/**
* Given a function, this utility returns the source code within it.
* @param fn
* 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
* build process.
*
* @param fn The function whose body will be returned.
*/
static unwrap(fn: Function): string {
let wrapper = fn.toString();
@ -74,7 +109,7 @@ class LMLayer {
}
}
// Let LMLayer be available both in browser and in Node.
// Let LMLayer be available both in the browser and in Node.
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = LMLayer;
} else {

View file

@ -1,55 +0,0 @@
/*
* Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos)
* Copyright (c) 2018 SIL International
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* This is a test model that simply returns "teapot" when given the context of
* «I'm a little ».
*/
registerModel(function () {
return {
predict(context, transform) {
return [
{
transform: {
insert: 'teapot',
delete: transform.insert.length,
deleteRight: 0,
},
displayAs: '🍵',
weight: 0.00,
}
];
/* TODO:
if (context.wordsLeft === ["I'm", "a", "little"] &&
transform.insert === 't') {
}
return [];
*/
},
configuration: {
}
};
});
/*global registerModel*/

View file

@ -1,8 +1,10 @@
var assert = require('chai').assert;
var sinon = require('sinon');
let LMLayer = require('../../build/');
let LMLayer = require('../../build');
// Test the top-level LMLayer interface.
// Note: these tests can only be run after BOTH stages of compilation are completed.
describe('LMLayer', function() {
describe('[[constructor]]', function () {
it.skip('should be take a URI to instantiate', function () {
@ -10,17 +12,16 @@ describe('LMLayer', function() {
});
});
/**
* .unwrap() is a static function that unwraps some function code.
*/
// Since the Blob API is browser-specific, look for those tests
// for .asBlobURI() in the in_browser tests.
describe('.unwrap', function () {
// Since the Blob API is DOM-specific, look for those tests
// in the in_browser tests.
it('should return the inner code of a function', function () {
// Create a multi-line function body we can match in a RegExp.
let text = LMLayer.unwrap(function hello() {
var hello;
var world;
});
// Unwrap should give us back ONLY the body. Whitespace isn't really important.
assert.match(text, /^\s*var\s+hello;\s*var\s+world;\s*$/);
});
});

View file

@ -3,8 +3,12 @@ var sinon = require('sinon');
let LMLayerWorker = require('../../build/intermediate');
// Unit tests for instantiating and initializing the LMLayer Worker in isolation.
//
// Although the LMLayerWorker expected to be used inside a DedicatedWorkerGlobalScope,
// these unit tests DO NOT run inside a Worker, and instead use Sinon fakes to assert
// behavior.
describe('LMLayerWorker', function() {
describe('#constructor()', function() {
it('should allow for the mocking of postMessage()', function () {
var fakePostMessage = sinon.fake();
@ -147,6 +151,7 @@ describe('LMLayerWorker', function() {
});
});
// TODO: move these tests to a different file.
describe('Message: predict', function () {
it.skip('should predict from a local model', function () {
// will need import scripts figured out
@ -161,6 +166,11 @@ describe('LMLayerWorker', function() {
return { data };
}
/**
* Deprecation warning: Soon, models will not be requierd to be passed as source code;
* when this happens, tests should refrain from sending source code for the model
* parameter.
*/
function dummyModelCode() {
return 'return {model: {}, configuration: {}}';
}

View file

@ -8,12 +8,15 @@ describe('LMLayer', function () {
});
});
describe('#asBlobURI', function () {
describe('#asBlobURI()', function () {
// #asBlobURI() requires browser APIs, hence why it cannot be tested headless in Node.
it('should take a function and convert it into a blob function', function (done) {
let uri = LMLayer.asBlobURI(function dummyHandler() {
// Post something weird, so we can be reasonably certain it's not a fluke.
// WARNING: Do NOT factor out this string as a variable.
// It MUST remain a string in this function body, because it gets stringified!
// Post something weird, so we can be reasonably certain the Web Worker is...
// well, working.
// WARNING: Do NOT refactor this string as a variable. It **MUST** remain a string
// in this function body, because the code in this function's body gets
// stringified!
postMessage('fhqwhgads');
});
assert.match(uri, /^blob:/);

View file

@ -20,33 +20,37 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* The signature of self.postMessage(), so that unit tests can mock it.
*/
type PostMessage = typeof DedicatedWorkerGlobalScope.prototype.postMessage;
/**
* The valid outgoing message types.
*/
type OutgoingMessageKind = 'ready' | 'suggestions';
/**
* 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 {
/**
* Source code of the model.
* TODO: write a description of what this source code should look like.
* The model, and its configuration.
* TODO: write a description of what this actually is!
*/
model: string;
/**
* The configuration that the keyboard can offer to the model.
*/
configuration: RequestedConfiguration;
}
interface InitializeMessage {
/**
* Source code of the model.
* TODO: write a description of what this source code should look like.
*/
model: string;
model: any;
/**
* The configuration that the keyboard can offer to the model.
*/
// TODO: rename to capabilities? They **are** the capabilities of the keyboard's platform.
configuration: RequestedConfiguration;
}
/**
* The structure of the message back to the keyboard.
*/
interface ReadyMessage {
configuration: ModelConfiguration;
}
@ -104,7 +108,7 @@ interface ModelConfiguration {
rightContextCodeUnits: number;
};
// TODO:
// TODO: define what valid values of the model are.
interface Model {};
/**
@ -222,7 +226,7 @@ class LMLayerWorker {
}
}
// Let LMLayerWorker be available both in browser and in Node.
// Let LMLayerWorker be available both in the browser and in Node.
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = LMLayerWorker;
} else if (typeof self !== 'undefined' && 'postMessage' in self) {