@corti/dictation-web 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,6 @@ The **Corti Dictation SDK** is a web component that enables real-time speech-to-
5
5
 
6
6
  > **Note:** OAuth 2.0 authentication is not handled by this SDK. The client must provide an API key or authorization token before using the component.
7
7
 
8
- ---
9
8
 
10
9
  ## Installation
11
10
 
@@ -15,10 +14,14 @@ Include the SDK in your project by importing the JavaScript module:
15
14
  npm i @corti/dictation-web
16
15
  ```
17
16
 
18
- ---
19
17
 
20
18
  ## Usage
21
19
 
20
+ ### Demo
21
+
22
+ 🚀 [Hosted Demo](https://codepen.io/hccullen/pen/OPJmxQR)
23
+
24
+
22
25
  ### Basic Example
23
26
 
24
27
  ```html
@@ -29,12 +32,12 @@ npm i @corti/dictation-web
29
32
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
30
33
  </head>
31
34
  <body>
35
+ <script src="https://cdn.jsdelivr.net/npm/@corti/dictation-web/dist/index.min.js"></script>
32
36
  <corti-dictation authToken="xyz"></corti-dictation>
33
37
  <textarea id="transcript" placeholder="Transcript will appear here..."></textarea>
34
38
 
35
- <script type="module">
36
- import '@corti/dictation-web';
37
-
39
+ <script>
40
+ const dictationEl = document.getElementById('transcript')
38
41
  // Listen for events
