Skip to content

Commit

Permalink
Modernization with TypeScript and Event Target (#220)
Browse files Browse the repository at this point in the history
* More typing

* Update PR number
  • Loading branch information
compulim authored Nov 20, 2024
1 parent 428d2a8 commit 181f814
Show file tree
Hide file tree
Showing 18 changed files with 437 additions and 231 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Changed

- 💥 Modernized some code with TypeScript, more type-aligned to W3C Speech API, and moved to official Event Target API, in PR [#220](https://github.com/compulim/web-speech-cognitive-services/pull/220)
- `SpeechRecognitionResult` and `SpeechRecognitionResultList` is now a array-like object, use `Array.from()` to convert them into an array
- Updated build tools and added named exports via CJS/ESM
- Bumped dependencies, in PR [#216](https://github.com/compulim/web-speech-cognitive-services/pull/216) and [#218](https://github.com/compulim/web-speech-cognitive-services/issues/218)
- Production dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const SpeechRecognitionSimpleEvents = () => {
</React.Fragment>
) : event.type === 'cognitiveservices' ? (
<span className="badge badge-light">
{event.type}:{event.data.type}
{event.type}:{event.data?.type}
</span>
) : (
<span className="badge badge-secondary">{event.type}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
type EventListener<T extends Event> = (event: T) => void;

export default class EventListenerMap<T extends string, EventMap extends Record<T, Event>> {
constructor(eventTarget: EventTarget) {
this.#eventTarget = eventTarget;
this.#propertyMap = {};
}

#eventTarget: EventTarget;
#propertyMap: { [Name in keyof EventMap]?: EventListener<EventMap[Name]> | undefined };

getProperty(name: T): ((event: EventMap[typeof name]) => void) | undefined {
return this.#propertyMap[name];
}

setProperty(name: T, value: ((event: EventMap[typeof name]) => void) | undefined) {
const existing = this.#propertyMap[name];

existing && this.#eventTarget.removeEventListener(name, existing as EventListener<Event>);

if (value) {
this.#eventTarget.addEventListener(name, value as EventListener<Event>);
}

this.#propertyMap[name] = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import FakeArray from './FakeArray';

test('should return indexed item', () => {
expect(new FakeArray([1, 2, 3])).toHaveProperty([1], 2);
});

test('should return indexed item via Reflect.get', () => {
expect(Reflect.get(new FakeArray([1, 2, 3]), 1)).toBe(2);
});

test('should return length', () => {
expect(new FakeArray([1, 2, 3])).toHaveProperty('length', 3);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
interface FakeArrayInterface<T> {
[index: number]: T | undefined;
get length(): number;
}

export default class FakeArray<T> implements FakeArrayInterface<T> {
constructor(array: readonly T[]) {
if (!array) {
throw new Error('array must be set.');
}

this.#array = array;

for (const key in array) {
Object.defineProperty(this, key, {
enumerable: true,
get() {
return array[key];
}
});
}
}

#array: readonly T[];
[index: number]: T | undefined;
[Symbol.iterator]() {
return this.#array[Symbol.iterator]();
}

get length(): number {
return this.#array.length;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type SpeechRecognitionAlternativeInit = {
confidence: number;
transcript: string;
};

export default class SpeechRecognitionAlternative {
constructor({ confidence, transcript }: SpeechRecognitionAlternativeInit) {
this.#confidence = confidence;
this.#transcript = transcript;
}

#confidence: number;
#transcript: string;

get confidence() {
return this.#confidence;
}

get transcript() {
return this.#transcript;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type SpeechRecognitionErrorType =
| 'aborted'
| 'audio-capture'
| 'bad-grammar'
| 'language-not-supported'
| 'network'
| 'no-speech'
| 'not-allowed'
| 'service-not-allowed';

export type SpeechRecognitionErrorEventInit = {
error: SpeechRecognitionErrorType;
message?: string | undefined;
};

export default class SpeechRecognitionErrorEvent extends Event {
constructor(type: 'error', { error, message }: SpeechRecognitionErrorEventInit) {
super(type);

this.#error = error;
this.#message = message;
}

#error: SpeechRecognitionErrorType;
#message: string | undefined;

get error(): SpeechRecognitionErrorType {
return this.#error;
}

get message(): string | undefined {
return this.#message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import SpeechRecognitionResultList from './SpeechRecognitionResultList';

export type SpeechRecognitionEventInit = {
data?: undefined | unknown;
resultIndex?: number | undefined;
results?: SpeechRecognitionResultList | undefined;
};

export default class SpeechRecognitionEvent<
T extends
| 'audioend'
| 'audiostart'
| 'cognitiveservices'
| 'end'
| 'result'
| 'soundend'
| 'soundstart'
| 'speechend'
| 'speechstart'
| 'start'
> extends Event {
constructor(type: 'cognitiveservices', init: SpeechRecognitionEventInit & { data: { type: string } });
constructor(type: 'audioend');
constructor(type: 'audiostart');
constructor(type: 'end');
constructor(type: 'result', init: SpeechRecognitionEventInit);
constructor(type: 'soundend');
constructor(type: 'soundstart');
constructor(type: 'speechend');
constructor(type: 'speechstart');
constructor(type: 'start');

constructor(type: T, { data, resultIndex, results }: SpeechRecognitionEventInit = {}) {
super(type);

this.#data = data;
this.#resultIndex = resultIndex;
this.#results = results || new SpeechRecognitionResultList([]);
}

#data: undefined | unknown;
// TODO: "resultIndex" should be set.
#resultIndex: number | undefined;
#results: SpeechRecognitionResultList;

get data(): unknown {
return this.#data;
}

get resultIndex(): number | undefined {
return this.#resultIndex;
}

get results(): SpeechRecognitionResultList {
return this.#results;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import EventListenerMap from './EventListenerMap';
import type SpeechRecognitionErrorEvent from './SpeechRecognitionErrorEvent';
import type SpeechRecognitionEvent from './SpeechRecognitionEvent';

export type SpeechRecognitionEventListenerMap = EventListenerMap<
| 'audioend'
| 'audiostart'
| 'cognitiveservices'
| 'end'
| 'error'
| 'result'
| 'soundend'
| 'soundstart'
| 'speechend'
| 'speechstart'
| 'start',
{
audioend: SpeechRecognitionEvent<'audioend'>;
audiostart: SpeechRecognitionEvent<'audiostart'>;
cognitiveservices: SpeechRecognitionEvent<'cognitiveservices'>;
end: SpeechRecognitionEvent<'end'>;
error: SpeechRecognitionErrorEvent;
result: SpeechRecognitionEvent<'result'>;
soundend: SpeechRecognitionEvent<'soundend'>;
soundstart: SpeechRecognitionEvent<'soundstart'>;
speechend: SpeechRecognitionEvent<'speechend'>;
speechstart: SpeechRecognitionEvent<'speechstart'>;
start: SpeechRecognitionEvent<'start'>;
}
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import FakeArray from './FakeArray';

export type SpeechRecognitionResultInit = {
isFinal: boolean;
results: readonly SpeechRecognitionAlternative[];
};

export default class SpeechRecognitionResult extends FakeArray<SpeechRecognitionAlternative> {
constructor(init: SpeechRecognitionResultInit) {
super(init.results);

this.#isFinal = init.isFinal;
}

#isFinal: boolean;

get isFinal(): boolean {
return this.#isFinal;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import FakeArray from './FakeArray';
import type SpeechRecognitionResult from './SpeechRecognitionResult';

export default class SpeechRecognitionResultList extends FakeArray<SpeechRecognitionResult> {
constructor(result: readonly SpeechRecognitionResult[]) {
super(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* @jest-environment jsdom
*/

import cognitiveServiceEventResultToWebSpeechRecognitionResultList from './cognitiveServiceEventResultToWebSpeechRecognitionResultList';
import cognitiveServiceEventResultToWebSpeechRecognitionResult from './cognitiveServiceEventResultToWebSpeechRecognitionResult';

test('Multiple results with RecognitionStatus === "Success"', () => {
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResultList({
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResult({
json: {
NBest: [
{
Expand All @@ -28,23 +28,23 @@ test('Multiple results with RecognitionStatus === "Success"', () => {
reason: 3
});

expect(resultList[0]).toEqual({ confidence: 0.25, transcript: 'No.' });
expect(resultList[1]).toEqual({ confidence: 0.1, transcript: 'Yes.' });
expect(resultList[0]).toEqual(expect.objectContaining({ confidence: 0.25, transcript: 'No.' }));
expect(resultList[1]).toEqual(expect.objectContaining({ confidence: 0.1, transcript: 'Yes.' }));
expect(resultList).toHaveProperty('isFinal', true);
});

test('Single interim results', () => {
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResultList({
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResult({
reason: 2,
text: 'No.'
});

expect(resultList[0]).toEqual({ confidence: 0.5, transcript: 'No.' });
expect(resultList).not.toHaveProperty('isFinal');
expect(resultList[0]).toEqual(expect.objectContaining({ confidence: 0.5, transcript: 'No.' }));
expect(resultList).toHaveProperty('isFinal', false);
});

test('Single final results', () => {
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResultList({
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResult({
json: {
NBest: [
{
Expand All @@ -59,12 +59,12 @@ test('Single final results', () => {
reason: 3
});

expect(resultList[0]).toEqual({ confidence: 0.25, transcript: 'No.' });
expect(resultList[0]).toEqual(expect.objectContaining({ confidence: 0.25, transcript: 'No.' }));
expect(resultList).toHaveProperty('isFinal', true);
});

test('Single final results with ITN', () => {
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResultList(
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResult(
{
json: {
NBest: [
Expand All @@ -84,12 +84,12 @@ test('Single final results with ITN', () => {
}
);

expect(resultList[0]).toEqual({ confidence: 0.25, transcript: 'no (ITN)' });
expect(resultList[0]).toEqual(expect.objectContaining({ confidence: 0.25, transcript: 'no (ITN)' }));
expect(resultList).toHaveProperty('isFinal', true);
});

test('Single final results with lexical', () => {
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResultList(
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResult(
{
json: {
NBest: [
Expand All @@ -109,12 +109,12 @@ test('Single final results with lexical', () => {
}
);

expect(resultList[0]).toEqual({ confidence: 0.25, transcript: 'no (Lexical)' });
expect(resultList[0]).toEqual(expect.objectContaining({ confidence: 0.25, transcript: 'no (Lexical)' }));
expect(resultList).toHaveProperty('isFinal', true);
});

test('Single final results with masked ITN', () => {
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResultList(
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResult(
{
json: {
NBest: [
Expand All @@ -134,12 +134,12 @@ test('Single final results with masked ITN', () => {
}
);

expect(resultList[0]).toEqual({ confidence: 0.25, transcript: 'no (MaskedITN)' });
expect(resultList[0]).toEqual(expect.objectContaining({ confidence: 0.25, transcript: 'no (MaskedITN)' }));
expect(resultList).toHaveProperty('isFinal', true);
});

test('Result is iterable', () => {
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResultList(
const resultList = cognitiveServiceEventResultToWebSpeechRecognitionResult(
{
json: {
NBest: [
Expand All @@ -160,6 +160,6 @@ test('Result is iterable', () => {
const [firstAlternative] = resultList;
const { isFinal } = resultList;

expect(firstAlternative).toEqual({ confidence: 0.25, transcript: 'No.' });
expect(firstAlternative).toEqual(expect.objectContaining({ confidence: 0.25, transcript: 'No.' }));
expect(isFinal).toBe(true);
});
Loading

0 comments on commit 181f814

Please sign in to comment.