speechrevolutions 0.2.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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/cjs/client.js +786 -0
- package/dist/cjs/exceptions.js +77 -0
- package/dist/cjs/index.js +28 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/progress.js +130 -0
- package/dist/cjs/sse.js +56 -0
- package/dist/cjs/transcript.js +303 -0
- package/dist/cjs/types.js +31 -0
- package/dist/cjs/upload.js +44 -0
- package/dist/esm/client.d.ts +94 -0
- package/dist/esm/client.js +748 -0
- package/dist/esm/exceptions.d.ts +46 -0
- package/dist/esm/exceptions.js +66 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/progress.d.ts +58 -0
- package/dist/esm/progress.js +125 -0
- package/dist/esm/sse.d.ts +7 -0
- package/dist/esm/sse.js +53 -0
- package/dist/esm/transcript.d.ts +51 -0
- package/dist/esm/transcript.js +267 -0
- package/dist/esm/types.d.ts +73 -0
- package/dist/esm/types.js +26 -0
- package/dist/esm/upload.d.ts +26 -0
- package/dist/esm/upload.js +39 -0
- package/package.json +57 -0
- package/src/client.ts +950 -0
- package/src/exceptions.ts +92 -0
- package/src/index.ts +20 -0
- package/src/progress.ts +147 -0
- package/src/sse.ts +61 -0
- package/src/transcript.ts +334 -0
- package/src/types.ts +114 -0
- package/src/upload.ts +50 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** SDK exception hierarchy.
|
|
3
|
+
*
|
|
4
|
+
* Every error carries the HTTP `statusCode` and the server `requestId` (from the
|
|
5
|
+
* response headers, when present) so failures can be correlated with server
|
|
6
|
+
* logs. `RateLimitError` also exposes `retryAfter` seconds.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.APIError = exports.TimeoutError = exports.UploadError = exports.JobFailedError = exports.JobNotFoundError = exports.RateLimitError = exports.AuthenticationError = exports.STTError = void 0;
|
|
10
|
+
class STTError extends Error {
|
|
11
|
+
statusCode;
|
|
12
|
+
requestId;
|
|
13
|
+
body;
|
|
14
|
+
constructor(message, opts) {
|
|
15
|
+
super(opts?.requestId ? `${message} (request_id=${opts.requestId})` : message);
|
|
16
|
+
this.name = "STTError";
|
|
17
|
+
this.statusCode = opts?.statusCode;
|
|
18
|
+
this.requestId = opts?.requestId;
|
|
19
|
+
this.body = opts?.body;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.STTError = STTError;
|
|
23
|
+
class AuthenticationError extends STTError {
|
|
24
|
+
constructor(message = "Unauthorized — check your API key", opts) {
|
|
25
|
+
super(message, opts);
|
|
26
|
+
this.name = "AuthenticationError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.AuthenticationError = AuthenticationError;
|
|
30
|
+
class RateLimitError extends STTError {
|
|
31
|
+
retryAfter;
|
|
32
|
+
constructor(message = "Rate limit exceeded — try again shortly", opts) {
|
|
33
|
+
super(message, opts);
|
|
34
|
+
this.name = "RateLimitError";
|
|
35
|
+
this.retryAfter = opts?.retryAfter;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.RateLimitError = RateLimitError;
|
|
39
|
+
class JobNotFoundError extends STTError {
|
|
40
|
+
constructor(message = "Job not found or upload session expired", opts) {
|
|
41
|
+
super(message, opts);
|
|
42
|
+
this.name = "JobNotFoundError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.JobNotFoundError = JobNotFoundError;
|
|
46
|
+
class JobFailedError extends STTError {
|
|
47
|
+
step;
|
|
48
|
+
reason;
|
|
49
|
+
constructor(message, opts) {
|
|
50
|
+
super(message, opts);
|
|
51
|
+
this.name = "JobFailedError";
|
|
52
|
+
this.step = opts?.step;
|
|
53
|
+
this.reason = opts?.reason;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.JobFailedError = JobFailedError;
|
|
57
|
+
class UploadError extends STTError {
|
|
58
|
+
constructor(message, opts) {
|
|
59
|
+
super(message, opts);
|
|
60
|
+
this.name = "UploadError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.UploadError = UploadError;
|
|
64
|
+
class TimeoutError extends STTError {
|
|
65
|
+
constructor(message, opts) {
|
|
66
|
+
super(message, opts);
|
|
67
|
+
this.name = "TimeoutError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.TimeoutError = TimeoutError;
|
|
71
|
+
class APIError extends STTError {
|
|
72
|
+
constructor(message, opts) {
|
|
73
|
+
super(message, opts);
|
|
74
|
+
this.name = "APIError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.APIError = APIError;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.computePercent = exports.ProgressPrinter = exports.parseTranscript = exports.STTClient = exports.SpeechRevolutionsClient = exports.SpeechRevolutions = void 0;
|
|
18
|
+
var client_js_1 = require("./client.js");
|
|
19
|
+
Object.defineProperty(exports, "SpeechRevolutions", { enumerable: true, get: function () { return client_js_1.SpeechRevolutions; } });
|
|
20
|
+
Object.defineProperty(exports, "SpeechRevolutionsClient", { enumerable: true, get: function () { return client_js_1.SpeechRevolutionsClient; } });
|
|
21
|
+
Object.defineProperty(exports, "STTClient", { enumerable: true, get: function () { return client_js_1.STTClient; } });
|
|
22
|
+
__exportStar(require("./exceptions.js"), exports);
|
|
23
|
+
var transcript_js_1 = require("./transcript.js");
|
|
24
|
+
Object.defineProperty(exports, "parseTranscript", { enumerable: true, get: function () { return transcript_js_1.parseTranscript; } });
|
|
25
|
+
var progress_js_1 = require("./progress.js");
|
|
26
|
+
Object.defineProperty(exports, "ProgressPrinter", { enumerable: true, get: function () { return progress_js_1.ProgressPrinter; } });
|
|
27
|
+
var types_js_1 = require("./types.js");
|
|
28
|
+
Object.defineProperty(exports, "computePercent", { enumerable: true, get: function () { return types_js_1.computePercent; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Optional console progress rendering for upload + transcription jobs.
|
|
4
|
+
*
|
|
5
|
+
* Neither AssemblyAI nor Deepgram surfaces live percentage progress for
|
|
6
|
+
* pre-recorded transcription — our pipeline is chunked and emits SSE progress
|
|
7
|
+
* events, so this is a Speech Revolutions extra. The same renderer also drives
|
|
8
|
+
* the byte-level *upload* bar.
|
|
9
|
+
*
|
|
10
|
+
* There is no tqdm in JS, so this is a tiny built-in renderer: a single line
|
|
11
|
+
* written to stderr, updated in place with a carriage return, showing a
|
|
12
|
+
* percentage and a `[####----]`-style bar. It always forwards each event to a
|
|
13
|
+
* user-supplied callback, so programmatic access via `onProgress` /
|
|
14
|
+
* `onUploadProgress` is unaffected whether or not the console display is on.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ProgressPrinter = void 0;
|
|
18
|
+
exports.resolveProgress = resolveProgress;
|
|
19
|
+
const DEFAULT_LABEL = "Transcribing";
|
|
20
|
+
// Minimum ms between redraws (a byte upload fires many events).
|
|
21
|
+
const MIN_REDRAW_INTERVAL_MS = 80;
|
|
22
|
+
const BAR_WIDTH = 30;
|
|
23
|
+
function formatBytes(n) {
|
|
24
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
25
|
+
let size = n;
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (size >= 1024 && i < units.length - 1) {
|
|
28
|
+
size /= 1024;
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
return `${size.toFixed(i === 0 ? 0 : 1)}${units[i]}`;
|
|
32
|
+
}
|
|
33
|
+
function stderr() {
|
|
34
|
+
if (typeof process === "undefined" || !process.stderr)
|
|
35
|
+
return undefined;
|
|
36
|
+
return process.stderr;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A progress callback that renders to the console and forwards events.
|
|
40
|
+
*
|
|
41
|
+
* `bytesMode` renders sizes (e.g. `2.5MB/6.0MB`) alongside the bar. For
|
|
42
|
+
* transcription the volatile step name (preprocess / chunk:N / aggregation) is
|
|
43
|
+
* deliberately kept off the bar — chunks finish out of order and made the label
|
|
44
|
+
* jump around; callers who want it read `event.step` in their callback.
|
|
45
|
+
*/
|
|
46
|
+
class ProgressPrinter {
|
|
47
|
+
forward;
|
|
48
|
+
label;
|
|
49
|
+
bytesMode;
|
|
50
|
+
lastDraw = 0;
|
|
51
|
+
lastPct = 0;
|
|
52
|
+
drewAny = false;
|
|
53
|
+
finished = false;
|
|
54
|
+
closed = false;
|
|
55
|
+
constructor(opts = {}) {
|
|
56
|
+
this.forward = opts.forward;
|
|
57
|
+
this.label = opts.label ?? DEFAULT_LABEL;
|
|
58
|
+
this.bytesMode = opts.bytesMode ?? false;
|
|
59
|
+
}
|
|
60
|
+
/** Use as the `onProgress` callback — renders, then forwards. */
|
|
61
|
+
handle = (event) => {
|
|
62
|
+
try {
|
|
63
|
+
this.render(event);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
this.forward?.(event);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
render(event) {
|
|
70
|
+
const pct = event.percent;
|
|
71
|
+
if (pct === undefined)
|
|
72
|
+
return;
|
|
73
|
+
this.draw(pct, event.completed, event.total);
|
|
74
|
+
}
|
|
75
|
+
draw(pct, completed, total, force = false) {
|
|
76
|
+
const out = stderr();
|
|
77
|
+
if (!out)
|
|
78
|
+
return;
|
|
79
|
+
const complete = pct >= 100;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
// Throttle redraws (uploads emit many events); always draw the final 100%.
|
|
82
|
+
if (!force &&
|
|
83
|
+
!complete &&
|
|
84
|
+
this.lastDraw !== 0 &&
|
|
85
|
+
now - this.lastDraw < MIN_REDRAW_INTERVAL_MS) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.lastDraw = now;
|
|
89
|
+
this.lastPct = pct;
|
|
90
|
+
const filled = Math.round((BAR_WIDTH * pct) / 100);
|
|
91
|
+
const bar = "#".repeat(filled) + "-".repeat(BAR_WIDTH - filled);
|
|
92
|
+
let line = `\r${this.label}: ${String(Math.round(pct)).padStart(3)}% [${bar}]`;
|
|
93
|
+
if (this.bytesMode && total) {
|
|
94
|
+
line += ` ${formatBytes(completed ?? 0)}/${formatBytes(total)}`;
|
|
95
|
+
}
|
|
96
|
+
out.write(line);
|
|
97
|
+
this.drewAny = true;
|
|
98
|
+
if (complete) {
|
|
99
|
+
out.write("\n");
|
|
100
|
+
this.finished = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Finish the bar. Safe to call more than once. Always leaves 100% drawn. */
|
|
104
|
+
close() {
|
|
105
|
+
if (this.closed)
|
|
106
|
+
return;
|
|
107
|
+
this.closed = true;
|
|
108
|
+
if (this.drewAny && !this.finished) {
|
|
109
|
+
this.draw(100, undefined, undefined, true);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.ProgressPrinter = ProgressPrinter;
|
|
114
|
+
/**
|
|
115
|
+
* Build the effective progress callback for an upload/transcription phase.
|
|
116
|
+
*
|
|
117
|
+
* When `show` is true, wrap `onProgress` in a {@link ProgressPrinter} that
|
|
118
|
+
* renders to the console and still forwards to the user callback. The returned
|
|
119
|
+
* printer (or `undefined`) must have `.close()` called when done.
|
|
120
|
+
*/
|
|
121
|
+
function resolveProgress(onProgress, show, opts = {}) {
|
|
122
|
+
if (!show)
|
|
123
|
+
return { callback: onProgress };
|
|
124
|
+
const printer = new ProgressPrinter({
|
|
125
|
+
forward: onProgress,
|
|
126
|
+
label: opts.label,
|
|
127
|
+
bytesMode: opts.bytesMode,
|
|
128
|
+
});
|
|
129
|
+
return { callback: printer.handle, printer };
|
|
130
|
+
}
|
package/dist/cjs/sse.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Minimal SSE line parser for fetch ReadableStreams. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.parseSSEStream = parseSSEStream;
|
|
5
|
+
async function* parseSSEStream(reader) {
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
let buffer = "";
|
|
8
|
+
let current = {};
|
|
9
|
+
let dataLines = [];
|
|
10
|
+
const flush = () => {
|
|
11
|
+
if (!dataLines.length)
|
|
12
|
+
return undefined;
|
|
13
|
+
const event = { ...current, data: dataLines.join("\n") };
|
|
14
|
+
dataLines = [];
|
|
15
|
+
return event;
|
|
16
|
+
};
|
|
17
|
+
while (true) {
|
|
18
|
+
const { done, value } = await reader.read();
|
|
19
|
+
if (done)
|
|
20
|
+
break;
|
|
21
|
+
buffer += decoder.decode(value, { stream: true });
|
|
22
|
+
const lines = buffer.split(/\r?\n/);
|
|
23
|
+
buffer = lines.pop() ?? "";
|
|
24
|
+
for (const rawLine of lines) {
|
|
25
|
+
if (rawLine === "") {
|
|
26
|
+
const event = flush();
|
|
27
|
+
if (event)
|
|
28
|
+
yield event;
|
|
29
|
+
current = {};
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (rawLine.startsWith(":"))
|
|
33
|
+
continue;
|
|
34
|
+
const colon = rawLine.indexOf(":");
|
|
35
|
+
let field;
|
|
36
|
+
let valueStr;
|
|
37
|
+
if (colon === -1) {
|
|
38
|
+
field = rawLine;
|
|
39
|
+
valueStr = "";
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
field = rawLine.slice(0, colon);
|
|
43
|
+
valueStr = rawLine.slice(colon + 1).replace(/^ /, "");
|
|
44
|
+
}
|
|
45
|
+
if (field === "id")
|
|
46
|
+
current.id = valueStr;
|
|
47
|
+
else if (field === "event")
|
|
48
|
+
current.event = valueStr;
|
|
49
|
+
else if (field === "data")
|
|
50
|
+
dataLines.push(valueStr);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const event = flush();
|
|
54
|
+
if (event)
|
|
55
|
+
yield event;
|
|
56
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transcript models + Deepgram / AssemblyAI-style adapters.
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.parseTranscript = parseTranscript;
|
|
40
|
+
function joinWords(words) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
for (const w of words) {
|
|
43
|
+
const token = w.word;
|
|
44
|
+
if (!token)
|
|
45
|
+
continue;
|
|
46
|
+
if (parts.length && ".,!?;:%)]}'\"".includes(token[0])) {
|
|
47
|
+
parts[parts.length - 1] = parts[parts.length - 1] + token;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
parts.push(token);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return parts.join(" ");
|
|
54
|
+
}
|
|
55
|
+
function asNumber(value) {
|
|
56
|
+
if (value === undefined || value === null)
|
|
57
|
+
return undefined;
|
|
58
|
+
const n = Number(value);
|
|
59
|
+
return Number.isFinite(n) ? n : undefined;
|
|
60
|
+
}
|
|
61
|
+
function parseWord(data) {
|
|
62
|
+
const word = String(data.word ?? data.text ?? "");
|
|
63
|
+
return {
|
|
64
|
+
word,
|
|
65
|
+
text: word,
|
|
66
|
+
start: asNumber(data.start),
|
|
67
|
+
end: asNumber(data.end),
|
|
68
|
+
speaker: data.speaker != null ? String(data.speaker) : undefined,
|
|
69
|
+
confidence: asNumber(data.confidence),
|
|
70
|
+
language: data.language != null ? String(data.language) : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function parseLanguageSegment(data) {
|
|
74
|
+
return {
|
|
75
|
+
start: asNumber(data.start) ?? 0,
|
|
76
|
+
end: asNumber(data.end) ?? 0,
|
|
77
|
+
language: String(data.language ?? ""),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function utteranceFromGroup(group) {
|
|
81
|
+
const text = joinWords(group);
|
|
82
|
+
return {
|
|
83
|
+
text,
|
|
84
|
+
transcript: text,
|
|
85
|
+
speaker: group[0]?.speaker,
|
|
86
|
+
start: group[0]?.start,
|
|
87
|
+
end: group[group.length - 1]?.end,
|
|
88
|
+
words: group,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function utterancesFromWords(words) {
|
|
92
|
+
if (!words.length)
|
|
93
|
+
return [];
|
|
94
|
+
if (words.every((w) => w.speaker == null)) {
|
|
95
|
+
return [utteranceFromGroup(words)];
|
|
96
|
+
}
|
|
97
|
+
const out = [];
|
|
98
|
+
let current = [words[0]];
|
|
99
|
+
for (let i = 1; i < words.length; i++) {
|
|
100
|
+
const w = words[i];
|
|
101
|
+
if (w.speaker === current[0].speaker)
|
|
102
|
+
current.push(w);
|
|
103
|
+
else {
|
|
104
|
+
out.push(utteranceFromGroup(current));
|
|
105
|
+
current = [w];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
out.push(utteranceFromGroup(current));
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Prefers the server's diarization segments, which separate turns the speaker
|
|
113
|
+
* labels alone cannot (the same speaker talking twice). Falls back to grouping
|
|
114
|
+
* consecutive words by speaker.
|
|
115
|
+
*/
|
|
116
|
+
function utterancesFromDiarization(words, diarization) {
|
|
117
|
+
if (!Array.isArray(diarization) || diarization.length === 0) {
|
|
118
|
+
return utterancesFromWords(words);
|
|
119
|
+
}
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const seg of diarization) {
|
|
122
|
+
const start = asNumber(seg?.start);
|
|
123
|
+
const end = asNumber(seg?.end);
|
|
124
|
+
if (start === undefined || end === undefined)
|
|
125
|
+
continue;
|
|
126
|
+
const segWords = wordsWithin(words, start, end);
|
|
127
|
+
const text = joinWords(segWords);
|
|
128
|
+
out.push({
|
|
129
|
+
text,
|
|
130
|
+
transcript: text,
|
|
131
|
+
speaker: seg.speaker == null ? undefined : String(seg.speaker),
|
|
132
|
+
start,
|
|
133
|
+
end,
|
|
134
|
+
words: segWords,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return out.length ? out : utterancesFromWords(words);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Words a segment covers, falling back to a midpoint test for words that
|
|
141
|
+
* straddle the boundary.
|
|
142
|
+
*/
|
|
143
|
+
function wordsWithin(words, start, end) {
|
|
144
|
+
const eps = 1e-3;
|
|
145
|
+
const timed = words.filter((w) => w.start !== undefined && w.end !== undefined);
|
|
146
|
+
const inside = timed.filter((w) => w.start >= start - eps && w.end <= end + eps);
|
|
147
|
+
if (inside.length)
|
|
148
|
+
return inside;
|
|
149
|
+
return timed.filter((w) => {
|
|
150
|
+
const mid = (w.start + w.end) / 2;
|
|
151
|
+
return mid >= start && mid <= end;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function speakerIndex(speaker) {
|
|
155
|
+
if (speaker == null)
|
|
156
|
+
return undefined;
|
|
157
|
+
if (speaker.toUpperCase().startsWith("SPEAKER_")) {
|
|
158
|
+
const n = Number(speaker.split("_")[1]);
|
|
159
|
+
return Number.isFinite(n) ? n : speaker;
|
|
160
|
+
}
|
|
161
|
+
if (speaker.length === 1 && /[a-zA-Z]/.test(speaker)) {
|
|
162
|
+
return speaker.toUpperCase().charCodeAt(0) - "A".charCodeAt(0);
|
|
163
|
+
}
|
|
164
|
+
const n = Number(speaker);
|
|
165
|
+
return Number.isFinite(n) ? n : speaker;
|
|
166
|
+
}
|
|
167
|
+
function parseTranscript(opts) {
|
|
168
|
+
const { jobId, content, outputType } = opts;
|
|
169
|
+
const downloadUrl = opts.downloadUrl ?? "";
|
|
170
|
+
if (outputType !== "json") {
|
|
171
|
+
let text = "";
|
|
172
|
+
try {
|
|
173
|
+
text = new TextDecoder().decode(content);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
text = "";
|
|
177
|
+
}
|
|
178
|
+
return makeTranscript({
|
|
179
|
+
jobId,
|
|
180
|
+
outputType,
|
|
181
|
+
content,
|
|
182
|
+
downloadUrl,
|
|
183
|
+
words: [],
|
|
184
|
+
utterances: [],
|
|
185
|
+
languages: [],
|
|
186
|
+
raw: null,
|
|
187
|
+
text,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
let raw;
|
|
191
|
+
try {
|
|
192
|
+
raw = JSON.parse(new TextDecoder().decode(content));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return makeTranscript({
|
|
196
|
+
jobId,
|
|
197
|
+
outputType,
|
|
198
|
+
content,
|
|
199
|
+
downloadUrl,
|
|
200
|
+
words: [],
|
|
201
|
+
utterances: [],
|
|
202
|
+
languages: [],
|
|
203
|
+
raw: null,
|
|
204
|
+
text: "",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const words = Array.isArray(raw.words)
|
|
208
|
+
? raw.words.map(parseWord)
|
|
209
|
+
: [];
|
|
210
|
+
const utterances = utterancesFromDiarization(words, raw.diarization);
|
|
211
|
+
const languages = Array.isArray(raw.languages)
|
|
212
|
+
? raw.languages.map(parseLanguageSegment)
|
|
213
|
+
: [];
|
|
214
|
+
const text = joinWords(words);
|
|
215
|
+
return makeTranscript({
|
|
216
|
+
jobId,
|
|
217
|
+
outputType,
|
|
218
|
+
content,
|
|
219
|
+
downloadUrl,
|
|
220
|
+
words,
|
|
221
|
+
utterances,
|
|
222
|
+
languages,
|
|
223
|
+
raw,
|
|
224
|
+
text,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function makeTranscript(args) {
|
|
228
|
+
const textValue = args.text;
|
|
229
|
+
return {
|
|
230
|
+
jobId: args.jobId,
|
|
231
|
+
outputType: args.outputType,
|
|
232
|
+
content: args.content,
|
|
233
|
+
downloadUrl: args.downloadUrl,
|
|
234
|
+
words: args.words,
|
|
235
|
+
utterances: args.utterances,
|
|
236
|
+
languages: args.languages,
|
|
237
|
+
raw: args.raw,
|
|
238
|
+
get text() {
|
|
239
|
+
return textValue;
|
|
240
|
+
},
|
|
241
|
+
get transcript() {
|
|
242
|
+
return textValue;
|
|
243
|
+
},
|
|
244
|
+
async save(path) {
|
|
245
|
+
const name = path.includes("/") ? path.slice(path.lastIndexOf("/") + 1) : path;
|
|
246
|
+
const out = name.includes(".") ? path : `${path}.${args.outputType}`;
|
|
247
|
+
const { writeFile } = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
|
|
248
|
+
await writeFile(out, args.content);
|
|
249
|
+
return out;
|
|
250
|
+
},
|
|
251
|
+
toDict() {
|
|
252
|
+
const dict = {
|
|
253
|
+
id: args.jobId,
|
|
254
|
+
status: "completed",
|
|
255
|
+
text: textValue,
|
|
256
|
+
words: args.words,
|
|
257
|
+
utterances: args.utterances,
|
|
258
|
+
output_type: args.outputType,
|
|
259
|
+
};
|
|
260
|
+
if (args.languages.length)
|
|
261
|
+
dict.languages = args.languages;
|
|
262
|
+
return dict;
|
|
263
|
+
},
|
|
264
|
+
toDeepgram() {
|
|
265
|
+
return {
|
|
266
|
+
metadata: { request_id: args.jobId, channels: 1 },
|
|
267
|
+
results: {
|
|
268
|
+
channels: [
|
|
269
|
+
{
|
|
270
|
+
alternatives: [
|
|
271
|
+
{
|
|
272
|
+
transcript: textValue,
|
|
273
|
+
confidence: 1.0,
|
|
274
|
+
words: args.words.map((w) => ({
|
|
275
|
+
word: w.word.replace(/[.,!?;:]+$/g, "").toLowerCase(),
|
|
276
|
+
punctuated_word: w.word,
|
|
277
|
+
start: w.start,
|
|
278
|
+
end: w.end,
|
|
279
|
+
speaker: speakerIndex(w.speaker),
|
|
280
|
+
})),
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
utterances: args.utterances.map((u) => ({
|
|
286
|
+
transcript: u.text,
|
|
287
|
+
channel: 0,
|
|
288
|
+
start: u.start,
|
|
289
|
+
end: u.end,
|
|
290
|
+
speaker: speakerIndex(u.speaker),
|
|
291
|
+
words: u.words.map((w) => ({
|
|
292
|
+
word: w.word,
|
|
293
|
+
punctuated_word: w.word,
|
|
294
|
+
start: w.start,
|
|
295
|
+
end: w.end,
|
|
296
|
+
speaker: speakerIndex(w.speaker),
|
|
297
|
+
})),
|
|
298
|
+
})),
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computePercent = computePercent;
|
|
4
|
+
exports.makeProgressEvent = makeProgressEvent;
|
|
5
|
+
exports.resolveOptions = resolveOptions;
|
|
6
|
+
/** Completion as a 0–100 number clamped to [0, 100], or `undefined` if unknown. */
|
|
7
|
+
function computePercent(completed, total) {
|
|
8
|
+
if (completed === undefined || completed === null || !total)
|
|
9
|
+
return undefined;
|
|
10
|
+
return Math.max(0, Math.min(100, (completed / total) * 100));
|
|
11
|
+
}
|
|
12
|
+
/** Build a {@link ProgressEvent} with its `percent` derived from completed/total. */
|
|
13
|
+
function makeProgressEvent(event) {
|
|
14
|
+
return { ...event, percent: computePercent(event.completed, event.total) };
|
|
15
|
+
}
|
|
16
|
+
function resolveOptions(options = {}) {
|
|
17
|
+
const speakerLabels = options.diarize !== undefined
|
|
18
|
+
? options.diarize
|
|
19
|
+
: options.speakerLabels !== undefined
|
|
20
|
+
? options.speakerLabels
|
|
21
|
+
: true;
|
|
22
|
+
return {
|
|
23
|
+
outputType: options.outputType ?? "json",
|
|
24
|
+
wordTimestamps: options.wordTimestamps ?? true,
|
|
25
|
+
speakerLabels,
|
|
26
|
+
nltk: options.nltk ?? true,
|
|
27
|
+
tier: options.tier ?? "standard",
|
|
28
|
+
customVocabulary: options.customVocabulary,
|
|
29
|
+
callbackUrl: options.callbackUrl,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Upload-body helpers that report byte-level progress.
|
|
4
|
+
*
|
|
5
|
+
* The HTTP client can only observe upload progress if it reads the body through
|
|
6
|
+
* something we control. For a presigned S3 PUT that's a byte-chunk async
|
|
7
|
+
* generator (`iterWithProgress`) paired with an explicit `Content-Length`
|
|
8
|
+
* header, which keeps S3 happy — undici (Node's fetch) would otherwise switch a
|
|
9
|
+
* streamed body to `Transfer-Encoding: chunked`, which presigned PUTs reject.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.UPLOAD_CHUNK_SIZE = void 0;
|
|
13
|
+
exports.byteProgressAdapter = byteProgressAdapter;
|
|
14
|
+
exports.iterWithProgress = iterWithProgress;
|
|
15
|
+
const types_js_1 = require("./types.js");
|
|
16
|
+
exports.UPLOAD_CHUNK_SIZE = 64 * 1024;
|
|
17
|
+
/**
|
|
18
|
+
* Adapt a {@link ProgressCallback} to a `(sent, total)` byte callback.
|
|
19
|
+
*
|
|
20
|
+
* Upload events are reported as `ProgressEvent(step="upload")` so they share the
|
|
21
|
+
* same shape (and `.percent`) as transcription progress.
|
|
22
|
+
*/
|
|
23
|
+
function byteProgressAdapter(onProgress) {
|
|
24
|
+
if (!onProgress)
|
|
25
|
+
return undefined;
|
|
26
|
+
return (sent, total) => {
|
|
27
|
+
onProgress((0, types_js_1.makeProgressEvent)({ completed: sent, total, step: "upload" }));
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Yield `data` in chunks, reporting progress after each — used as the streamed
|
|
32
|
+
* PUT body. A fresh generator is created per upload attempt, so a retry simply
|
|
33
|
+
* restarts progress from 0.
|
|
34
|
+
*/
|
|
35
|
+
async function* iterWithProgress(data, callback) {
|
|
36
|
+
const total = data.byteLength;
|
|
37
|
+
let sent = 0;
|
|
38
|
+
for (let start = 0; start < total; start += exports.UPLOAD_CHUNK_SIZE) {
|
|
39
|
+
const chunk = data.subarray(start, Math.min(start + exports.UPLOAD_CHUNK_SIZE, total));
|
|
40
|
+
sent += chunk.byteLength;
|
|
41
|
+
yield chunk;
|
|
42
|
+
callback?.(sent, total);
|
|
43
|
+
}
|
|
44
|
+
}
|