@ai-sdk/revai 0.0.0-64aae7dd-20260114144918 → 0.0.0-98261322-20260122142521
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 +31 -4
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/160-revai.mdx +206 -0
- package/package.json +10 -5
- package/src/index.ts +3 -0
- package/src/revai-api-types.ts +274 -0
- package/src/revai-config.ts +9 -0
- package/src/revai-error.test.ts +34 -0
- package/src/revai-error.ts +16 -0
- package/src/revai-provider.ts +120 -0
- package/src/revai-transcription-model.test.ts +282 -0
- package/src/revai-transcription-model.ts +516 -0
- package/src/revai-transcription-options.ts +1 -0
- package/src/transcript-test.mp3 +0 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TranscriptionModelV3,
|
|
3
|
+
ProviderV3,
|
|
4
|
+
NoSuchModelError,
|
|
5
|
+
} from '@ai-sdk/provider';
|
|
6
|
+
import {
|
|
7
|
+
FetchFunction,
|
|
8
|
+
loadApiKey,
|
|
9
|
+
withUserAgentSuffix,
|
|
10
|
+
} from '@ai-sdk/provider-utils';
|
|
11
|
+
import { RevaiTranscriptionModel } from './revai-transcription-model';
|
|
12
|
+
import { RevaiTranscriptionModelId } from './revai-transcription-options';
|
|
13
|
+
import { VERSION } from './version';
|
|
14
|
+
|
|
15
|
+
export interface RevaiProvider extends ProviderV3 {
|
|
16
|
+
(
|
|
17
|
+
modelId: 'machine',
|
|
18
|
+
settings?: {},
|
|
19
|
+
): {
|
|
20
|
+
transcription: RevaiTranscriptionModel;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
Creates a model for transcription.
|
|
25
|
+
*/
|
|
26
|
+
transcription(modelId: RevaiTranscriptionModelId): TranscriptionModelV3;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @deprecated Use `embeddingModel` instead.
|
|
30
|
+
*/
|
|
31
|
+
textEmbeddingModel(modelId: string): never;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RevaiProviderSettings {
|
|
35
|
+
/**
|
|
36
|
+
API key for authenticating requests.
|
|
37
|
+
*/
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
Custom headers to include in the requests.
|
|
42
|
+
*/
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
Custom fetch implementation. You can use it as a middleware to intercept requests,
|
|
47
|
+
or to provide a custom fetch implementation for e.g. testing.
|
|
48
|
+
*/
|
|
49
|
+
fetch?: FetchFunction;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
Create a Rev.ai provider instance.
|
|
54
|
+
*/
|
|
55
|
+
export function createRevai(
|
|
56
|
+
options: RevaiProviderSettings = {},
|
|
57
|
+
): RevaiProvider {
|
|
58
|
+
const getHeaders = () =>
|
|
59
|
+
withUserAgentSuffix(
|
|
60
|
+
{
|
|
61
|
+
authorization: `Bearer ${loadApiKey({
|
|
62
|
+
apiKey: options.apiKey,
|
|
63
|
+
environmentVariableName: 'REVAI_API_KEY',
|
|
64
|
+
description: 'Rev.ai',
|
|
65
|
+
})}`,
|
|
66
|
+
...options.headers,
|
|
67
|
+
},
|
|
68
|
+
`ai-sdk/revai/${VERSION}`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const createTranscriptionModel = (modelId: RevaiTranscriptionModelId) =>
|
|
72
|
+
new RevaiTranscriptionModel(modelId, {
|
|
73
|
+
provider: `revai.transcription`,
|
|
74
|
+
url: ({ path }) => `https://api.rev.ai${path}`,
|
|
75
|
+
headers: getHeaders,
|
|
76
|
+
fetch: options.fetch,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const provider = function (modelId: RevaiTranscriptionModelId) {
|
|
80
|
+
return {
|
|
81
|
+
transcription: createTranscriptionModel(modelId),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
provider.specificationVersion = 'v3' as const;
|
|
86
|
+
provider.transcription = createTranscriptionModel;
|
|
87
|
+
provider.transcriptionModel = createTranscriptionModel;
|
|
88
|
+
|
|
89
|
+
provider.languageModel = () => {
|
|
90
|
+
throw new NoSuchModelError({
|
|
91
|
+
modelId: 'unknown',
|
|
92
|
+
modelType: 'languageModel',
|
|
93
|
+
message: 'Rev.ai does not provide language models',
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
provider.embeddingModel = () => {
|
|
98
|
+
throw new NoSuchModelError({
|
|
99
|
+
modelId: 'unknown',
|
|
100
|
+
modelType: 'embeddingModel',
|
|
101
|
+
message: 'Rev.ai does not provide text embedding models',
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
provider.textEmbeddingModel = provider.embeddingModel;
|
|
105
|
+
|
|
106
|
+
provider.imageModel = () => {
|
|
107
|
+
throw new NoSuchModelError({
|
|
108
|
+
modelId: 'unknown',
|
|
109
|
+
modelType: 'imageModel',
|
|
110
|
+
message: 'Rev.ai does not provide image models',
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return provider as RevaiProvider;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
Default Rev.ai provider instance.
|
|
119
|
+
*/
|
|
120
|
+
export const revai = createRevai();
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
|
|
2
|
+
import { RevaiTranscriptionModel } from './revai-transcription-model';
|
|
3
|
+
import { createRevai } from './revai-provider';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
vi.mock('./version', () => ({
|
|
9
|
+
VERSION: '0.0.0-test',
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3'));
|
|
13
|
+
const provider = createRevai({ apiKey: 'test-api-key' });
|
|
14
|
+
const model = provider.transcription('machine');
|
|
15
|
+
|
|
16
|
+
const server = createTestServer({
|
|
17
|
+
'https://api.rev.ai/speechtotext/v1/jobs': {},
|
|
18
|
+
'https://api.rev.ai/speechtotext/v1/jobs/test-id': {},
|
|
19
|
+
'https://api.rev.ai/speechtotext/v1/jobs/test-id/transcript': {},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('doGenerate', () => {
|
|
23
|
+
function prepareJsonResponse({
|
|
24
|
+
headers,
|
|
25
|
+
}: {
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
} = {}) {
|
|
28
|
+
server.urls['https://api.rev.ai/speechtotext/v1/jobs'].response = {
|
|
29
|
+
type: 'json-value',
|
|
30
|
+
headers,
|
|
31
|
+
body: {
|
|
32
|
+
id: 'test-id',
|
|
33
|
+
status: 'in_progress',
|
|
34
|
+
language: 'en',
|
|
35
|
+
created_on: '2018-05-05T23:23:22.29Z',
|
|
36
|
+
transcriber: 'machine',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
server.urls['https://api.rev.ai/speechtotext/v1/jobs/test-id'].response = {
|
|
40
|
+
type: 'json-value',
|
|
41
|
+
headers,
|
|
42
|
+
body: {
|
|
43
|
+
id: 'test-id',
|
|
44
|
+
status: 'transcribed',
|
|
45
|
+
language: 'en',
|
|
46
|
+
created_on: '2018-05-05T23:23:22.29Z',
|
|
47
|
+
transcriber: 'machine',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
server.urls[
|
|
51
|
+
'https://api.rev.ai/speechtotext/v1/jobs/test-id/transcript'
|
|
52
|
+
].response = {
|
|
53
|
+
type: 'json-value',
|
|
54
|
+
headers,
|
|
55
|
+
body: {
|
|
56
|
+
monologues: [
|
|
57
|
+
{
|
|
58
|
+
speaker: 1,
|
|
59
|
+
elements: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
value: 'Hello',
|
|
63
|
+
ts: 0.5,
|
|
64
|
+
end_ts: 1.5,
|
|
65
|
+
confidence: 1,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'punct',
|
|
69
|
+
value: ' ',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'text',
|
|
73
|
+
value: 'World',
|
|
74
|
+
ts: 1.75,
|
|
75
|
+
end_ts: 2.85,
|
|
76
|
+
confidence: 0.8,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'punct',
|
|
80
|
+
value: '.',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
speaker: 2,
|
|
86
|
+
elements: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
value: 'monologues',
|
|
90
|
+
ts: 3,
|
|
91
|
+
end_ts: 3.5,
|
|
92
|
+
confidence: 1,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'punct',
|
|
96
|
+
value: ' ',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
value: 'are',
|
|
101
|
+
ts: 3.6,
|
|
102
|
+
end_ts: 3.9,
|
|
103
|
+
confidence: 1,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: 'punct',
|
|
107
|
+
value: ' ',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
value: 'a',
|
|
112
|
+
ts: 4,
|
|
113
|
+
end_ts: 4.3,
|
|
114
|
+
confidence: 1,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: 'punct',
|
|
118
|
+
value: ' ',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: 'text',
|
|
122
|
+
value: 'block',
|
|
123
|
+
ts: 4.5,
|
|
124
|
+
end_ts: 5.5,
|
|
125
|
+
confidence: 1,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: 'punct',
|
|
129
|
+
value: ' ',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: 'text',
|
|
133
|
+
value: 'of',
|
|
134
|
+
ts: 5.75,
|
|
135
|
+
end_ts: 6.14,
|
|
136
|
+
confidence: 1,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'punct',
|
|
140
|
+
value: ' ',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'unknown',
|
|
144
|
+
value: '<inaudible>',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: 'punct',
|
|
148
|
+
value: ' ',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
value: 'text',
|
|
153
|
+
ts: 6.5,
|
|
154
|
+
end_ts: 7.78,
|
|
155
|
+
confidence: 1,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'punct',
|
|
159
|
+
value: '.',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
it('should pass the model', async () => {
|
|
169
|
+
prepareJsonResponse();
|
|
170
|
+
|
|
171
|
+
await model.doGenerate({
|
|
172
|
+
audio: audioData,
|
|
173
|
+
mediaType: 'audio/wav',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(await server.calls[0].requestBodyMultipart).toMatchObject({
|
|
177
|
+
media: expect.any(File),
|
|
178
|
+
config: '{"transcriber":"machine"}',
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should pass headers', async () => {
|
|
183
|
+
prepareJsonResponse();
|
|
184
|
+
|
|
185
|
+
const provider = createRevai({
|
|
186
|
+
apiKey: 'test-api-key',
|
|
187
|
+
headers: {
|
|
188
|
+
'Custom-Provider-Header': 'provider-header-value',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await provider.transcription('machine').doGenerate({
|
|
193
|
+
audio: audioData,
|
|
194
|
+
mediaType: 'audio/wav',
|
|
195
|
+
headers: {
|
|
196
|
+
'Custom-Request-Header': 'request-header-value',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(server.calls[0].requestHeaders).toMatchObject({
|
|
201
|
+
authorization: 'Bearer test-api-key',
|
|
202
|
+
'content-type': expect.stringMatching(
|
|
203
|
+
/^multipart\/form-data; boundary=----formdata-undici-\d+$/,
|
|
204
|
+
),
|
|
205
|
+
'custom-provider-header': 'provider-header-value',
|
|
206
|
+
'custom-request-header': 'request-header-value',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(server.calls[0].requestUserAgent).toContain(
|
|
210
|
+
`ai-sdk/revai/0.0.0-test`,
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should extract the transcription text', async () => {
|
|
215
|
+
prepareJsonResponse();
|
|
216
|
+
|
|
217
|
+
const result = await model.doGenerate({
|
|
218
|
+
audio: audioData,
|
|
219
|
+
mediaType: 'audio/wav',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.text).toBe(
|
|
223
|
+
'Hello World. monologues are a block of <inaudible> text.',
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should include response data with timestamp, modelId and headers', async () => {
|
|
228
|
+
prepareJsonResponse({
|
|
229
|
+
headers: {
|
|
230
|
+
'x-request-id': 'test-request-id',
|
|
231
|
+
'x-ratelimit-remaining': '123',
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const testDate = new Date(0);
|
|
236
|
+
const customModel = new RevaiTranscriptionModel('machine', {
|
|
237
|
+
provider: 'test-provider',
|
|
238
|
+
url: ({ path }) => `https://api.rev.ai${path}`,
|
|
239
|
+
headers: () => ({}),
|
|
240
|
+
_internal: {
|
|
241
|
+
currentDate: () => testDate,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const result = await customModel.doGenerate({
|
|
246
|
+
audio: audioData,
|
|
247
|
+
mediaType: 'audio/wav',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(result.response).toMatchObject({
|
|
251
|
+
timestamp: testDate,
|
|
252
|
+
modelId: 'machine',
|
|
253
|
+
headers: {
|
|
254
|
+
'content-type': 'application/json',
|
|
255
|
+
'x-request-id': 'test-request-id',
|
|
256
|
+
'x-ratelimit-remaining': '123',
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should use real date when no custom date provider is specified', async () => {
|
|
262
|
+
prepareJsonResponse();
|
|
263
|
+
|
|
264
|
+
const testDate = new Date(0);
|
|
265
|
+
const customModel = new RevaiTranscriptionModel('machine', {
|
|
266
|
+
provider: 'test-provider',
|
|
267
|
+
url: ({ path }) => `https://api.rev.ai${path}`,
|
|
268
|
+
headers: () => ({}),
|
|
269
|
+
_internal: {
|
|
270
|
+
currentDate: () => testDate,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const result = await customModel.doGenerate({
|
|
275
|
+
audio: audioData,
|
|
276
|
+
mediaType: 'audio/wav',
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result.response.timestamp.getTime()).toEqual(testDate.getTime());
|
|
280
|
+
expect(result.response.modelId).toBe('machine');
|
|
281
|
+
});
|
|
282
|
+
});
|