39
42
  dictationEl.addEventListener('transcript', (e) => {
40
43
  document.getElementById('transcript').value += e.detail.data.text + ' ';
@@ -44,7 +47,6 @@ npm i @corti/dictation-web
44
47
  </html>
45
48
  ```
46
49
 
47
- ---
48
50
 
49
51
  ## API Reference
50
52
 
@@ -72,30 +74,20 @@ npm i @corti/dictation-web
72
74
  | `transcript` | Fired when a new transcript is received. `detail.data.text` contains the transcribed text. |
73
75
  | `audio-level-changed` | Fired when the input audio level changes. `detail.audioLevel` contains the new level. |
74
76
 
75
- ---
76
77
 
77
78
  ## Authentication
78
79
 
79
80
  This SDK does not handle OAuth 2.0 authentication. The client must provide an API key or access token as a string in `authToken`.
80
81
 
81
- ---
82
82
 
83
83
  ## Notes
84
- - Ensure the provided API token is valid.
85
- - The component requires microphone access permissions.
86
84
  - Works in modern browsers that support Web Components and MediaRecorder API.
85
+ - Supports dark and light mode based on browser preference.
87
86
 
88
- ---
89
87
 
90
88
  ## License
91
89
  This SDK is not licensed.
92
90
 
93
- ---
94
-
95
- ## Developer Guide
96
- See [Developer Setup](docs/DEV_README.md) for installation and development details.
97
-
98
- ---
99
91
 
100
92
  ## Support
101
93
  For issues or questions, contact **Corti Support** at [support@corti.ai](mailto:support@corti.ai).
@@ -1,8 +1,8 @@
1
1
  import { LitElement } from 'lit';
2
- import './components/settings-menu';
3
- import './components/audio-visualiser';
4
- import './icons/icons';
5
- import { DictationConfig } from './types';
2
+ import './components/settings-menu.js';
3
+ import './components/audio-visualiser.js';
4
+ import './icons/icons.js';
5
+ import type { DictationConfig } from './types.js';
6
6
  export declare class CortiDictation extends LitElement {
7
7
  static styles: import("lit").CSSResult[];
8
8
  devices: MediaDeviceInfo[];
@@ -2,15 +2,15 @@ import { __decorate } from "tslib";
2
2
  // corti-dictation.ts
3
3
  import { html, LitElement } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
- import { RecorderManager } from './RecorderManager';
6
- import './components/settings-menu';
7
- import './components/audio-visualiser';
8
- import './icons/icons';
9
- import ThemeStyles from './styles/theme';
10
- import ButtonStyles from './styles/buttons';
11
- import ComponentStyles from './styles/ComponentStyles';
12
- import { DEFAULT_DICTATION_CONFIG } from './constants';
13
- import CalloutStyles from './styles/callout';
5
+ import { RecorderManager } from './RecorderManager.js';
6
+ import './components/settings-menu.js';
7
+ import './components/audio-visualiser.js';
8
+ import './icons/icons.js';
9
+ import ThemeStyles from './styles/theme.js';
10
+ import ButtonStyles from './styles/buttons.js';
11
+ import ComponentStyles from './styles/ComponentStyles.js';
12
+ import { DEFAULT_DICTATION_CONFIG } from './constants.js';
13
+ import CalloutStyles from './styles/callout.js';
14
14
  export class CortiDictation extends LitElement {
15
15
  constructor() {
16
16
  super(...arguments);
@@ -1 +1 @@
1
- {"version":3,"file":"CortiDictation.js","sourceRoot":"","sources":["../src/CortiDictation.ts"],"names":[],"mappings":"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","sourcesContent":["// corti-dictation.ts\nimport { html, LitElement } from 'lit';\nimport { property, state } from 'lit/decorators.js';\nimport { RecorderManager } from './RecorderManager';\nimport './components/settings-menu';\nimport './components/audio-visualiser';\nimport './icons/icons';\nimport ThemeStyles from './styles/theme';\nimport ButtonStyles from './styles/buttons';\nimport ComponentStyles from './styles/ComponentStyles';\n\nimport { DictationConfig } from './types';\nimport { DEFAULT_DICTATION_CONFIG } from './constants';\nimport CalloutStyles from './styles/callout';\n\nexport class CortiDictation extends LitElement {\n static styles = [ButtonStyles, ThemeStyles, ComponentStyles, CalloutStyles];\n\n @property({ type: Array })\n devices: MediaDeviceInfo[] = [];\n\n @property({ type: String, reflect: true })\n recordingState = 'stopped';\n\n @property({ type: Object })\n dictationConfig: DictationConfig = DEFAULT_DICTATION_CONFIG;\n\n @property({ type: String })\n authToken: string | undefined;\n\n @state()\n private _audioLevel = 0;\n\n private recorderManager = new RecorderManager();\n\n async connectedCallback() {\n super.connectedCallback();\n await this.recorderManager.initialize();\n this.devices = this.recorderManager.devices;\n\n // Map event names to any extra handling logic\n const eventHandlers: Record<string, (e: CustomEvent) => void> = {\n 'recording-state-changed': e => {\n this.recordingState = e.detail.state;\n },\n 'audio-level-changed': e => {\n this._audioLevel = e.detail.audioLevel;\n this.requestUpdate();\n },\n };\n\n const eventsToRelay = [\n 'recording-state-changed',\n 'audio-level-changed',\n 'error',\n 'transcript',\n ];\n\n eventsToRelay.forEach(eventName => {\n this.recorderManager.addEventListener(eventName, (e: Event) => {\n const customEvent = e as CustomEvent;\n // Perform any additional handling if defined\n if (eventHandlers[eventName]) {\n eventHandlers[eventName](customEvent);\n }\n // Re-dispatch the event from the component\n this.dispatchEvent(\n new CustomEvent(eventName, {\n detail: customEvent.detail,\n bubbles: true,\n composed: true,\n }),\n );\n });\n });\n }\n\n public toggleRecording() {\n this._toggleRecording();\n }\n\n _toggleRecording() {\n if (!this.authToken) return;\n if (this.recordingState === 'recording') {\n this.recorderManager.stopRecording();\n } else if (this.recordingState === 'stopped') {\n this.recorderManager.startRecording({\n dictationConfig: this.dictationConfig,\n authToken: this.authToken,\n });\n }\n }\n\n // Handle device change events if needed\n async _onRecordingDeviceChanged(event: Event) {\n const customEvent = event as CustomEvent;\n if (!this.authToken) return;\n this.recorderManager.selectedDevice = customEvent.detail.deviceId;\n if (this.recordingState === 'recording') {\n await this.recorderManager.stopRecording();\n await this.recorderManager.startRecording({\n dictationConfig: this.dictationConfig,\n authToken: this.authToken,\n });\n }\n }\n\n render() {\n const isConfigured = this.authToken;\n if (!isConfigured) {\n return html`\n <div class=\"wrapper\">\n <div class=\"callout red tiny\">\n Please configure the server settings in the parent component.\n </div>\n </div>\n `;\n }\n\n const isLoading =\n this.recordingState === 'initializing' ||\n this.recordingState === 'stopping';\n const isRecording = this.recordingState === 'recording';\n return html`\n <div class=\"wrapper\">\n <button\n @click=${this._toggleRecording}\n class=${isRecording ? 'red' : 'accent'}\n >\n ${isLoading\n ? html`<icon-loading-spinner></icon-loading-spinner>`\n : isRecording\n ? html`<icon-recording></icon-recording>`\n : html`<icon-mic-on></icon-mic-on>`}\n <audio-visualiser\n .level=${this._audioLevel}\n .active=${isRecording}\n ></audio-visualiser>\n </button>\n\n <settings-menu\n .devices=${this.devices}\n .selectedDevice=${this.recorderManager.selectedDevice}\n ?settingsDisabled=${this.recordingState !== 'stopped'}\n @recording-device-changed=${this._onRecordingDeviceChanged}\n ></settings-menu>\n </div>\n `;\n }\n}\n\nexport default CortiDictation;\n"]}
1
+ {"version":3,"file":"CortiDictation.js","sourceRoot":"","sources":["../src/CortiDictation.ts"],"names":[],"mappings":"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","sourcesContent":["// corti-dictation.ts\nimport { html, LitElement } from 'lit';\nimport { property, state } from 'lit/decorators.js';\nimport { RecorderManager } from './RecorderManager.js';\nimport './components/settings-menu.js';\nimport './components/audio-visualiser.js';\nimport './icons/icons.js';\nimport ThemeStyles from './styles/theme.js';\nimport ButtonStyles from './styles/buttons.js';\nimport ComponentStyles from './styles/ComponentStyles.js';\n\nimport type { DictationConfig } from './types.js';\nimport { DEFAULT_DICTATION_CONFIG } from './constants.js';\nimport CalloutStyles from './styles/callout.js';\n\nexport class CortiDictation extends LitElement {\n static styles = [ButtonStyles, ThemeStyles, ComponentStyles, CalloutStyles];\n\n @property({ type: Array })\n devices: MediaDeviceInfo[] = [];\n\n @property({ type: String, reflect: true })\n recordingState = 'stopped';\n\n @property({ type: Object })\n dictationConfig: DictationConfig = DEFAULT_DICTATION_CONFIG;\n\n @property({ type: String })\n authToken: string | undefined;\n\n @state()\n private _audioLevel = 0;\n\n private recorderManager = new RecorderManager();\n\n async connectedCallback() {\n super.connectedCallback();\n await this.recorderManager.initialize();\n this.devices = this.recorderManager.devices;\n\n // Map event names to any extra handling logic\n const eventHandlers: Record<string, (e: CustomEvent) => void> = {\n 'recording-state-changed': e => {\n this.recordingState = e.detail.state;\n },\n 'audio-level-changed': e => {\n this._audioLevel = e.detail.audioLevel;\n this.requestUpdate();\n },\n };\n\n const eventsToRelay = [\n 'recording-state-changed',\n 'audio-level-changed',\n 'error',\n 'transcript',\n ];\n\n eventsToRelay.forEach(eventName => {\n this.recorderManager.addEventListener(eventName, (e: Event) => {\n const customEvent = e as CustomEvent;\n // Perform any additional handling if defined\n if (eventHandlers[eventName]) {\n eventHandlers[eventName](customEvent);\n }\n // Re-dispatch the event from the component\n this.dispatchEvent(\n new CustomEvent(eventName, {\n detail: customEvent.detail,\n bubbles: true,\n composed: true,\n }),\n );\n });\n });\n }\n\n public toggleRecording() {\n this._toggleRecording();\n }\n\n _toggleRecording() {\n if (!this.authToken) return;\n if (this.recordingState === 'recording') {\n this.recorderManager.stopRecording();\n } else if (this.recordingState === 'stopped') {\n this.recorderManager.startRecording({\n dictationConfig: this.dictationConfig,\n authToken: this.authToken,\n });\n }\n }\n\n // Handle device change events if needed\n async _onRecordingDeviceChanged(event: Event) {\n const customEvent = event as CustomEvent;\n if (!this.authToken) return;\n this.recorderManager.selectedDevice = customEvent.detail.deviceId;\n if (this.recordingState === 'recording') {\n await this.recorderManager.stopRecording();\n await this.recorderManager.startRecording({\n dictationConfig: this.dictationConfig,\n authToken: this.authToken,\n });\n }\n }\n\n render() {\n const isConfigured = this.authToken;\n if (!isConfigured) {\n return html`\n <div class=\"wrapper\">\n <div class=\"callout red tiny\">\n Please configure the server settings in the parent component.\n </div>\n </div>\n `;\n }\n\n const isLoading =\n this.recordingState === 'initializing' ||\n this.recordingState === 'stopping';\n const isRecording = this.recordingState === 'recording';\n return html`\n <div class=\"wrapper\">\n <button\n @click=${this._toggleRecording}\n class=${isRecording ? 'red' : 'accent'}\n >\n ${isLoading\n ? html`<icon-loading-spinner></icon-loading-spinner>`\n : isRecording\n ? html`<icon-recording></icon-recording>`\n : html`<icon-mic-on></icon-mic-on>`}\n <audio-visualiser\n .level=${this._audioLevel}\n .active=${isRecording}\n ></audio-visualiser>\n </button>\n\n <settings-menu\n .devices=${this.devices}\n .selectedDevice=${this.recorderManager.selectedDevice}\n ?settingsDisabled=${this.recordingState !== 'stopped'}\n @recording-device-changed=${this._onRecordingDeviceChanged}\n ></settings-menu>\n </div>\n `;\n }\n}\n\nexport default CortiDictation;\n"]}
@@ -1,13 +1,15 @@
1
- import { DictationConfig } from './types';
1
+ import type { DictationConfig } from './types.js';
2
2
  export declare class DictationService extends EventTarget {
3
3
  private mediaRecorder;
4
4
  private webSocket;
5
5
  private authToken;
6
6
  private dictationConfig;
7
+ private serverConfig;
7
8
  constructor(mediaStream: MediaStream, { dictationConfig, authToken, }: {
8
9
  dictationConfig: DictationConfig;
9
10
  authToken: string;
10
11
  });
12
+ private dispatchCustomEvent;
11
13
  startRecording(): void;
12
14
  stopRecording(): Promise<void>;
13
15
  }
