@automattic/jetpack-ai-client 0.8.1 → 0.9.0
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/CHANGELOG.md +24 -0
- package/build/api-fetch/index.d.ts +2 -3
- package/build/api-fetch/index.js +5 -1
- package/build/audio-transcription/index.js +8 -5
- package/build/components/ai-control/index.d.ts +2 -2
- package/build/hooks/use-audio-transcription/index.js +34 -1
- package/build/hooks/use-audio-validation/index.d.ts +21 -0
- package/build/hooks/use-audio-validation/index.js +61 -0
- package/build/hooks/use-media-recording/index.d.ts +1 -5
- package/build/hooks/use-media-recording/index.js +32 -11
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/types.d.ts +2 -12
- package/package.json +13 -13
- package/src/api-fetch/index.ts +4 -3
- package/src/audio-transcription/index.ts +11 -19
- package/src/components/ai-control/index.tsx +1 -1
- package/src/hooks/use-audio-transcription/index.ts +59 -1
- package/src/hooks/use-audio-validation/index.ts +110 -0
- package/src/hooks/use-media-recording/index.ts +37 -18
- package/src/index.ts +1 -0
- package/src/types.ts +9 -16
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.9.0] - 2024-03-12
|
|
9
|
+
### Changed
|
|
10
|
+
- Fix typescript errors [#35904]
|
|
11
|
+
- Updated package dependencies. [#36325]
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- AI Client: Fix audio recording where WebM is not supported (iOS for example). [#36160]
|
|
15
|
+
|
|
16
|
+
## [0.8.2] - 2024-03-04
|
|
17
|
+
### Added
|
|
18
|
+
- AI Client: add audio validation hook. [#36043]
|
|
19
|
+
- Voice to Content: Close audio stream on hook destruction [#36086]
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- AI Client: change loading and error state handling on media recording hook. [#36001]
|
|
23
|
+
- AI Client: publish audio information on the validation success callback of the audio validation hook. [#36094]
|
|
24
|
+
- Updated package dependencies. [#36095]
|
|
25
|
+
- Updated package dependencies. [#36143]
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- AI Client: fixed transcription request from P2 editor [#36081]
|
|
29
|
+
|
|
8
30
|
## [0.8.1] - 2024-02-27
|
|
9
31
|
### Changed
|
|
10
32
|
- AI Client: support audio transcription and transcription post-processing canceling. [#35923]
|
|
@@ -233,6 +255,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
233
255
|
- Updated package dependencies. [#31659]
|
|
234
256
|
- Updated package dependencies. [#31785]
|
|
235
257
|
|
|
258
|
+
[0.9.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.8.2...v0.9.0
|
|
259
|
+
[0.8.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.8.1...v0.8.2
|
|
236
260
|
[0.8.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.8.0...v0.8.1
|
|
237
261
|
[0.8.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.7.0...v0.8.0
|
|
238
262
|
[0.7.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.6.1...v0.7.0
|
|
@@ -2,6 +2,5 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import apiFetchMod from '@wordpress/api-fetch';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export default apiFetch;
|
|
5
|
+
declare const _default: typeof apiFetchMod.default;
|
|
6
|
+
export default _default;
|
package/build/api-fetch/index.js
CHANGED
|
@@ -2,5 +2,9 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import apiFetchMod from '@wordpress/api-fetch';
|
|
5
|
-
|
|
5
|
+
// @wordpress/api-fetch (as of 6.47.0) declares itself in such a way that tsc and node see the function at apiFetchMod.default
|
|
6
|
+
// while some other environments (including code running inside WordPress itself) see it at apiFetch.
|
|
7
|
+
// See https://arethetypeswrong.github.io/?p=@wordpress/api-fetch@6.47.0
|
|
8
|
+
// This is a helper to simplify the usage of the api-fetch module on the ai-client package.
|
|
9
|
+
const apiFetch = 'default' in apiFetchMod ? apiFetchMod.default : apiFetchMod;
|
|
6
10
|
export default apiFetch;
|
|
@@ -5,7 +5,6 @@ import debugFactory from 'debug';
|
|
|
5
5
|
/**
|
|
6
6
|
* Internal dependencies
|
|
7
7
|
*/
|
|
8
|
-
import apiFetch from '../api-fetch/index.js';
|
|
9
8
|
import requestJwt from '../jwt/index.js';
|
|
10
9
|
const debug = debugFactory('jetpack-ai-client:audio-transcription');
|
|
11
10
|
/**
|
|
@@ -34,15 +33,19 @@ export default async function transcribeAudio(audio, feature, requestAbortSignal
|
|
|
34
33
|
const headers = {
|
|
35
34
|
Authorization: `Bearer ${token}`,
|
|
36
35
|
};
|
|
37
|
-
const
|
|
38
|
-
|
|
36
|
+
const URL = `https://public-api.wordpress.com/wpcom/v2/jetpack-ai-transcription${feature ? `?feature=${feature}` : ''}`;
|
|
37
|
+
return fetch(URL, {
|
|
39
38
|
method: 'POST',
|
|
40
39
|
body: formData,
|
|
41
40
|
headers,
|
|
42
41
|
signal: requestAbortSignal ?? undefined,
|
|
42
|
+
}).then(response => {
|
|
43
|
+
debug('Transcription response: %o', response);
|
|
44
|
+
if (response.ok) {
|
|
45
|
+
return response.json().then(data => data?.text);
|
|
46
|
+
}
|
|
47
|
+
return response.json().then(data => Promise.reject(data));
|
|
43
48
|
});
|
|
44
|
-
debug('Transcription response: %o', response);
|
|
45
|
-
return response.text;
|
|
46
49
|
}
|
|
47
50
|
catch (error) {
|
|
48
51
|
debug('Transcription error response: %o', error);
|
|
@@ -35,6 +35,6 @@ type AiControlProps = {
|
|
|
35
35
|
* @param {React.MutableRefObject} ref - Ref to the component.
|
|
36
36
|
* @returns {ReactElement} Rendered component.
|
|
37
37
|
*/
|
|
38
|
-
export declare function AIControl({ disabled, value, placeholder, showAccept, acceptLabel, showButtonLabels, isTransparent, state, showGuideLine, customFooter, onChange, onSend, onStop, onAccept, onDiscard, showRemove, bannerComponent, errorComponent, }: AiControlProps, ref: React.MutableRefObject<
|
|
39
|
-
declare const _default: React.ForwardRefExoticComponent<AiControlProps & React.RefAttributes<
|
|
38
|
+
export declare function AIControl({ disabled, value, placeholder, showAccept, acceptLabel, showButtonLabels, isTransparent, state, showGuideLine, customFooter, onChange, onSend, onStop, onAccept, onDiscard, showRemove, bannerComponent, errorComponent, }: AiControlProps, ref: React.MutableRefObject<HTMLInputElement>): ReactElement;
|
|
39
|
+
declare const _default: React.ForwardRefExoticComponent<AiControlProps & React.RefAttributes<HTMLInputElement>>;
|
|
40
40
|
export default _default;
|
|
@@ -2,12 +2,45 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { useCallback, useState, useRef } from '@wordpress/element';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
5
6
|
import debugFactory from 'debug';
|
|
6
7
|
/**
|
|
7
8
|
* Internal dependencies
|
|
8
9
|
*/
|
|
9
10
|
import transcribeAudio from '../../audio-transcription/index.js';
|
|
10
11
|
const debug = debugFactory('jetpack-ai-client:use-audio-transcription');
|
|
12
|
+
/**
|
|
13
|
+
* Map error response to a string.
|
|
14
|
+
* @param {Error | string | AudioTranscriptionErrorResponse} error - The error response from the audio transcription service.
|
|
15
|
+
* @returns {string} the translated error message
|
|
16
|
+
*/
|
|
17
|
+
const mapErrorResponse = (error) => {
|
|
18
|
+
if (typeof error === 'string') {
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
if ('code' in error) {
|
|
22
|
+
switch (error.code) {
|
|
23
|
+
case 'error_quota_exceeded':
|
|
24
|
+
return __('You exceeded your current quota, please check your plan details.', 'jetpack-ai-client');
|
|
25
|
+
case 'jetpack_ai_missing_audio_param':
|
|
26
|
+
return __('The audio_file is required to perform a transcription.', 'jetpack-ai-client');
|
|
27
|
+
case 'jetpack_ai_service_unavailable':
|
|
28
|
+
return __('The Jetpack AI service is temporarily unavailable.', 'jetpack-ai-client');
|
|
29
|
+
case 'file_size_not_supported':
|
|
30
|
+
return __('The provided audio file is too big.', 'jetpack-ai-client');
|
|
31
|
+
case 'file_type_not_supported':
|
|
32
|
+
return __('The provided audio file type is not supported.', 'jetpack-ai-client');
|
|
33
|
+
case 'jetpack_ai_error':
|
|
34
|
+
return __('There was an error processing the transcription request.', 'jetpack-ai-client');
|
|
35
|
+
default:
|
|
36
|
+
return error.message;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if ('message' in error) {
|
|
40
|
+
return error.message;
|
|
41
|
+
}
|
|
42
|
+
return __('There was an error processing the transcription request.', 'jetpack-ai-client');
|
|
43
|
+
};
|
|
11
44
|
/**
|
|
12
45
|
* A hook to handle audio transcription.
|
|
13
46
|
*
|
|
@@ -43,7 +76,7 @@ export default function useAudioTranscription({ feature, onReady, onError, }) {
|
|
|
43
76
|
.catch(error => {
|
|
44
77
|
if (!controller.signal.aborted) {
|
|
45
78
|
setTranscriptionError(error.message);
|
|
46
|
-
onError?.(error
|
|
79
|
+
onError?.(mapErrorResponse(error));
|
|
47
80
|
}
|
|
48
81
|
})
|
|
49
82
|
.finally(() => setIsTranscribingAudio(false));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The return value for the audio validation hook.
|
|
3
|
+
*/
|
|
4
|
+
export type UseAudioValidationReturn = {
|
|
5
|
+
isValidatingAudio: boolean;
|
|
6
|
+
validateAudio: (audio: Blob, successCallback: (info?: ValidatedAudioInformation) => void, errorCallback: (error: string) => void) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The validated audio information.
|
|
10
|
+
*/
|
|
11
|
+
export type ValidatedAudioInformation = {
|
|
12
|
+
duration: number;
|
|
13
|
+
isFile: boolean;
|
|
14
|
+
size: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Hook to handle the validation of an audio file.
|
|
18
|
+
*
|
|
19
|
+
* @returns {UseAudioValidationReturn} - Object with the audio validation state and the function to validate the audio.
|
|
20
|
+
*/
|
|
21
|
+
export default function useAudioValidation(): UseAudioValidationReturn;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useCallback, useState } from '@wordpress/element';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
const MAX_AUDIO_SIZE = 25000000; // 25MB
|
|
7
|
+
const MAX_AUDIO_DURATION = 25 * 60; // 25 minutes
|
|
8
|
+
const ALLOWED_MEDIA_TYPES = [
|
|
9
|
+
'audio/mpeg',
|
|
10
|
+
'audio/mp3',
|
|
11
|
+
'audio/ogg',
|
|
12
|
+
'audio/flac',
|
|
13
|
+
'audio/x-flac',
|
|
14
|
+
'audio/m4a',
|
|
15
|
+
'audio/x-m4a',
|
|
16
|
+
'audio/mp4',
|
|
17
|
+
'audio/wav',
|
|
18
|
+
'audio/wave',
|
|
19
|
+
'audio/x-wav',
|
|
20
|
+
'audio/webm',
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Hook to handle the validation of an audio file.
|
|
24
|
+
*
|
|
25
|
+
* @returns {UseAudioValidationReturn} - Object with the audio validation state and the function to validate the audio.
|
|
26
|
+
*/
|
|
27
|
+
export default function useAudioValidation() {
|
|
28
|
+
const [isValidatingAudio, setIsValidatingAudio] = useState(false);
|
|
29
|
+
const validateAudio = useCallback((audio, successCallback, errorCallback) => {
|
|
30
|
+
setIsValidatingAudio(true);
|
|
31
|
+
// Check if the audio file is too large
|
|
32
|
+
if (audio?.size > MAX_AUDIO_SIZE) {
|
|
33
|
+
setIsValidatingAudio(false);
|
|
34
|
+
return errorCallback(__('The audio file is too large. The maximum file size is 25MB.', 'jetpack-ai-client'));
|
|
35
|
+
}
|
|
36
|
+
// When it's a file, check the media type
|
|
37
|
+
const isFile = audio instanceof File;
|
|
38
|
+
if (isFile) {
|
|
39
|
+
if (!ALLOWED_MEDIA_TYPES.includes(audio.type)) {
|
|
40
|
+
setIsValidatingAudio(false);
|
|
41
|
+
return errorCallback(__('The audio file type is not supported. Please use a supported audio file type.', 'jetpack-ai-client'));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Check the duration of the audio
|
|
45
|
+
const audioContext = new AudioContext();
|
|
46
|
+
// Map blob to an array buffer
|
|
47
|
+
audio.arrayBuffer().then(arrayBuffer => {
|
|
48
|
+
// Decode audio file data contained in an ArrayBuffer
|
|
49
|
+
audioContext.decodeAudioData(arrayBuffer, function (audioBuffer) {
|
|
50
|
+
const duration = Math.ceil(audioBuffer.duration);
|
|
51
|
+
if (duration > MAX_AUDIO_DURATION) {
|
|
52
|
+
setIsValidatingAudio(false);
|
|
53
|
+
return errorCallback(__('The audio file is too long. The maximum recording time is 25 minutes.', 'jetpack-ai-client'));
|
|
54
|
+
}
|
|
55
|
+
setIsValidatingAudio(false);
|
|
56
|
+
return successCallback({ duration, isFile, size: audio?.size });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}, [setIsValidatingAudio]);
|
|
60
|
+
return { isValidatingAudio, validateAudio };
|
|
61
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type RecordingState = 'inactive' | 'recording' | 'paused' | '
|
|
1
|
+
export type RecordingState = 'inactive' | 'recording' | 'paused' | 'error';
|
|
2
2
|
type UseMediaRecordingProps = {
|
|
3
3
|
onDone?: (blob: Blob) => void;
|
|
4
4
|
};
|
|
@@ -27,10 +27,6 @@ type UseMediaRecordingReturn = {
|
|
|
27
27
|
* The error handler
|
|
28
28
|
*/
|
|
29
29
|
onError: (err: string | Error) => void;
|
|
30
|
-
/**
|
|
31
|
-
* The processing handler
|
|
32
|
-
*/
|
|
33
|
-
onProcessing: () => void;
|
|
34
30
|
controls: {
|
|
35
31
|
/**
|
|
36
32
|
* `start` recording handler
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { useRef, useState, useEffect, useCallback } from '@wordpress/element';
|
|
5
|
+
/**
|
|
6
|
+
* Media types
|
|
7
|
+
*/
|
|
8
|
+
const MEDIA_TYPE_MP4_MP4A = 'audio/mp4;codecs=mp4a';
|
|
9
|
+
const MEDIA_TYPE_MP4 = 'audio/mp4';
|
|
10
|
+
const MEDIA_TYPE_WEBM = 'audio/webm';
|
|
5
11
|
/**
|
|
6
12
|
* react custom hook to handle media recording.
|
|
7
13
|
*
|
|
@@ -11,7 +17,7 @@ import { useRef, useState, useEffect, useCallback } from '@wordpress/element';
|
|
|
11
17
|
export default function useMediaRecording({ onDone, } = {}) {
|
|
12
18
|
// Reference to the media recorder instance
|
|
13
19
|
const mediaRecordRef = useRef(null);
|
|
14
|
-
// Recording state: `inactive`, `recording`, `paused`, `
|
|
20
|
+
// Recording state: `inactive`, `recording`, `paused`, `error`
|
|
15
21
|
const [state, setState] = useState('inactive');
|
|
16
22
|
// reference to the paused state to be used in the `onDataAvailable` event listener,
|
|
17
23
|
// as the `mediaRecordRef.current.state` is already `inactive` when the recorder is stopped,
|
|
@@ -19,6 +25,7 @@ export default function useMediaRecording({ onDone, } = {}) {
|
|
|
19
25
|
const isPaused = useRef(false);
|
|
20
26
|
const recordStartTimestamp = useRef(0);
|
|
21
27
|
const [duration, setDuration] = useState(0);
|
|
28
|
+
const audioStream = useRef(null);
|
|
22
29
|
// The recorded blob
|
|
23
30
|
const [blob, setBlob] = useState(null);
|
|
24
31
|
// Store the recorded chunks
|
|
@@ -31,9 +38,10 @@ export default function useMediaRecording({ onDone, } = {}) {
|
|
|
31
38
|
* @returns {Blob} The recorded blob
|
|
32
39
|
*/
|
|
33
40
|
function getBlob() {
|
|
34
|
-
|
|
35
|
-
type:
|
|
36
|
-
}
|
|
41
|
+
if (MediaRecorder.isTypeSupported(MEDIA_TYPE_MP4_MP4A)) {
|
|
42
|
+
return new Blob(recordedChunks, { type: MEDIA_TYPE_MP4 }); // omit the codecs parameter
|
|
43
|
+
}
|
|
44
|
+
return new Blob(recordedChunks, { type: MEDIA_TYPE_WEBM });
|
|
37
45
|
}
|
|
38
46
|
// `start` recording handler
|
|
39
47
|
const start = useCallback((timeslice) => {
|
|
@@ -111,9 +119,18 @@ export default function useMediaRecording({ onDone, } = {}) {
|
|
|
111
119
|
navigator.mediaDevices
|
|
112
120
|
.getUserMedia(constraints)
|
|
113
121
|
.then(stream => {
|
|
122
|
+
audioStream.current = stream;
|
|
114
123
|
const source = audioCtx.createMediaStreamSource(stream);
|
|
115
124
|
source.connect(analyser.current);
|
|
116
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Special handling for iOS devices.
|
|
127
|
+
*/
|
|
128
|
+
if (MediaRecorder.isTypeSupported(MEDIA_TYPE_MP4_MP4A)) {
|
|
129
|
+
mediaRecordRef.current = new MediaRecorder(stream, { mimeType: MEDIA_TYPE_MP4_MP4A });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
mediaRecordRef.current = new MediaRecorder(stream, { mimeType: MEDIA_TYPE_WEBM });
|
|
133
|
+
}
|
|
117
134
|
mediaRecordRef.current.addEventListener('start', onStartListener);
|
|
118
135
|
mediaRecordRef.current.addEventListener('stop', onStopListener);
|
|
119
136
|
mediaRecordRef.current.addEventListener('pause', onPauseListener);
|
|
@@ -131,10 +148,6 @@ export default function useMediaRecording({ onDone, } = {}) {
|
|
|
131
148
|
setError(typeof err === 'string' ? err : err.message);
|
|
132
149
|
setState('error');
|
|
133
150
|
}, []);
|
|
134
|
-
// manually set the state to `processing` for the file upload case
|
|
135
|
-
const onProcessing = useCallback(() => {
|
|
136
|
-
setState('processing');
|
|
137
|
-
}, []);
|
|
138
151
|
/**
|
|
139
152
|
* `start` event listener for the media recorder instance.
|
|
140
153
|
*/
|
|
@@ -148,7 +161,6 @@ export default function useMediaRecording({ onDone, } = {}) {
|
|
|
148
161
|
* @returns {void}
|
|
149
162
|
*/
|
|
150
163
|
function onStopListener() {
|
|
151
|
-
setState('processing');
|
|
152
164
|
const lastBlob = getBlob();
|
|
153
165
|
onDone?.(lastBlob);
|
|
154
166
|
// Clear the recorded chunks
|
|
@@ -192,10 +204,20 @@ export default function useMediaRecording({ onDone, } = {}) {
|
|
|
192
204
|
});
|
|
193
205
|
}
|
|
194
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Close the audio stream
|
|
209
|
+
*/
|
|
210
|
+
function closeStream() {
|
|
211
|
+
if (audioStream.current) {
|
|
212
|
+
const tracks = audioStream.current.getTracks();
|
|
213
|
+
tracks.forEach(track => track.stop());
|
|
214
|
+
}
|
|
215
|
+
}
|
|
195
216
|
// Remove listeners and clear the recorded chunks
|
|
196
217
|
useEffect(() => {
|
|
197
218
|
reset();
|
|
198
219
|
return () => {
|
|
220
|
+
closeStream();
|
|
199
221
|
clearListeners();
|
|
200
222
|
};
|
|
201
223
|
}, []);
|
|
@@ -206,7 +228,6 @@ export default function useMediaRecording({ onDone, } = {}) {
|
|
|
206
228
|
duration,
|
|
207
229
|
analyser: analyser.current,
|
|
208
230
|
onError,
|
|
209
|
-
onProcessing,
|
|
210
231
|
controls: {
|
|
211
232
|
start,
|
|
212
233
|
pause,
|
package/build/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { default as useAiSuggestions } from './hooks/use-ai-suggestions/index.js
|
|
|
6
6
|
export { default as useMediaRecording } from './hooks/use-media-recording/index.js';
|
|
7
7
|
export { default as useAudioTranscription } from './hooks/use-audio-transcription/index.js';
|
|
8
8
|
export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
|
|
9
|
+
export { default as useAudioValidation } from './hooks/use-audio-validation/index.js';
|
|
9
10
|
export * from './icons/index.js';
|
|
10
11
|
export * from './components/index.js';
|
|
11
12
|
export * from './data-flow/index.js';
|
package/build/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { default as useAiSuggestions } from './hooks/use-ai-suggestions/index.js
|
|
|
12
12
|
export { default as useMediaRecording } from './hooks/use-media-recording/index.js';
|
|
13
13
|
export { default as useAudioTranscription } from './hooks/use-audio-transcription/index.js';
|
|
14
14
|
export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
|
|
15
|
+
export { default as useAudioValidation } from './hooks/use-audio-validation/index.js';
|
|
15
16
|
/*
|
|
16
17
|
* Components: Icons
|
|
17
18
|
*/
|
package/build/types.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type { UseAiContextOptions } from './data-flow/use-ai-context.js';
|
|
|
17
17
|
export type { RequestingErrorProps } from './hooks/use-ai-suggestions/index.js';
|
|
18
18
|
export type { UseAudioTranscriptionProps, UseAudioTranscriptionReturn, } from './hooks/use-audio-transcription/index.js';
|
|
19
19
|
export type { UseTranscriptionPostProcessingProps, UseTranscriptionPostProcessingReturn, PostProcessingAction, } from './hooks/use-transcription-post-processing/index.js';
|
|
20
|
+
export type { UseAudioValidationReturn, ValidatedAudioInformation, } from './hooks/use-audio-validation/index.js';
|
|
20
21
|
export { TRANSCRIPTION_POST_PROCESSING_ACTION_SIMPLE_DRAFT } from './hooks/use-transcription-post-processing/index.js';
|
|
21
22
|
export declare const REQUESTING_STATES: readonly ["init", "requesting", "suggesting", "done", "error"];
|
|
22
23
|
export type RequestingStateProp = (typeof REQUESTING_STATES)[number];
|
|
@@ -27,15 +28,4 @@ export type { RecordingState } from './hooks/use-media-recording/index.js';
|
|
|
27
28
|
export type CancelablePromise<T = void> = Promise<T> & {
|
|
28
29
|
canceled?: boolean;
|
|
29
30
|
};
|
|
30
|
-
|
|
31
|
-
apiNonce: string;
|
|
32
|
-
siteSuffix: string;
|
|
33
|
-
connectionStatus: {
|
|
34
|
-
isActive: boolean;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
declare global {
|
|
38
|
-
interface Window {
|
|
39
|
-
JP_CONNECTION_INITIAL_STATE: JPConnectionInitialState;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
31
|
+
export type TranscriptionState = RecordingState | 'validating' | 'processing' | 'error';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"private": false,
|
|
3
3
|
"name": "@automattic/jetpack-ai-client",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"description": "A JS client for consuming Jetpack AI services",
|
|
6
6
|
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/ai-client/#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -38,19 +38,19 @@
|
|
|
38
38
|
"main": "./build/index.js",
|
|
39
39
|
"types": "./build/index.d.ts",
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@automattic/jetpack-base-styles": "^0.6.
|
|
42
|
-
"@automattic/jetpack-connection": "^0.
|
|
43
|
-
"@automattic/jetpack-shared-extension-utils": "^0.14.
|
|
41
|
+
"@automattic/jetpack-base-styles": "^0.6.19",
|
|
42
|
+
"@automattic/jetpack-connection": "^0.33.3",
|
|
43
|
+
"@automattic/jetpack-shared-extension-utils": "^0.14.7",
|
|
44
44
|
"@microsoft/fetch-event-source": "2.0.1",
|
|
45
|
-
"@types/react": "18.2.
|
|
46
|
-
"@wordpress/api-fetch": "6.
|
|
47
|
-
"@wordpress/block-editor": "12.
|
|
48
|
-
"@wordpress/components": "
|
|
49
|
-
"@wordpress/compose": "6.
|
|
50
|
-
"@wordpress/data": "9.
|
|
51
|
-
"@wordpress/element": "5.
|
|
52
|
-
"@wordpress/i18n": "4.
|
|
53
|
-
"@wordpress/icons": "9.
|
|
45
|
+
"@types/react": "18.2.61",
|
|
46
|
+
"@wordpress/api-fetch": "6.50.0",
|
|
47
|
+
"@wordpress/block-editor": "12.21.0",
|
|
48
|
+
"@wordpress/components": "27.1.0",
|
|
49
|
+
"@wordpress/compose": "6.30.0",
|
|
50
|
+
"@wordpress/data": "9.23.0",
|
|
51
|
+
"@wordpress/element": "5.30.0",
|
|
52
|
+
"@wordpress/i18n": "4.53.0",
|
|
53
|
+
"@wordpress/icons": "9.44.0",
|
|
54
54
|
"classnames": "2.3.2",
|
|
55
55
|
"debug": "4.3.4",
|
|
56
56
|
"react": "18.2.0",
|
package/src/api-fetch/index.ts
CHANGED
|
@@ -7,7 +7,8 @@ import apiFetchMod from '@wordpress/api-fetch';
|
|
|
7
7
|
// while some other environments (including code running inside WordPress itself) see it at apiFetch.
|
|
8
8
|
// See https://arethetypeswrong.github.io/?p=@wordpress/api-fetch@6.47.0
|
|
9
9
|
// This is a helper to simplify the usage of the api-fetch module on the ai-client package.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const apiFetch = 'default' in apiFetchMod ? apiFetchMod.default : apiFetchMod;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
12
|
+
type ApiFetchType = typeof apiFetch extends Function ? typeof apiFetch : typeof apiFetchMod;
|
|
12
13
|
|
|
13
|
-
export default apiFetch;
|
|
14
|
+
export default apiFetch as ApiFetchType;
|
|
@@ -5,21 +5,10 @@ import debugFactory from 'debug';
|
|
|
5
5
|
/**
|
|
6
6
|
* Internal dependencies
|
|
7
7
|
*/
|
|
8
|
-
import apiFetch from '../api-fetch/index.js';
|
|
9
8
|
import requestJwt from '../jwt/index.js';
|
|
10
9
|
|
|
11
10
|
const debug = debugFactory( 'jetpack-ai-client:audio-transcription' );
|
|
12
11
|
|
|
13
|
-
/**
|
|
14
|
-
* The response from the audio transcription service.
|
|
15
|
-
*/
|
|
16
|
-
type AudioTranscriptionResponse = {
|
|
17
|
-
/**
|
|
18
|
-
* The transcribed text.
|
|
19
|
-
*/
|
|
20
|
-
text: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
12
|
/**
|
|
24
13
|
* A function that takes an audio blob and transcribes it.
|
|
25
14
|
*
|
|
@@ -53,19 +42,22 @@ export default async function transcribeAudio(
|
|
|
53
42
|
Authorization: `Bearer ${ token }`,
|
|
54
43
|
};
|
|
55
44
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
const URL = `https://public-api.wordpress.com/wpcom/v2/jetpack-ai-transcription${
|
|
46
|
+
feature ? `?feature=${ feature }` : ''
|
|
47
|
+
}`;
|
|
48
|
+
|
|
49
|
+
return fetch( URL, {
|
|
60
50
|
method: 'POST',
|
|
61
51
|
body: formData,
|
|
62
52
|
headers,
|
|
63
53
|
signal: requestAbortSignal ?? undefined,
|
|
54
|
+
} ).then( response => {
|
|
55
|
+
debug( 'Transcription response: %o', response );
|
|
56
|
+
if ( response.ok ) {
|
|
57
|
+
return response.json().then( data => data?.text );
|
|
58
|
+
}
|
|
59
|
+
return response.json().then( data => Promise.reject( data ) );
|
|
64
60
|
} );
|
|
65
|
-
|
|
66
|
-
debug( 'Transcription response: %o', response );
|
|
67
|
-
|
|
68
|
-
return response.text;
|
|
69
61
|
} catch ( error ) {
|
|
70
62
|
debug( 'Transcription error response: %o', error );
|
|
71
63
|
return Promise.reject( error );
|
|
@@ -73,7 +73,7 @@ export function AIControl(
|
|
|
73
73
|
bannerComponent = null,
|
|
74
74
|
errorComponent = null,
|
|
75
75
|
}: AiControlProps,
|
|
76
|
-
ref: React.MutableRefObject<
|
|
76
|
+
ref: React.MutableRefObject< HTMLInputElement >
|
|
77
77
|
): ReactElement {
|
|
78
78
|
const promptUserInputRef = useRef( null );
|
|
79
79
|
const loading = state === 'requesting' || state === 'suggesting';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { useCallback, useState, useRef } from '@wordpress/element';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
5
6
|
import debugFactory from 'debug';
|
|
6
7
|
/**
|
|
7
8
|
* Internal dependencies
|
|
@@ -30,6 +31,63 @@ export type UseAudioTranscriptionProps = {
|
|
|
30
31
|
onError?: ( error: string ) => void;
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The error response from the audio transcription service.
|
|
36
|
+
*/
|
|
37
|
+
type AudioTranscriptionErrorResponse = {
|
|
38
|
+
/**
|
|
39
|
+
* The error message.
|
|
40
|
+
*/
|
|
41
|
+
message: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The error code.
|
|
45
|
+
*/
|
|
46
|
+
code: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Map error response to a string.
|
|
51
|
+
* @param {Error | string | AudioTranscriptionErrorResponse} error - The error response from the audio transcription service.
|
|
52
|
+
* @returns {string} the translated error message
|
|
53
|
+
*/
|
|
54
|
+
const mapErrorResponse = ( error: Error | string | AudioTranscriptionErrorResponse ): string => {
|
|
55
|
+
if ( typeof error === 'string' ) {
|
|
56
|
+
return error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ( 'code' in error ) {
|
|
60
|
+
switch ( error.code ) {
|
|
61
|
+
case 'error_quota_exceeded':
|
|
62
|
+
return __(
|
|
63
|
+
'You exceeded your current quota, please check your plan details.',
|
|
64
|
+
'jetpack-ai-client'
|
|
65
|
+
);
|
|
66
|
+
case 'jetpack_ai_missing_audio_param':
|
|
67
|
+
return __( 'The audio_file is required to perform a transcription.', 'jetpack-ai-client' );
|
|
68
|
+
case 'jetpack_ai_service_unavailable':
|
|
69
|
+
return __( 'The Jetpack AI service is temporarily unavailable.', 'jetpack-ai-client' );
|
|
70
|
+
case 'file_size_not_supported':
|
|
71
|
+
return __( 'The provided audio file is too big.', 'jetpack-ai-client' );
|
|
72
|
+
case 'file_type_not_supported':
|
|
73
|
+
return __( 'The provided audio file type is not supported.', 'jetpack-ai-client' );
|
|
74
|
+
case 'jetpack_ai_error':
|
|
75
|
+
return __(
|
|
76
|
+
'There was an error processing the transcription request.',
|
|
77
|
+
'jetpack-ai-client'
|
|
78
|
+
);
|
|
79
|
+
default:
|
|
80
|
+
return error.message;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ( 'message' in error ) {
|
|
85
|
+
return error.message;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return __( 'There was an error processing the transcription request.', 'jetpack-ai-client' );
|
|
89
|
+
};
|
|
90
|
+
|
|
33
91
|
/**
|
|
34
92
|
* A hook to handle audio transcription.
|
|
35
93
|
*
|
|
@@ -74,7 +132,7 @@ export default function useAudioTranscription( {
|
|
|
74
132
|
.catch( error => {
|
|
75
133
|
if ( ! controller.signal.aborted ) {
|
|
76
134
|
setTranscriptionError( error.message );
|
|
77
|
-
onError?.( error
|
|
135
|
+
onError?.( mapErrorResponse( error ) );
|
|
78
136
|
}
|
|
79
137
|
} )
|
|
80
138
|
.finally( () => setIsTranscribingAudio( false ) );
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useCallback, useState } from '@wordpress/element';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
const MAX_AUDIO_SIZE = 25000000; // 25MB
|
|
8
|
+
const MAX_AUDIO_DURATION = 25 * 60; // 25 minutes
|
|
9
|
+
const ALLOWED_MEDIA_TYPES = [
|
|
10
|
+
'audio/mpeg',
|
|
11
|
+
'audio/mp3',
|
|
12
|
+
'audio/ogg',
|
|
13
|
+
'audio/flac',
|
|
14
|
+
'audio/x-flac',
|
|
15
|
+
'audio/m4a',
|
|
16
|
+
'audio/x-m4a',
|
|
17
|
+
'audio/mp4',
|
|
18
|
+
'audio/wav',
|
|
19
|
+
'audio/wave',
|
|
20
|
+
'audio/x-wav',
|
|
21
|
+
'audio/webm',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The return value for the audio validation hook.
|
|
26
|
+
*/
|
|
27
|
+
export type UseAudioValidationReturn = {
|
|
28
|
+
isValidatingAudio: boolean;
|
|
29
|
+
validateAudio: (
|
|
30
|
+
audio: Blob,
|
|
31
|
+
successCallback: ( info?: ValidatedAudioInformation ) => void,
|
|
32
|
+
errorCallback: ( error: string ) => void
|
|
33
|
+
) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The validated audio information.
|
|
38
|
+
*/
|
|
39
|
+
export type ValidatedAudioInformation = {
|
|
40
|
+
duration: number;
|
|
41
|
+
isFile: boolean;
|
|
42
|
+
size: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hook to handle the validation of an audio file.
|
|
47
|
+
*
|
|
48
|
+
* @returns {UseAudioValidationReturn} - Object with the audio validation state and the function to validate the audio.
|
|
49
|
+
*/
|
|
50
|
+
export default function useAudioValidation(): UseAudioValidationReturn {
|
|
51
|
+
const [ isValidatingAudio, setIsValidatingAudio ] = useState< boolean >( false );
|
|
52
|
+
|
|
53
|
+
const validateAudio = useCallback(
|
|
54
|
+
(
|
|
55
|
+
audio: Blob,
|
|
56
|
+
successCallback: ( info?: ValidatedAudioInformation ) => void,
|
|
57
|
+
errorCallback: ( error: string ) => void
|
|
58
|
+
) => {
|
|
59
|
+
setIsValidatingAudio( true );
|
|
60
|
+
|
|
61
|
+
// Check if the audio file is too large
|
|
62
|
+
if ( audio?.size > MAX_AUDIO_SIZE ) {
|
|
63
|
+
setIsValidatingAudio( false );
|
|
64
|
+
return errorCallback(
|
|
65
|
+
__( 'The audio file is too large. The maximum file size is 25MB.', 'jetpack-ai-client' )
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// When it's a file, check the media type
|
|
70
|
+
const isFile = audio instanceof File;
|
|
71
|
+
if ( isFile ) {
|
|
72
|
+
if ( ! ALLOWED_MEDIA_TYPES.includes( audio.type ) ) {
|
|
73
|
+
setIsValidatingAudio( false );
|
|
74
|
+
return errorCallback(
|
|
75
|
+
__(
|
|
76
|
+
'The audio file type is not supported. Please use a supported audio file type.',
|
|
77
|
+
'jetpack-ai-client'
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check the duration of the audio
|
|
84
|
+
const audioContext = new AudioContext();
|
|
85
|
+
|
|
86
|
+
// Map blob to an array buffer
|
|
87
|
+
audio.arrayBuffer().then( arrayBuffer => {
|
|
88
|
+
// Decode audio file data contained in an ArrayBuffer
|
|
89
|
+
audioContext.decodeAudioData( arrayBuffer, function ( audioBuffer ) {
|
|
90
|
+
const duration = Math.ceil( audioBuffer.duration );
|
|
91
|
+
|
|
92
|
+
if ( duration > MAX_AUDIO_DURATION ) {
|
|
93
|
+
setIsValidatingAudio( false );
|
|
94
|
+
return errorCallback(
|
|
95
|
+
__(
|
|
96
|
+
'The audio file is too long. The maximum recording time is 25 minutes.',
|
|
97
|
+
'jetpack-ai-client'
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
setIsValidatingAudio( false );
|
|
102
|
+
return successCallback( { duration, isFile, size: audio?.size } );
|
|
103
|
+
} );
|
|
104
|
+
} );
|
|
105
|
+
},
|
|
106
|
+
[ setIsValidatingAudio ]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return { isValidatingAudio, validateAudio };
|
|
110
|
+
}
|
|
@@ -5,11 +5,18 @@ import { useRef, useState, useEffect, useCallback } from '@wordpress/element';
|
|
|
5
5
|
/*
|
|
6
6
|
* Types
|
|
7
7
|
*/
|
|
8
|
-
export type RecordingState = 'inactive' | 'recording' | 'paused' | '
|
|
8
|
+
export type RecordingState = 'inactive' | 'recording' | 'paused' | 'error';
|
|
9
9
|
type UseMediaRecordingProps = {
|
|
10
10
|
onDone?: ( blob: Blob ) => void;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Media types
|
|
15
|
+
*/
|
|
16
|
+
const MEDIA_TYPE_MP4_MP4A = 'audio/mp4;codecs=mp4a';
|
|
17
|
+
const MEDIA_TYPE_MP4 = 'audio/mp4';
|
|
18
|
+
const MEDIA_TYPE_WEBM = 'audio/webm';
|
|
19
|
+
|
|
13
20
|
type UseMediaRecordingReturn = {
|
|
14
21
|
/**
|
|
15
22
|
* The current recording state
|
|
@@ -41,11 +48,6 @@ type UseMediaRecordingReturn = {
|
|
|
41
48
|
*/
|
|
42
49
|
onError: ( err: string | Error ) => void;
|
|
43
50
|
|
|
44
|
-
/**
|
|
45
|
-
* The processing handler
|
|
46
|
-
*/
|
|
47
|
-
onProcessing: () => void;
|
|
48
|
-
|
|
49
51
|
controls: {
|
|
50
52
|
/**
|
|
51
53
|
* `start` recording handler
|
|
@@ -90,7 +92,7 @@ export default function useMediaRecording( {
|
|
|
90
92
|
// Reference to the media recorder instance
|
|
91
93
|
const mediaRecordRef = useRef( null );
|
|
92
94
|
|
|
93
|
-
// Recording state: `inactive`, `recording`, `paused`, `
|
|
95
|
+
// Recording state: `inactive`, `recording`, `paused`, `error`
|
|
94
96
|
const [ state, setState ] = useState< RecordingState >( 'inactive' );
|
|
95
97
|
|
|
96
98
|
// reference to the paused state to be used in the `onDataAvailable` event listener,
|
|
@@ -101,6 +103,8 @@ export default function useMediaRecording( {
|
|
|
101
103
|
const recordStartTimestamp = useRef< number >( 0 );
|
|
102
104
|
const [ duration, setDuration ] = useState< number >( 0 );
|
|
103
105
|
|
|
106
|
+
const audioStream = useRef< MediaStream | null >( null );
|
|
107
|
+
|
|
104
108
|
// The recorded blob
|
|
105
109
|
const [ blob, setBlob ] = useState< Blob | null >( null );
|
|
106
110
|
|
|
@@ -117,9 +121,11 @@ export default function useMediaRecording( {
|
|
|
117
121
|
* @returns {Blob} The recorded blob
|
|
118
122
|
*/
|
|
119
123
|
function getBlob() {
|
|
120
|
-
|
|
121
|
-
type:
|
|
122
|
-
}
|
|
124
|
+
if ( MediaRecorder.isTypeSupported( MEDIA_TYPE_MP4_MP4A ) ) {
|
|
125
|
+
return new Blob( recordedChunks, { type: MEDIA_TYPE_MP4 } ); // omit the codecs parameter
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Blob( recordedChunks, { type: MEDIA_TYPE_WEBM } );
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
// `start` recording handler
|
|
@@ -216,10 +222,19 @@ export default function useMediaRecording( {
|
|
|
216
222
|
navigator.mediaDevices
|
|
217
223
|
.getUserMedia( constraints )
|
|
218
224
|
.then( stream => {
|
|
225
|
+
audioStream.current = stream;
|
|
219
226
|
const source = audioCtx.createMediaStreamSource( stream );
|
|
220
227
|
source.connect( analyser.current );
|
|
221
228
|
|
|
222
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Special handling for iOS devices.
|
|
231
|
+
*/
|
|
232
|
+
if ( MediaRecorder.isTypeSupported( MEDIA_TYPE_MP4_MP4A ) ) {
|
|
233
|
+
mediaRecordRef.current = new MediaRecorder( stream, { mimeType: MEDIA_TYPE_MP4_MP4A } );
|
|
234
|
+
} else {
|
|
235
|
+
mediaRecordRef.current = new MediaRecorder( stream, { mimeType: MEDIA_TYPE_WEBM } );
|
|
236
|
+
}
|
|
237
|
+
|
|
223
238
|
mediaRecordRef.current.addEventListener( 'start', onStartListener );
|
|
224
239
|
mediaRecordRef.current.addEventListener( 'stop', onStopListener );
|
|
225
240
|
mediaRecordRef.current.addEventListener( 'pause', onPauseListener );
|
|
@@ -239,11 +254,6 @@ export default function useMediaRecording( {
|
|
|
239
254
|
setState( 'error' );
|
|
240
255
|
}, [] );
|
|
241
256
|
|
|
242
|
-
// manually set the state to `processing` for the file upload case
|
|
243
|
-
const onProcessing = useCallback( () => {
|
|
244
|
-
setState( 'processing' );
|
|
245
|
-
}, [] );
|
|
246
|
-
|
|
247
257
|
/**
|
|
248
258
|
* `start` event listener for the media recorder instance.
|
|
249
259
|
*/
|
|
@@ -258,7 +268,6 @@ export default function useMediaRecording( {
|
|
|
258
268
|
* @returns {void}
|
|
259
269
|
*/
|
|
260
270
|
function onStopListener(): void {
|
|
261
|
-
setState( 'processing' );
|
|
262
271
|
const lastBlob = getBlob();
|
|
263
272
|
onDone?.( lastBlob );
|
|
264
273
|
|
|
@@ -310,11 +319,22 @@ export default function useMediaRecording( {
|
|
|
310
319
|
}
|
|
311
320
|
}
|
|
312
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Close the audio stream
|
|
324
|
+
*/
|
|
325
|
+
function closeStream() {
|
|
326
|
+
if ( audioStream.current ) {
|
|
327
|
+
const tracks = audioStream.current.getTracks();
|
|
328
|
+
tracks.forEach( track => track.stop() );
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
313
332
|
// Remove listeners and clear the recorded chunks
|
|
314
333
|
useEffect( () => {
|
|
315
334
|
reset();
|
|
316
335
|
|
|
317
336
|
return () => {
|
|
337
|
+
closeStream();
|
|
318
338
|
clearListeners();
|
|
319
339
|
};
|
|
320
340
|
}, [] );
|
|
@@ -326,7 +346,6 @@ export default function useMediaRecording( {
|
|
|
326
346
|
duration,
|
|
327
347
|
analyser: analyser.current,
|
|
328
348
|
onError,
|
|
329
|
-
onProcessing,
|
|
330
349
|
|
|
331
350
|
controls: {
|
|
332
351
|
start,
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { default as useAiSuggestions } from './hooks/use-ai-suggestions/index.js
|
|
|
13
13
|
export { default as useMediaRecording } from './hooks/use-media-recording/index.js';
|
|
14
14
|
export { default as useAudioTranscription } from './hooks/use-audio-transcription/index.js';
|
|
15
15
|
export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
|
|
16
|
+
export { default as useAudioValidation } from './hooks/use-audio-validation/index.js';
|
|
16
17
|
|
|
17
18
|
/*
|
|
18
19
|
* Components: Icons
|
package/src/types.ts
CHANGED
|
@@ -46,6 +46,11 @@ export type {
|
|
|
46
46
|
UseTranscriptionPostProcessingReturn,
|
|
47
47
|
PostProcessingAction,
|
|
48
48
|
} from './hooks/use-transcription-post-processing/index.js';
|
|
49
|
+
export type {
|
|
50
|
+
UseAudioValidationReturn,
|
|
51
|
+
ValidatedAudioInformation,
|
|
52
|
+
} from './hooks/use-audio-validation/index.js';
|
|
53
|
+
|
|
49
54
|
/*
|
|
50
55
|
* Hook constants
|
|
51
56
|
*/
|
|
@@ -88,19 +93,7 @@ export type { RecordingState } from './hooks/use-media-recording/index.js';
|
|
|
88
93
|
*/
|
|
89
94
|
export type CancelablePromise< T = void > = Promise< T > & { canceled?: boolean };
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
siteSuffix: string;
|
|
96
|
-
connectionStatus: {
|
|
97
|
-
isActive: boolean;
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Global
|
|
102
|
-
declare global {
|
|
103
|
-
interface Window {
|
|
104
|
-
JP_CONNECTION_INITIAL_STATE: JPConnectionInitialState;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
96
|
+
/*
|
|
97
|
+
* Transcription types
|
|
98
|
+
*/
|
|
99
|
+
export type TranscriptionState = RecordingState | 'validating' | 'processing' | 'error';
|