@@ -1,20 +1,31 @@
1
- import { decodeToken } from './utils';
1
+ import { decodeToken } from './utils.js';
2
2
  export class DictationService extends EventTarget {
3
3
  constructor(mediaStream, { dictationConfig, authToken, }) {
4
4
  super();
5
5
  this.mediaRecorder = new MediaRecorder(mediaStream);
6
6
  this.authToken = authToken;
7
7
  this.dictationConfig = dictationConfig;
8
+ // Decode token during construction.
9
+ const config = decodeToken(this.authToken);
10
+ if (!config) {
11
+ throw new Error('Invalid token');
12
+ }
13
+ this.serverConfig = config;
8
14
  this.mediaRecorder.ondataavailable = event => {
9
- // if webSocket is open, send the data
10
15
  if (this.webSocket?.readyState === WebSocket.OPEN) {
11
16
  this.webSocket.send(event.data);
12
17
  }
13
18
  };
14
19
  }
20
+ dispatchCustomEvent(eventName, detail) {
21
+ this.dispatchEvent(new CustomEvent(eventName, {
22
+ detail,
23
+ bubbles: true,
24
+ composed: true,
25
+ }));
26
+ }
15
27
  startRecording() {
16
- const serverConfig = decodeToken(this.authToken || '');
17
- const url = `wss://api.${serverConfig.environment}.corti.app/audio-bridge/v2/transcribe?tenant-name=${serverConfig.tenant}&token=Bearer%20${this.authToken}`;
28
+ const url = `wss://api.${this.serverConfig.environment}.corti.app/audio-bridge/v2/transcribe?tenant-name=${this.serverConfig.tenant}&token=Bearer%20${this.authToken}`;
18
29
  this.webSocket = new WebSocket(url);
19
30
  this.webSocket.onopen = () => {
20
31
  this.webSocket.send(JSON.stringify({
@@ -28,44 +39,29 @@ export class DictationService extends EventTarget {
28
39
  this.mediaRecorder.start(250);
29
40
  }
30
41
  else if (message.type === 'transcript') {
31
- this.dispatchEvent(new CustomEvent('transcript', {
32
- detail: message,
33
- bubbles: true,
34
- composed: true,
35
- }));
42
+ this.dispatchCustomEvent('transcript', message);
36
43
  }
37
44
  };
38
45
  this.webSocket.onerror = event => {
39
- this.dispatchEvent(new CustomEvent('error', {
40
- detail: event,
41
- bubbles: true,
42
- composed: true,
43
- }));
46
+ this.dispatchCustomEvent('error', event);
44
47
  };
45
48
  this.webSocket.onclose = event => {
46
- this.dispatchEvent(new CustomEvent('stream-closed', {
47
- detail: event,
48
- bubbles: true,
49
- composed: true,
50
- }));
49
+ this.dispatchCustomEvent('stream-closed', event);
51
50
  };
52
51
  }
53
52
  async stopRecording() {
54
53
  this.mediaRecorder.stop();
55
54
  if (this.webSocket?.readyState === WebSocket.OPEN) {
56
- this.webSocket.send(JSON.stringify({
57
- type: 'end',
58
- }));
55
+ this.webSocket.send(JSON.stringify({ type: 'end' }));
59
56
  }
60
- const timeOut = setTimeout(() => {
57
+ const timeout = setTimeout(() => {
61
58
  if (this.webSocket?.readyState === WebSocket.OPEN) {
62
59
  this.webSocket.close();
63
60
  }
64
61
  }, 10000);
65
- // This implementation should be replaced by handling a proper 'ended' message from the server
66
62
  this.webSocket.onclose = () => {
67
63
  this.webSocket?.close();
68
- clearTimeout(timeOut);
64
+ clearTimeout(timeout);
69
65
  };
70
66
  }
71
67
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DictationService.js","sourceRoot":"","sources":["../src/DictationService.ts"],"names":[],"mappings":"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","sourcesContent":["import { DictationConfig } from './types';\nimport { decodeToken } from './utils';\n\nexport class DictationService extends EventTarget {\n private mediaRecorder: MediaRecorder;\n\n private webSocket!: WebSocket;\n\n private authToken!: string;\n\n private dictationConfig!: DictationConfig;\n\n constructor(\n mediaStream: MediaStream,\n {\n dictationConfig,\n authToken,\n }: { dictationConfig: DictationConfig; authToken: string },\n ) {\n super();\n this.mediaRecorder = new MediaRecorder(mediaStream);\n this.authToken = authToken;\n this.dictationConfig = dictationConfig;\n this.mediaRecorder.ondataavailable = event => {\n // if webSocket is open, send the data\n if (this.webSocket?.readyState === WebSocket.OPEN) {\n this.webSocket.send(event.data);\n }\n };\n }\n\n public startRecording() {\n const serverConfig = decodeToken(this.authToken || '');\n\n const url = `wss://api.${serverConfig.environment}.corti.app/audio-bridge/v2/transcribe?tenant-name=${serverConfig.tenant}&token=Bearer%20${this.authToken}`;\n this.webSocket = new WebSocket(url);\n this.webSocket.onopen = () => {\n this.webSocket.send(\n JSON.stringify({\n type: 'config',\n configuration: this.dictationConfig,\n }),\n );\n };\n this.webSocket.onmessage = event => {\n const message = JSON.parse(event.data);\n if (message.type === 'config') {\n this.mediaRecorder.start(250);\n } else if (message.type === 'transcript') {\n this.dispatchEvent(\n new CustomEvent('transcript', {\n detail: message,\n bubbles: true,\n composed: true,\n }),\n );\n }\n };\n this.webSocket.onerror = event => {\n this.dispatchEvent(\n new CustomEvent('error', {\n detail: event,\n bubbles: true,\n composed: true,\n }),\n );\n };\n this.webSocket.onclose = event => {\n this.dispatchEvent(\n new CustomEvent('stream-closed', {\n detail: event,\n bubbles: true,\n composed: true,\n }),\n );\n };\n }\n\n public async stopRecording() {\n this.mediaRecorder.stop();\n\n if (this.webSocket?.readyState === WebSocket.OPEN) {\n this.webSocket.send(\n JSON.stringify({\n type: 'end',\n }),\n );\n }\n\n const timeOut: NodeJS.Timeout = setTimeout(() => {\n if (this.webSocket?.readyState === WebSocket.OPEN) {\n this.webSocket.close();\n }\n }, 10000);\n\n // This implementation should be replaced by handling a proper 'ended' message from the server\n this.webSocket.onclose = () => {\n this.webSocket?.close();\n clearTimeout(timeOut);\n };\n }\n}\n"]}
1
+ {"version":3,"file":"DictationService.js","sourceRoot":"","sources":["../src/DictationService.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DictationConfig, ServerConfig } from './types.js';\nimport { decodeToken } from './utils.js';\n\nexport class DictationService extends EventTarget {\n private mediaRecorder: MediaRecorder;\n private webSocket!: WebSocket;\n private authToken: string;\n private dictationConfig: DictationConfig;\n private serverConfig: ServerConfig;\n\n constructor(\n mediaStream: MediaStream,\n {\n dictationConfig,\n authToken,\n }: { dictationConfig: DictationConfig; authToken: string },\n ) {\n super();\n this.mediaRecorder = new MediaRecorder(mediaStream);\n this.authToken = authToken;\n this.dictationConfig = dictationConfig;\n\n // Decode token during construction.\n const config = decodeToken(this.authToken);\n if (!config) {\n throw new Error('Invalid token');\n }\n this.serverConfig = config;\n\n this.mediaRecorder.ondataavailable = event => {\n if (this.webSocket?.readyState === WebSocket.OPEN) {\n this.webSocket.send(event.data);\n }\n };\n }\n\n private dispatchCustomEvent(eventName: string, detail: unknown): void {\n this.dispatchEvent(\n new CustomEvent(eventName, {\n detail,\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n public startRecording(): void {\n const url = `wss://api.${this.serverConfig.environment}.corti.app/audio-bridge/v2/transcribe?tenant-name=${this.serverConfig.tenant}&token=Bearer%20${this.authToken}`;\n this.webSocket = new WebSocket(url);\n\n this.webSocket.onopen = () => {\n this.webSocket.send(\n JSON.stringify({\n type: 'config',\n configuration: this.dictationConfig,\n }),\n );\n };\n\n this.webSocket.onmessage = event => {\n const message = JSON.parse(event.data);\n if (message.type === 'config') {\n this.mediaRecorder.start(250);\n } else if (message.type === 'transcript') {\n this.dispatchCustomEvent('transcript', message);\n }\n };\n\n this.webSocket.onerror = event => {\n this.dispatchCustomEvent('error', event);\n };\n\n this.webSocket.onclose = event => {\n this.dispatchCustomEvent('stream-closed', event);\n };\n }\n\n public async stopRecording(): Promise<void> {\n this.mediaRecorder.stop();\n\n if (this.webSocket?.readyState === WebSocket.OPEN) {\n this.webSocket.send(JSON.stringify({ type: 'end' }));\n }\n\n const timeout = setTimeout(() => {\n if (this.webSocket?.readyState === WebSocket.OPEN) {\n this.webSocket.close();\n }\n }, 10000);\n\n this.webSocket.onclose = () => {\n this.webSocket?.close();\n clearTimeout(timeout);\n };\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { DictationConfig, RecordingState } from './types';
1
+ import type { DictationConfig, RecordingState } from './types.js';
2
2
  export declare class RecorderManager extends EventTarget {
3
3
  devices: MediaDeviceInfo[];
4
4
  selectedDevice: string;
@@ -11,6 +11,7 @@ export declare class RecorderManager extends EventTarget {
11
11
  devices: MediaDeviceInfo[];
12
12
  defaultDeviceId?: string;
13
13
  }>;
14
+ private dispatchCustomEvent;
14
15
  startRecording(params: {
15
16
  dictationConfig: DictationConfig;
16
17
  authToken: string;
@@ -1,6 +1,6 @@
1
- import { getAudioDevices } from './utils';
2
- import { AudioService } from './audioService';
3
- import { DictationService } from './DictationService';
1
+ import { getAudioDevices } from './utils.js';
2
+ import { AudioService } from './audioService.js';
3
+ import { DictationService } from './DictationService.js';
4
4
  export class RecorderManager extends EventTarget {
5
5
  constructor() {
6
6
  super(...arguments);
@@ -17,47 +17,48 @@ export class RecorderManager extends EventTarget {
17
17
  this.selectedDevice = deviceResponse.defaultDeviceId || '';
18
18
  return deviceResponse;
19
19
  }
20
+ dispatchCustomEvent(eventName, detail) {
21
+ this.dispatchEvent(new CustomEvent(eventName, {
22
+ detail,
23
+ bubbles: true,
24
+ composed: true,
25
+ }));
26
+ }
20
27
  async startRecording(params) {
21
28
  this._updateRecordingState('initializing');
29
+ // Get media stream and initialize audio service.
22
30
  try {
23
31
  this._mediaStream = await navigator.mediaDevices.getUserMedia({
24
32
  audio: { deviceId: this.selectedDevice },
25
33
  });
26
34
  this._audioService = new AudioService(this._mediaStream);
35
+ }
36
+ catch (error) {
37
+ this.dispatchCustomEvent('error', error);
38
+ this._updateRecordingState('stopped');
39
+ return;
40
+ }
41
+ // Initialize dictation service.
42
+ try {
27
43
  this._dictationService = new DictationService(this._mediaStream, params);
28
- // Forward custom events from dictation service
29
- this._dictationService.addEventListener('error', e => this.dispatchEvent(new CustomEvent('error', {
30
- detail: e.detail,
31
- bubbles: true,
32
- composed: true,
33
- })));
34
- this._dictationService.addEventListener('stream-closed', () => this.stopRecording());
35
- this._dictationService.addEventListener('transcript', e => this.dispatchEvent(new CustomEvent('transcript', {
36
- detail: e.detail,
37
- bubbles: true,
38
- composed: true,
39
- })));
40
44
  }
41
45
  catch (error) {
42
- this.dispatchEvent(new CustomEvent('error', {
43
- detail: error,
44
- bubbles: true,
45
- composed: true,
46
- }));
46
+ this.dispatchCustomEvent('error', error);
47
47
  this._updateRecordingState('stopped');
48
48
  return;
49
49
  }
50
- this._dictationService?.startRecording();
50
+ // Forward dictation service events.
51
+ this._dictationService.addEventListener('error', e => this.dispatchCustomEvent('error', e.detail));
52
+ this._dictationService.addEventListener('stream-closed', () => this.stopRecording());
53
+ this._dictationService.addEventListener('transcript', e => this.dispatchCustomEvent('transcript', e.detail));
54
+ this._dictationService.startRecording();
51
55
  this._updateRecordingState('recording');
56
+ // Update audio level visualization.
52
57
  this._visualiserInterval = window.setInterval(() => {
53
58
  const level = this._audioService
54
59
  ? this._audioService.getAudioLevel() * 3
55
60
  : 0;
56
- this.dispatchEvent(new CustomEvent('audio-level-changed', {
57
- detail: { audioLevel: level },
58
- bubbles: true,
59
- composed: true,
60
- }));
61
+ this.dispatchCustomEvent('audio-level-changed', { audioLevel: level });
61
62
  }, 150);
62
63
  }
63
64
  async stopRecording() {
@@ -1 +1 @@
1
- {"version":3,"file":"RecorderManager.js","sourceRoot":"","sources":["../src/RecorderManager.ts"],"names":[],"mappings":"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","sourcesContent":["import { getAudioDevices } from './utils';\nimport { AudioService } from './audioService';\nimport { DictationService } from './DictationService';\nimport type { DictationConfig, RecordingState } from './types';\n\nexport class RecorderManager extends EventTarget {\n public devices: MediaDeviceInfo[] = [];\n\n public selectedDevice: string = '';\n\n public recordingState: RecordingState = 'stopped';\n\n private _mediaStream: MediaStream | null = null;\n\n private _audioService: AudioService | null = null;\n\n private _dictationService: DictationService | null = null;\n\n private _visualiserInterval?: number;\n\n async initialize() {\n const deviceResponse = await getAudioDevices();\n this.devices = deviceResponse.devices;\n this.selectedDevice = deviceResponse.defaultDeviceId || '';\n return deviceResponse;\n }\n\n async startRecording(params: {\n dictationConfig: DictationConfig;\n authToken: string;\n }) {\n this._updateRecordingState('initializing');\n try {\n this._mediaStream = await navigator.mediaDevices.getUserMedia({\n audio: { deviceId: this.selectedDevice },\n });\n this._audioService = new AudioService(this._mediaStream);\n this._dictationService = new DictationService(this._mediaStream, params);\n\n // Forward custom events from dictation service\n this._dictationService.addEventListener('error', e =>\n this.dispatchEvent(\n new CustomEvent('error', {\n detail: (e as CustomEvent).detail,\n bubbles: true,\n composed: true,\n }),\n ),\n );\n this._dictationService.addEventListener('stream-closed', () =>\n this.stopRecording(),\n );\n this._dictationService.addEventListener('transcript', e =>\n this.dispatchEvent(\n new CustomEvent('transcript', {\n detail: (e as CustomEvent).detail,\n bubbles: true,\n composed: true,\n }),\n ),\n );\n } catch (error) {\n this.dispatchEvent(\n new CustomEvent('error', {\n detail: error,\n bubbles: true,\n composed: true,\n }),\n );\n this._updateRecordingState('stopped');\n return;\n }\n\n this._dictationService?.startRecording();\n this._updateRecordingState('recording');\n this._visualiserInterval = window.setInterval(() => {\n const level = this._audioService\n ? this._audioService.getAudioLevel() * 3\n : 0;\n this.dispatchEvent(\n new CustomEvent('audio-level-changed', {\n detail: { audioLevel: level },\n bubbles: true,\n composed: true,\n }),\n );\n }, 150);\n }\n\n async stopRecording() {\n this._updateRecordingState('stopping');\n if (this._visualiserInterval) {\n clearInterval(this._visualiserInterval);\n this._visualiserInterval = undefined;\n }\n if (this._mediaStream) {\n this._mediaStream.getTracks().forEach(track => track.stop());\n this._mediaStream = null;\n }\n await this._dictationService?.stopRecording();\n this._updateRecordingState('stopped');\n }\n\n private _updateRecordingState(state: RecordingState) {\n this.recordingState = state;\n this.dispatchEvent(\n new CustomEvent('recording-state-changed', {\n detail: { state },\n bubbles: true,\n composed: true,\n }),\n );\n }\n}\n"]}
1
+ {"version":3,"file":"RecorderManager.js","sourceRoot":"","sources":["../src/RecorderManager.ts"],"names":[],"mappings":"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","sourcesContent":["import { getAudioDevices } from './utils.js';\nimport { AudioService } from './audioService.js';\nimport { DictationService } from './DictationService.js';\nimport type { DictationConfig, RecordingState } from './types.js';\n\nexport class RecorderManager extends EventTarget {\n public devices: MediaDeviceInfo[] = [];\n\n public selectedDevice: string = '';\n\n public recordingState: RecordingState = 'stopped';\n\n private _mediaStream: MediaStream | null = null;\n\n private _audioService: AudioService | null = null;\n\n private _dictationService: DictationService | null = null;\n\n private _visualiserInterval?: number;\n\n async initialize() {\n const deviceResponse = await getAudioDevices();\n this.devices = deviceResponse.devices;\n this.selectedDevice = deviceResponse.defaultDeviceId || '';\n return deviceResponse;\n }\n private dispatchCustomEvent(eventName: string, detail: unknown): void {\n this.dispatchEvent(\n new CustomEvent(eventName, {\n detail,\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n async startRecording(params: {\n dictationConfig: DictationConfig;\n authToken: string;\n }): Promise<void> {\n this._updateRecordingState('initializing');\n\n // Get media stream and initialize audio service.\n try {\n this._mediaStream = await navigator.mediaDevices.getUserMedia({\n audio: { deviceId: this.selectedDevice },\n });\n this._audioService = new AudioService(this._mediaStream);\n } catch (error) {\n this.dispatchCustomEvent('error', error);\n this._updateRecordingState('stopped');\n return;\n }\n\n // Initialize dictation service.\n try {\n this._dictationService = new DictationService(this._mediaStream, params);\n } catch (error) {\n this.dispatchCustomEvent('error', error);\n this._updateRecordingState('stopped');\n return;\n }\n\n // Forward dictation service events.\n this._dictationService.addEventListener('error', e =>\n this.dispatchCustomEvent('error', (e as CustomEvent).detail),\n );\n this._dictationService.addEventListener('stream-closed', () =>\n this.stopRecording(),\n );\n this._dictationService.addEventListener('transcript', e =>\n this.dispatchCustomEvent('transcript', (e as CustomEvent).detail),\n );\n\n this._dictationService.startRecording();\n this._updateRecordingState('recording');\n\n // Update audio level visualization.\n this._visualiserInterval = window.setInterval(() => {\n const level = this._audioService\n ? this._audioService.getAudioLevel() * 3\n : 0;\n this.dispatchCustomEvent('audio-level-changed', { audioLevel: level });\n }, 150);\n }\n\n async stopRecording() {\n this._updateRecordingState('stopping');\n if (this._visualiserInterval) {\n clearInterval(this._visualiserInterval);\n this._visualiserInterval = undefined;\n }\n if (this._mediaStream) {\n this._mediaStream.getTracks().forEach(track => track.stop());\n this._mediaStream = null;\n }\n await this._dictationService?.stopRecording();\n this._updateRecordingState('stopped');\n }\n\n private _updateRecordingState(state: RecordingState) {\n this.recordingState = state;\n this.dispatchEvent(\n new CustomEvent('recording-state-changed', {\n detail: { state },\n bubbles: true,\n composed: true,\n }),\n );\n }\n}\n"]}