diffio 0.1.111 → 0.2.1
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 +60 -17
- package/dist/Client.d.ts +12 -3
- package/dist/Client.js +100 -84
- package/dist/api/resources/projects/client/Client.d.ts +7 -1
- package/dist/api/resources/projects/client/Client.js +4 -0
- package/dist/api/serialization.d.ts +5 -2
- package/dist/api/serialization.js +65 -8
- package/dist/api/types.d.ts +53 -9
- package/dist/core/edgeUpload.d.ts +62 -0
- package/dist/core/edgeUpload.js +213 -0
- package/dist/errors/index.d.ts +17 -0
- package/dist/errors/index.js +13 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Diffio JS SDK
|
|
2
2
|
|
|
3
|
-
The Diffio JS SDK helps you call the Diffio API from Node. This version covers project creation, upload,
|
|
3
|
+
The Diffio JS SDK helps you call the Diffio API from Node. This version covers project creation, edge upload, Diffio 4.5 generations, progress checks, and download URLs.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -42,9 +42,34 @@ const projects = await client.listProjects({
|
|
|
42
42
|
});
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Models
|
|
46
|
+
|
|
47
|
+
| Model | `model` value | Endpoint | Notes |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| Diffio 4.5 Flash | `diffio-4.5-flash` | `/v1/diffio-4.5-flash-generation` | Default. Fast, high quality speech restoration. |
|
|
50
|
+
| Diffio 4.5 Pro | `diffio-4.5-pro` | `/v1/diffio-4.5-pro-generation` | Best quality. Paid accounts only. |
|
|
51
|
+
|
|
52
|
+
Omitting `model` uses `diffio-4.5-flash`. Earlier models (`diffio-2`, `diffio-2-flash`, `diffio-3.2`,
|
|
53
|
+
`diffio-3.4`, `diffio-3.5`, `diffio-4.0-flash`, `diffio-4.0-pro`) are retired: the SDK refuses them
|
|
54
|
+
before sending a request, and the API answers their endpoints with HTTP 410 `model_retired`.
|
|
55
|
+
Generations created before a model was retired keep their original `modelKey`, so response
|
|
56
|
+
`modelKey` fields are typed as `string`.
|
|
57
|
+
|
|
45
58
|
## Create a project and generation
|
|
46
59
|
|
|
47
|
-
`createProject` uploads the file
|
|
60
|
+
`createProject` creates the project, uploads the file through Diffio's upload edge, and confirms
|
|
61
|
+
the upload, so the project is ready for a generation when it returns.
|
|
62
|
+
|
|
63
|
+
The upload follows the session `create_project` returns: the SDK starts a multipart upload at
|
|
64
|
+
`{edgeBaseUrl}/v1/uploads/start`, sends the file in parts of `partSizeBytes` (32 MiB, three at a time)
|
|
65
|
+
to `/v1/uploads/parts/{partNumber}`, completes it with `/v1/uploads/complete`, and then calls
|
|
66
|
+
`/v1/complete_project_upload`. Each part is tried up to four times on network errors, timeouts,
|
|
67
|
+
`408`, `429`, and `5xx` answers. A failed upload is aborted and raises `DiffioUploadError` (a
|
|
68
|
+
`DiffioApiError`) with `uploadErrorCode` (`upload/too-large`, `upload/unauthorized`,
|
|
69
|
+
`upload/rejected`, `upload/network`, `upload/server`, `upload/invalid-response`, or
|
|
70
|
+
`upload/canceled`), the edge's `edgeErrorCode` when it sent one, and the `apiProjectId`. Files
|
|
71
|
+
larger than the session's `maxBytes` (2 GiB) are refused before any bytes are sent. The upload token
|
|
72
|
+
is used only inside the SDK and is not part of the returned project.
|
|
48
73
|
|
|
49
74
|
```ts
|
|
50
75
|
import { DiffioClient } from "diffio";
|
|
@@ -58,12 +83,19 @@ const project = await client.createProject({
|
|
|
58
83
|
|
|
59
84
|
const generation = await client.createGeneration({
|
|
60
85
|
apiProjectId: project.apiProjectId,
|
|
61
|
-
model: "diffio-4.
|
|
62
|
-
sampling: { steps: 12, guidance: 1.5 },
|
|
86
|
+
model: "diffio-4.5-flash",
|
|
63
87
|
idempotencyKey: "restore-sample-001"
|
|
64
88
|
});
|
|
65
89
|
|
|
66
90
|
console.log(generation.generationId, generation.idempotentReplay ?? false);
|
|
91
|
+
console.log(project.upload.objectKey, project.uploadCompletion.sizeBytes);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
If `createProject` uploaded the file but the confirmation call failed, confirm it yourself. The call
|
|
95
|
+
is idempotent, and Diffio also records the upload on its own shortly after the edge completes it.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
await client.projects.completeUpload({ apiProjectId: "proj_123" });
|
|
67
99
|
```
|
|
68
100
|
|
|
69
101
|
Reuse the same `idempotencyKey` when retrying generation creation for a project. The API then
|
|
@@ -77,11 +109,12 @@ retrying after an uncertain response.
|
|
|
77
109
|
|
|
78
110
|
`waitForGeneration` and `generations.waitForComplete` wait for the overall `status` to become
|
|
79
111
|
`complete`. Individual stages reaching 100% or `complete` do not end polling while video publication
|
|
80
|
-
or usage settlement is still pending.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
or usage settlement is still pending. They poll for up to 600 seconds unless you pass `timeout`
|
|
113
|
+
or `timeoutInSeconds`. `complete` means restored media is ready. Diffio 4.5 transcribes the recording before
|
|
114
|
+
restoration starts, so a completed generation has its transcript unless transcription finished as
|
|
115
|
+
`unavailable`; while a generation runs, transcription can be `pending`, `available`, or `unavailable`. Read
|
|
116
|
+
`progress.transcription?.status` independently. Older responses omit `transcription`; absence does
|
|
117
|
+
not establish availability. Unavailable transcription does not fail completed media.
|
|
85
118
|
|
|
86
119
|
## Audio isolation helper
|
|
87
120
|
|
|
@@ -91,8 +124,7 @@ import { DiffioClient } from "diffio";
|
|
|
91
124
|
const client = new DiffioClient({ apiKey: "diffio_live_..." });
|
|
92
125
|
const result = await client.audioIsolation.isolate({
|
|
93
126
|
filePath: "sample.wav",
|
|
94
|
-
model: "diffio-4.
|
|
95
|
-
sampling: { steps: 12, guidance: 1.5 },
|
|
127
|
+
model: "diffio-4.5-flash",
|
|
96
128
|
idempotencyKey: "restore-sample-001"
|
|
97
129
|
});
|
|
98
130
|
|
|
@@ -113,8 +145,7 @@ import { DiffioClient } from "diffio";
|
|
|
113
145
|
const client = new DiffioClient({ apiKey: "diffio_live_..." });
|
|
114
146
|
const [audioBytes, info] = await client.restoreAudio({
|
|
115
147
|
filePath: "sample.wav",
|
|
116
|
-
model: "diffio-4.
|
|
117
|
-
sampling: { steps: 12, guidance: 1.5 },
|
|
148
|
+
model: "diffio-4.5-flash",
|
|
118
149
|
idempotencyKey: "restore-sample-001",
|
|
119
150
|
onProgress: (progress) => console.log(progress.status)
|
|
120
151
|
});
|
|
@@ -139,10 +170,17 @@ const progress = await client.generations.getProgress({
|
|
|
139
170
|
apiProjectId: "proj_123"
|
|
140
171
|
});
|
|
141
172
|
|
|
142
|
-
console.log(progress.status);
|
|
173
|
+
console.log(progress.status, progress.stage);
|
|
174
|
+
console.log(progress.queue?.message ?? "not queued");
|
|
175
|
+
console.log(progress.stageProgress?.overallPercent);
|
|
143
176
|
console.log(progress.transcription?.status ?? "not reported");
|
|
144
177
|
```
|
|
145
178
|
|
|
179
|
+
`stage` names the one step the generation is in (`pending`, `preparing`, `transcribing`, `queued`,
|
|
180
|
+
`starting`, `downloading`, `decoding`, `restoring`, `finalizing`, `uploading`, `complete`, or
|
|
181
|
+
`failed`). While it waits for a processing worker, `queue` reports its position and why it waits;
|
|
182
|
+
while a worker runs it, `stageProgress` reports percentages and byte counts when known.
|
|
183
|
+
|
|
146
184
|
## Generation download
|
|
147
185
|
|
|
148
186
|
```ts
|
|
@@ -158,6 +196,10 @@ const download = await client.generations.getDownload({
|
|
|
158
196
|
console.log(download.downloadUrl);
|
|
159
197
|
```
|
|
160
198
|
|
|
199
|
+
`downloadUrl` is a signed, time-limited media URL that needs no `Authorization` header. The
|
|
200
|
+
response has `generationId`, `apiProjectId`, `downloadType`, `downloadUrl`, `fileName`,
|
|
201
|
+
`storagePath`, and `mimeType`.
|
|
202
|
+
|
|
161
203
|
Set `downloadType` to `"transcript"` to fetch the transcript JSON artifact when available.
|
|
162
204
|
Pending transcripts raise `DiffioApiError` with `statusCode === 409` and error code
|
|
163
205
|
`TRANSCRIPT_PENDING`. Unavailable transcripts return `404` with `TRANSCRIPT_UNAVAILABLE`.
|
|
@@ -260,9 +302,9 @@ console.log(event.svixMessageId);
|
|
|
260
302
|
Use the raw request body (not parsed JSON) plus the `svix-*` headers and your webhook signing secret.
|
|
261
303
|
|
|
262
304
|
Verified events expose the same optional `event.transcription` object as generation progress.
|
|
263
|
-
A
|
|
264
|
-
|
|
265
|
-
|
|
305
|
+
A `generation.completed` event reports `available` transcription, or `unavailable` when no
|
|
306
|
+
transcript could be produced; completion does not wait for a later transcript. Older events can omit
|
|
307
|
+
`transcription`.
|
|
266
308
|
|
|
267
309
|
```ts
|
|
268
310
|
import express from "express";
|
|
@@ -303,4 +345,5 @@ Examples use ES modules. Save files with a `.mjs` extension or set `"type": "mod
|
|
|
303
345
|
```bash
|
|
304
346
|
cd diffio-js
|
|
305
347
|
npm run build
|
|
348
|
+
npm test
|
|
306
349
|
```
|
package/dist/Client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseClientOptions, BaseRequestOptions, NormalizedClientOptions } from "./BaseClient";
|
|
2
|
-
import type { AccountSettingsResponse, ApiKeyResponse, ApiKeysListResponse, AudioIsolationResult, CreateGenerationResponse, CreateProjectResponse, GenerationDownloadResponse, GenerationProgressResponse, ListProjectGenerationsResponse, ListProjectsResponse, ModelKey, RestoreMetadata, UsageSummaryResponse, WebhookConfigureResponse, WebhookTestEventResponse } from "./api/types";
|
|
2
|
+
import type { AccountSettingsResponse, ApiKeyResponse, ApiKeysListResponse, AudioIsolationResult, CompleteProjectUploadResponse, CreateGenerationResponse, CreateProjectResponse, GenerationDownloadResponse, GenerationProgressResponse, ListProjectGenerationsResponse, ListProjectsResponse, ModelKey, RestoreMetadata, UsageSummaryResponse, WebhookConfigureResponse, WebhookTestEventResponse } from "./api/types";
|
|
3
3
|
import { AccountClient, ApiKeysClient, AudioIsolationClient, GenerationsClient, ProjectsClient, UsageClient, WebhooksClient } from "./api/resources";
|
|
4
4
|
export declare namespace DiffioClient {
|
|
5
5
|
type Options = BaseClientOptions;
|
|
@@ -25,7 +25,16 @@ export declare class DiffioClient {
|
|
|
25
25
|
fileFormat?: string;
|
|
26
26
|
requestOptions?: DiffioClient.RequestOptions;
|
|
27
27
|
}): Promise<CreateProjectResponse>;
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Confirms that a project's edge upload landed and starts preprocessing. createProject calls it;
|
|
30
|
+
* call it yourself only to finish an upload whose confirmation failed. It is idempotent.
|
|
31
|
+
*/
|
|
32
|
+
completeProjectUpload(options: {
|
|
33
|
+
apiProjectId: string;
|
|
34
|
+
requestOptions?: DiffioClient.RequestOptions;
|
|
35
|
+
}): Promise<CompleteProjectUploadResponse>;
|
|
36
|
+
/** Sends one edge upload call with the session's upload token; never the API key or SDK default headers. */
|
|
37
|
+
private _sendEdgeUploadRequest;
|
|
29
38
|
/** Retries generation admission only when the caller supplies a nonblank idempotency key. */
|
|
30
39
|
createGeneration(options: {
|
|
31
40
|
apiProjectId: string;
|
|
@@ -47,7 +56,7 @@ export declare class DiffioClient {
|
|
|
47
56
|
apiProjectId?: string;
|
|
48
57
|
requestOptions?: DiffioClient.RequestOptions;
|
|
49
58
|
}): Promise<GenerationProgressResponse>;
|
|
50
|
-
/** Waits for media completion and settlement
|
|
59
|
+
/** Waits for media completion and settlement; transcription may still be pending or unavailable. */
|
|
51
60
|
waitForGeneration(options: {
|
|
52
61
|
generationId: string;
|
|
53
62
|
apiProjectId?: string;
|
package/dist/Client.js
CHANGED
|
@@ -40,22 +40,29 @@ const supplier_1 = require("./core/supplier");
|
|
|
40
40
|
const url_1 = require("./core/url");
|
|
41
41
|
const retry_1 = require("./core/retry");
|
|
42
42
|
const errors_1 = require("./errors");
|
|
43
|
+
const edgeUpload_1 = require("./core/edgeUpload");
|
|
43
44
|
const serialization_1 = require("./api/serialization");
|
|
44
45
|
const resources_1 = require("./api/resources");
|
|
45
46
|
const mime_types_1 = require("mime-types");
|
|
46
47
|
const DEFAULT_BASE_URL = "https://api.diffio.ai";
|
|
47
48
|
const API_PREFIX = "v1";
|
|
49
|
+
/** Generation endpoint for each supported model, as api/model_registry.json in diffio-ui lists them. */
|
|
48
50
|
const MODEL_ENDPOINTS = {
|
|
49
|
-
"diffio-
|
|
50
|
-
"diffio-
|
|
51
|
-
"diffio-3.4": "diffio-3.4-generation",
|
|
52
|
-
"diffio-3.5": "diffio-3.5-generation",
|
|
53
|
-
"diffio-4.0-flash": "diffio-4.0-flash-generation",
|
|
54
|
-
"diffio-4.0-pro": "diffio-4.0-pro-generation"
|
|
51
|
+
"diffio-4.5-flash": "diffio-4.5-flash-generation",
|
|
52
|
+
"diffio-4.5-pro": "diffio-4.5-pro-generation"
|
|
55
53
|
};
|
|
54
|
+
/** The registry's `freeDefault` model; Pro is paid-only, so it cannot be the default for every key. */
|
|
55
|
+
const DEFAULT_MODEL_KEY = "diffio-4.5-flash";
|
|
56
|
+
const SUPPORTED_MODEL_KEYS = Object.keys(MODEL_ENDPOINTS);
|
|
57
|
+
/** Per-request timeout for one edge upload call; a 32 MiB part needs about 1 Mbit/s to finish in time. */
|
|
58
|
+
const DEFAULT_EDGE_UPLOAD_TIMEOUT_SECONDS = 300;
|
|
59
|
+
/** complete_project_upload is idempotent, so it is retried even when the client disables retries by default. */
|
|
60
|
+
const DEFAULT_COMPLETE_UPLOAD_MAX_RETRIES = 3;
|
|
56
61
|
const DEFAULT_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
57
62
|
const DEFAULT_RETRY_BACKOFF = 0.5;
|
|
58
63
|
const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
64
|
+
/** How long waitForGeneration polls by default; fleet generations can queue and run for minutes (matches the Python SDK). */
|
|
65
|
+
const DEFAULT_GENERATION_WAIT_TIMEOUT_SECONDS = 600;
|
|
59
66
|
const WEBHOOK_EVENT_TYPES = [
|
|
60
67
|
"generation.queued",
|
|
61
68
|
"generation.processing",
|
|
@@ -107,45 +114,75 @@ class DiffioClient {
|
|
|
107
114
|
payload.fileFormat = fileFormat;
|
|
108
115
|
}
|
|
109
116
|
const response = await this._requestJson("POST", "create_project", payload, requestOptions);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
const apiProjectId = typeof response?.apiProjectId === "string" ? response.apiProjectId : undefined;
|
|
118
|
+
const session = (0, edgeUpload_1.parseEdgeUploadSession)(response?.upload);
|
|
119
|
+
if (!apiProjectId || !session) {
|
|
120
|
+
throw new errors_1.DiffioUploadError("upload/invalid-response", "create_project did not return an upload session (apiProjectId and upload are required).", { responseBody: response, apiProjectId });
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const sizeBytes = getFileSize(filePath);
|
|
124
|
+
const fileHandle = await openFileForReading(filePath);
|
|
125
|
+
try {
|
|
126
|
+
await (0, edgeUpload_1.uploadProjectMediaToEdge)({
|
|
127
|
+
session,
|
|
128
|
+
sizeBytes,
|
|
129
|
+
readPartBytes: (startByte, endByte) => readFileBytes(fileHandle, startByte, endByte),
|
|
130
|
+
sendEdgeRequest: (request) => this._sendEdgeUploadRequest(request, requestOptions)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await fileHandle.close();
|
|
135
|
+
}
|
|
124
136
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof errors_1.DiffioUploadError) {
|
|
139
|
+
error.apiProjectId = error.apiProjectId ?? apiProjectId;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
129
142
|
}
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
const uploadCompletion = await this.completeProjectUpload({ apiProjectId, requestOptions });
|
|
144
|
+
return (0, serialization_1.createProjectUploadResult)(response, session, uploadCompletion);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Confirms that a project's edge upload landed and starts preprocessing. createProject calls it;
|
|
148
|
+
* call it yourself only to finish an upload whose confirmation failed. It is idempotent.
|
|
149
|
+
*/
|
|
150
|
+
async completeProjectUpload(options) {
|
|
151
|
+
const { apiProjectId, requestOptions } = options;
|
|
152
|
+
if (!apiProjectId) {
|
|
153
|
+
throw new errors_1.DiffioApiError("apiProjectId is required");
|
|
132
154
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
const completeRequestOptions = {
|
|
156
|
+
...requestOptions,
|
|
157
|
+
maxRetries: requestOptions?.maxRetries ?? this._options.maxRetries ?? DEFAULT_COMPLETE_UPLOAD_MAX_RETRIES
|
|
136
158
|
};
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
const response = await this._requestJson("POST", "complete_project_upload", { apiProjectId }, completeRequestOptions);
|
|
160
|
+
return (0, serialization_1.parseCompleteProjectUploadResponse)(response);
|
|
161
|
+
}
|
|
162
|
+
/** Sends one edge upload call with the session's upload token; never the API key or SDK default headers. */
|
|
163
|
+
async _sendEdgeUploadRequest(request, requestOptions) {
|
|
164
|
+
const fetchFn = this._options.fetch ?? globalThis.fetch;
|
|
165
|
+
if (!fetchFn) {
|
|
166
|
+
throw new errors_1.DiffioApiError("fetch is not available in this runtime");
|
|
139
167
|
}
|
|
140
|
-
const
|
|
141
|
-
|
|
168
|
+
const timeoutSeconds = requestOptions?.timeoutInSeconds ?? requestOptions?.timeout ?? DEFAULT_EDGE_UPLOAD_TIMEOUT_SECONDS;
|
|
169
|
+
// A Uint8Array or string body has a known length, so fetch sends the Content-Length the edge requires.
|
|
170
|
+
const response = await fetchWithTimeout(fetchFn, request.url, {
|
|
171
|
+
method: request.method,
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: `Bearer ${request.bearerToken}`,
|
|
174
|
+
"Content-Type": request.contentType
|
|
175
|
+
},
|
|
176
|
+
body: request.body
|
|
177
|
+
}, timeoutSeconds * 1000, requestOptions?.abortSignal);
|
|
178
|
+
return { status: response.status, bodyText: await response.text() };
|
|
142
179
|
}
|
|
143
180
|
/** Retries generation admission only when the caller supplies a nonblank idempotency key. */
|
|
144
181
|
async createGeneration(options) {
|
|
145
|
-
const { apiProjectId, model =
|
|
146
|
-
const endpoint = MODEL_ENDPOINTS[model];
|
|
182
|
+
const { apiProjectId, model = DEFAULT_MODEL_KEY, sampling, params, idempotencyKey, requestOptions } = options;
|
|
183
|
+
const endpoint = Object.prototype.hasOwnProperty.call(MODEL_ENDPOINTS, model) ? MODEL_ENDPOINTS[model] : undefined;
|
|
147
184
|
if (!endpoint) {
|
|
148
|
-
throw new errors_1.DiffioApiError(`Unsupported model: ${model}
|
|
185
|
+
throw new errors_1.DiffioApiError(`Unsupported model: ${model}. Use ${SUPPORTED_MODEL_KEYS.join(" or ")}.`);
|
|
149
186
|
}
|
|
150
187
|
const payload = { apiProjectId };
|
|
151
188
|
if (sampling != null) {
|
|
@@ -184,10 +221,10 @@ class DiffioClient {
|
|
|
184
221
|
const response = await this._requestJson("POST", "get_generation_progress", payload, requestOptions);
|
|
185
222
|
return (0, serialization_1.parseGenerationProgressResponse)(response);
|
|
186
223
|
}
|
|
187
|
-
/** Waits for media completion and settlement
|
|
224
|
+
/** Waits for media completion and settlement; transcription may still be pending or unavailable. */
|
|
188
225
|
async waitForGeneration(options) {
|
|
189
226
|
const { generationId, apiProjectId, pollInterval = 2, timeout, timeoutInSeconds, onProgress, showProgress, requestOptions } = options;
|
|
190
|
-
const timeoutSeconds = timeoutInSeconds ?? timeout ??
|
|
227
|
+
const timeoutSeconds = timeoutInSeconds ?? timeout ?? DEFAULT_GENERATION_WAIT_TIMEOUT_SECONDS;
|
|
191
228
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
192
229
|
let lastProgress = null;
|
|
193
230
|
while (Date.now() < deadline) {
|
|
@@ -450,11 +487,8 @@ class DiffioClient {
|
|
|
450
487
|
return (0, serialization_1.createAudioIsolationResult)(project, generation);
|
|
451
488
|
}
|
|
452
489
|
async _downloadBinary(downloadUrl, requestOptions) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
extraHeaders.Authorization = "Bearer owner";
|
|
456
|
-
}
|
|
457
|
-
const response = await this._requestBinary("GET", downloadUrl, () => undefined, requestOptions, extraHeaders, true);
|
|
490
|
+
// The download URL is a signed edge media URL; it needs no Authorization header.
|
|
491
|
+
const response = await this._requestBinary("GET", downloadUrl, () => undefined, requestOptions, {}, true);
|
|
458
492
|
return response;
|
|
459
493
|
}
|
|
460
494
|
async _requestJson(method, path, payload, requestOptions) {
|
|
@@ -636,9 +670,12 @@ async function parseErrorResponse(response) {
|
|
|
636
670
|
}
|
|
637
671
|
function getErrorMessage(body, status) {
|
|
638
672
|
if (body && typeof body === "object" && "error" in body) {
|
|
639
|
-
const
|
|
640
|
-
if (message) {
|
|
641
|
-
return
|
|
673
|
+
const error = body.error;
|
|
674
|
+
if (error && typeof error === "object" && typeof error.message === "string") {
|
|
675
|
+
return error.message;
|
|
676
|
+
}
|
|
677
|
+
if (error) {
|
|
678
|
+
return String(error);
|
|
642
679
|
}
|
|
643
680
|
}
|
|
644
681
|
return `Request failed with status ${status}`;
|
|
@@ -657,36 +694,6 @@ function guessContentType(filePath) {
|
|
|
657
694
|
}
|
|
658
695
|
return undefined;
|
|
659
696
|
}
|
|
660
|
-
function isStorageEmulatorUrl(url) {
|
|
661
|
-
try {
|
|
662
|
-
const parsed = new URL(url);
|
|
663
|
-
const host = parsed.hostname.toLowerCase();
|
|
664
|
-
const port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
|
665
|
-
if (["127.0.0.1", "localhost", "0.0.0.0", "::1"].includes(host)) {
|
|
666
|
-
if (!parsed.port || port === 9199) {
|
|
667
|
-
return true;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
const envHost = typeof process !== "undefined"
|
|
671
|
-
? process.env.STORAGE_EMULATOR_HOST || process.env.FIREBASE_STORAGE_EMULATOR_HOST
|
|
672
|
-
: undefined;
|
|
673
|
-
if (!envHost) {
|
|
674
|
-
return false;
|
|
675
|
-
}
|
|
676
|
-
const normalized = envHost.startsWith("http://") || envHost.startsWith("https://") ? envHost : `http://${envHost}`;
|
|
677
|
-
const emulatorParsed = new URL(normalized);
|
|
678
|
-
const emulatorHost = emulatorParsed.hostname.toLowerCase();
|
|
679
|
-
const emulatorPort = emulatorParsed.port
|
|
680
|
-
? Number(emulatorParsed.port)
|
|
681
|
-
: emulatorParsed.protocol === "https:"
|
|
682
|
-
? 443
|
|
683
|
-
: 80;
|
|
684
|
-
return host === emulatorHost && port === emulatorPort;
|
|
685
|
-
}
|
|
686
|
-
catch {
|
|
687
|
-
return false;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
697
|
function isNodeReadable(value) {
|
|
691
698
|
return Boolean(value) && typeof value === "object" && typeof value.pipe === "function";
|
|
692
699
|
}
|
|
@@ -698,14 +705,23 @@ function destroyNodeReadable(value) {
|
|
|
698
705
|
destroy.call(value);
|
|
699
706
|
}
|
|
700
707
|
}
|
|
701
|
-
async function
|
|
702
|
-
const fs = await Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
708
|
+
async function openFileForReading(filePath) {
|
|
709
|
+
const fs = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
|
|
710
|
+
return fs.open(filePath, "r");
|
|
711
|
+
}
|
|
712
|
+
/** Reads bytes [startByte, endByte) of an open file into one buffer, looping over short reads. */
|
|
713
|
+
async function readFileBytes(fileHandle, startByte, endByte) {
|
|
714
|
+
const length = endByte - startByte;
|
|
715
|
+
const buffer = Buffer.alloc(length);
|
|
716
|
+
let offset = 0;
|
|
717
|
+
while (offset < length) {
|
|
718
|
+
const { bytesRead } = await fileHandle.read(buffer, offset, length - offset, startByte + offset);
|
|
719
|
+
if (bytesRead === 0) {
|
|
720
|
+
throw new errors_1.DiffioUploadError("upload/invalid-response", "The file became shorter while it was uploading.");
|
|
721
|
+
}
|
|
722
|
+
offset += bytesRead;
|
|
723
|
+
}
|
|
724
|
+
return buffer;
|
|
709
725
|
}
|
|
710
726
|
function getFileSize(filePath) {
|
|
711
727
|
const fs = require("node:fs");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DiffioClient } from "../../../../Client";
|
|
2
|
-
import type { ListProjectGenerationsResponse, ListProjectsResponse } from "../../../types";
|
|
2
|
+
import type { CompleteProjectUploadResponse, ListProjectGenerationsResponse, ListProjectsResponse } from "../../../types";
|
|
3
3
|
export interface ProjectsListOptions {
|
|
4
4
|
requestOptions?: DiffioClient.RequestOptions;
|
|
5
5
|
}
|
|
@@ -7,9 +7,15 @@ export interface ProjectsListGenerationsOptions {
|
|
|
7
7
|
apiProjectId: string;
|
|
8
8
|
requestOptions?: DiffioClient.RequestOptions;
|
|
9
9
|
}
|
|
10
|
+
export interface ProjectsCompleteUploadOptions {
|
|
11
|
+
apiProjectId: string;
|
|
12
|
+
requestOptions?: DiffioClient.RequestOptions;
|
|
13
|
+
}
|
|
10
14
|
export declare class ProjectsClient {
|
|
11
15
|
private _parent;
|
|
12
16
|
constructor(parent: DiffioClient);
|
|
13
17
|
list(options?: ProjectsListOptions): Promise<ListProjectsResponse>;
|
|
18
|
+
/** Confirms a finished edge upload and starts preprocessing; createProject already does this. */
|
|
19
|
+
completeUpload(options: ProjectsCompleteUploadOptions): Promise<CompleteProjectUploadResponse>;
|
|
14
20
|
listGenerations(options: ProjectsListGenerationsOptions): Promise<ListProjectGenerationsResponse>;
|
|
15
21
|
}
|
|
@@ -8,6 +8,10 @@ class ProjectsClient {
|
|
|
8
8
|
async list(options = {}) {
|
|
9
9
|
return this._parent.listProjects(options);
|
|
10
10
|
}
|
|
11
|
+
/** Confirms a finished edge upload and starts preprocessing; createProject already does this. */
|
|
12
|
+
async completeUpload(options) {
|
|
13
|
+
return this._parent.completeProjectUpload(options);
|
|
14
|
+
}
|
|
11
15
|
async listGenerations(options) {
|
|
12
16
|
return this._parent.listProjectGenerations(options);
|
|
13
17
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { EdgeUploadSession } from "../core/edgeUpload";
|
|
2
|
+
import type { AccountSettingsResponse, ApiKeyResponse, ApiKeysListResponse, AudioIsolationResult, CompleteProjectUploadResponse, CreateGenerationResponse, CreateProjectResponse, GenerationWebhookEvent, GenerationDownloadResponse, GenerationProgressResponse, GenerationProgressStage, ListProjectGenerationsResponse, ListProjectsResponse, ProjectGenerationSummary, ProjectSummary, UsageSummaryResponse, WebhookConfigureResponse, WebhookTestEventResponse } from "./types";
|
|
3
|
+
/** Builds the public createProject result; it omits the upload token, which can still overwrite the upload. */
|
|
4
|
+
export declare function createProjectUploadResult(data: any, session: EdgeUploadSession, uploadCompletion: CompleteProjectUploadResponse): CreateProjectResponse;
|
|
5
|
+
export declare function parseCompleteProjectUploadResponse(data: any): CompleteProjectUploadResponse;
|
|
3
6
|
export declare function parseProjectSummary(data: any): ProjectSummary;
|
|
4
7
|
export declare function parseListProjectsResponse(data: any): ListProjectsResponse;
|
|
5
8
|
export declare function parseCreateGenerationResponse(data: any): CreateGenerationResponse;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.createProjectUploadResult = createProjectUploadResult;
|
|
4
|
+
exports.parseCompleteProjectUploadResponse = parseCompleteProjectUploadResponse;
|
|
4
5
|
exports.parseProjectSummary = parseProjectSummary;
|
|
5
6
|
exports.parseListProjectsResponse = parseListProjectsResponse;
|
|
6
7
|
exports.parseCreateGenerationResponse = parseCreateGenerationResponse;
|
|
@@ -17,14 +18,30 @@ exports.parseUsageSummaryResponse = parseUsageSummaryResponse;
|
|
|
17
18
|
exports.parseWebhookConfigureResponse = parseWebhookConfigureResponse;
|
|
18
19
|
exports.parseGenerationWebhookEvent = parseGenerationWebhookEvent;
|
|
19
20
|
exports.createAudioIsolationResult = createAudioIsolationResult;
|
|
20
|
-
|
|
21
|
+
/** Builds the public createProject result; it omits the upload token, which can still overwrite the upload. */
|
|
22
|
+
function createProjectUploadResult(data, session, uploadCompletion) {
|
|
23
|
+
const upload = {
|
|
24
|
+
uploadSessionId: session.uploadSessionId,
|
|
25
|
+
edgeBaseUrl: session.edgeBaseUrl,
|
|
26
|
+
objectKey: session.objectKey,
|
|
27
|
+
partSizeBytes: session.partSizeBytes,
|
|
28
|
+
maxBytes: session.maxBytes,
|
|
29
|
+
expiresAt: session.expiresAt
|
|
30
|
+
};
|
|
21
31
|
return {
|
|
22
32
|
apiProjectId: data.apiProjectId,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
upload,
|
|
34
|
+
objectPath: data.objectPath ?? session.objectKey,
|
|
35
|
+
expiresAt: data.expiresAt ?? session.expiresAt,
|
|
36
|
+
uploadCompletion
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function parseCompleteProjectUploadResponse(data) {
|
|
40
|
+
const sizeBytes = data?.sizeBytes;
|
|
41
|
+
return {
|
|
42
|
+
apiProjectId: data?.apiProjectId,
|
|
43
|
+
status: data?.status ?? "uploaded",
|
|
44
|
+
sizeBytes: typeof sizeBytes === "number" ? sizeBytes : null
|
|
28
45
|
};
|
|
29
46
|
}
|
|
30
47
|
function parseProjectSummary(data) {
|
|
@@ -86,6 +103,8 @@ function parseGenerationProgressStage(data) {
|
|
|
86
103
|
function parseGenerationProgressResponse(data) {
|
|
87
104
|
const restoredVideo = data?.restoredVideo;
|
|
88
105
|
const transcription = parseGenerationTranscription(data?.transcription);
|
|
106
|
+
const stageProgress = parseGenerationStageProgress(data?.stageProgress);
|
|
107
|
+
const queue = parseGenerationQueueStatus(data?.queue);
|
|
89
108
|
return {
|
|
90
109
|
generationId: data.generationId,
|
|
91
110
|
apiProjectId: data.apiProjectId,
|
|
@@ -94,6 +113,9 @@ function parseGenerationProgressResponse(data) {
|
|
|
94
113
|
preProcessing: parseGenerationProgressStage(data.preProcessing),
|
|
95
114
|
inference: parseGenerationProgressStage(data.inference),
|
|
96
115
|
restoredVideo: restoredVideo ? parseGenerationProgressStage(restoredVideo) : null,
|
|
116
|
+
...(typeof data?.stage === "string" ? { stage: data.stage } : {}),
|
|
117
|
+
...(stageProgress ? { stageProgress } : {}),
|
|
118
|
+
...(queue ? { queue } : {}),
|
|
97
119
|
...(transcription ? { transcription } : {}),
|
|
98
120
|
error: data.error ?? null,
|
|
99
121
|
errorDetails: data.errorDetails ?? null
|
|
@@ -107,7 +129,6 @@ function parseGenerationDownloadResponse(data) {
|
|
|
107
129
|
downloadUrl: data.downloadUrl,
|
|
108
130
|
fileName: data.fileName,
|
|
109
131
|
storagePath: data.storagePath,
|
|
110
|
-
bucket: data.bucket,
|
|
111
132
|
mimeType: data.mimeType
|
|
112
133
|
};
|
|
113
134
|
}
|
|
@@ -175,6 +196,42 @@ function parseGenerationWebhookEvent(data) {
|
|
|
175
196
|
errorDetails: data.errorDetails ?? null
|
|
176
197
|
};
|
|
177
198
|
}
|
|
199
|
+
const optionalNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
200
|
+
function parseGenerationStageProgress(data) {
|
|
201
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const source = data;
|
|
205
|
+
const progress = {};
|
|
206
|
+
for (const key of [
|
|
207
|
+
"overallPercent",
|
|
208
|
+
"stagePercent",
|
|
209
|
+
"bytesDone",
|
|
210
|
+
"bytesTotal",
|
|
211
|
+
"availableThroughSeconds",
|
|
212
|
+
"durationSeconds"
|
|
213
|
+
]) {
|
|
214
|
+
const value = optionalNumber(source[key]);
|
|
215
|
+
if (value != null) {
|
|
216
|
+
progress[key] = value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return progress;
|
|
220
|
+
}
|
|
221
|
+
function parseGenerationQueueStatus(data) {
|
|
222
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
const source = data;
|
|
226
|
+
return {
|
|
227
|
+
position: optionalNumber(source.position),
|
|
228
|
+
connectedWorkers: optionalNumber(source.connectedWorkers),
|
|
229
|
+
idleWorkers: optionalNumber(source.idleWorkers),
|
|
230
|
+
busyWorkers: optionalNumber(source.busyWorkers),
|
|
231
|
+
waitReason: typeof source.waitReason === "string" ? source.waitReason : null,
|
|
232
|
+
message: typeof source.message === "string" ? source.message : ""
|
|
233
|
+
};
|
|
234
|
+
}
|
|
178
235
|
function parseGenerationTranscription(data) {
|
|
179
236
|
if (data == null || typeof data !== "object" || !("status" in data)) {
|
|
180
237
|
return undefined;
|
package/dist/api/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** Models new generations can use (api/model_registry.json in diffio-ui); each has its own endpoint. */
|
|
2
|
+
export type ModelKey = "diffio-4.5-flash" | "diffio-4.5-pro";
|
|
2
3
|
export type DownloadType = "audio" | "video" | "transcript";
|
|
3
4
|
export type WebhookMode = "test" | "live";
|
|
4
5
|
export type WebhookEventType = "generation.queued" | "generation.processing" | "generation.failed" | "generation.completed";
|
|
@@ -7,13 +8,28 @@ export type TranscriptionStatus = "pending" | "available" | "unavailable";
|
|
|
7
8
|
export interface GenerationTranscription {
|
|
8
9
|
status: TranscriptionStatus;
|
|
9
10
|
}
|
|
11
|
+
/** The edge upload session create_project opened; the upload token stays inside the SDK. */
|
|
12
|
+
export interface ProjectUploadSession {
|
|
13
|
+
uploadSessionId: string;
|
|
14
|
+
edgeBaseUrl: string;
|
|
15
|
+
objectKey: string;
|
|
16
|
+
partSizeBytes: number;
|
|
17
|
+
maxBytes: number;
|
|
18
|
+
expiresAt: string;
|
|
19
|
+
}
|
|
20
|
+
/** Response of `/v1/complete_project_upload`; repeated calls return the same answer. */
|
|
21
|
+
export interface CompleteProjectUploadResponse {
|
|
22
|
+
apiProjectId: string;
|
|
23
|
+
status: "uploaded" | string;
|
|
24
|
+
sizeBytes: number | null;
|
|
25
|
+
}
|
|
26
|
+
/** A created project whose media `createProject` already uploaded through the edge and confirmed. */
|
|
10
27
|
export interface CreateProjectResponse {
|
|
11
28
|
apiProjectId: string;
|
|
12
|
-
|
|
13
|
-
uploadMethod: string;
|
|
29
|
+
upload: ProjectUploadSession;
|
|
14
30
|
objectPath: string;
|
|
15
|
-
bucket: string;
|
|
16
31
|
expiresAt: string;
|
|
32
|
+
uploadCompletion: CompleteProjectUploadResponse;
|
|
17
33
|
}
|
|
18
34
|
export interface ProjectSummary {
|
|
19
35
|
apiProjectId: string;
|
|
@@ -31,14 +47,16 @@ export interface ListProjectsResponse {
|
|
|
31
47
|
export interface CreateGenerationResponse {
|
|
32
48
|
generationId: string;
|
|
33
49
|
apiProjectId: string;
|
|
34
|
-
|
|
50
|
+
/** A string because generations created before a model was retired keep their original key. */
|
|
51
|
+
modelKey: string;
|
|
35
52
|
status: string;
|
|
36
53
|
idempotentReplay?: boolean;
|
|
37
54
|
}
|
|
38
55
|
export interface ProjectGenerationSummary {
|
|
39
56
|
generationId: string;
|
|
40
57
|
status: string;
|
|
41
|
-
|
|
58
|
+
/** Older generations keep the key of the model that produced them. */
|
|
59
|
+
modelKey?: string | null;
|
|
42
60
|
progress?: number | null;
|
|
43
61
|
createdAt?: string | null;
|
|
44
62
|
updatedAt?: string | null;
|
|
@@ -56,6 +74,26 @@ export interface GenerationProgressStage {
|
|
|
56
74
|
error?: string | null;
|
|
57
75
|
errorDetails?: string | null;
|
|
58
76
|
}
|
|
77
|
+
/** The one stage a generation is in, as `get_generation_progress` reports it. */
|
|
78
|
+
export type GenerationStage = "pending" | "preparing" | "transcribing" | "queued" | "starting" | "downloading" | "decoding" | "restoring" | "finalizing" | "uploading" | "complete" | "failed";
|
|
79
|
+
/** Progress within the current fleet stage; every field is optional and present only when known. */
|
|
80
|
+
export interface GenerationStageProgress {
|
|
81
|
+
overallPercent?: number;
|
|
82
|
+
stagePercent?: number;
|
|
83
|
+
bytesDone?: number;
|
|
84
|
+
bytesTotal?: number;
|
|
85
|
+
availableThroughSeconds?: number;
|
|
86
|
+
durationSeconds?: number;
|
|
87
|
+
}
|
|
88
|
+
/** Why a queued generation waits for a Mac fleet worker. */
|
|
89
|
+
export interface GenerationQueueStatus {
|
|
90
|
+
position: number | null;
|
|
91
|
+
connectedWorkers: number | null;
|
|
92
|
+
idleWorkers: number | null;
|
|
93
|
+
busyWorkers: number | null;
|
|
94
|
+
waitReason: "all_busy" | "no_workers" | "next_in_line" | string | null;
|
|
95
|
+
message: string;
|
|
96
|
+
}
|
|
59
97
|
export interface GenerationProgressResponse {
|
|
60
98
|
generationId: string;
|
|
61
99
|
apiProjectId: string;
|
|
@@ -64,6 +102,12 @@ export interface GenerationProgressResponse {
|
|
|
64
102
|
preProcessing: GenerationProgressStage;
|
|
65
103
|
inference: GenerationProgressStage;
|
|
66
104
|
restoredVideo?: GenerationProgressStage | null;
|
|
105
|
+
/** Omitted by older API versions. */
|
|
106
|
+
stage?: GenerationStage | string;
|
|
107
|
+
/** Present while a fleet stage reports progress. */
|
|
108
|
+
stageProgress?: GenerationStageProgress;
|
|
109
|
+
/** Present while the generation waits in the fleet queue. */
|
|
110
|
+
queue?: GenerationQueueStatus;
|
|
67
111
|
/** Independent of media completion; omitted by older API versions. */
|
|
68
112
|
transcription?: GenerationTranscription;
|
|
69
113
|
error?: string | null;
|
|
@@ -76,7 +120,6 @@ export interface GenerationDownloadResponse {
|
|
|
76
120
|
downloadUrl: string;
|
|
77
121
|
fileName: string;
|
|
78
122
|
storagePath: string;
|
|
79
|
-
bucket: string;
|
|
80
123
|
mimeType: string;
|
|
81
124
|
}
|
|
82
125
|
export interface AudioIsolationResult {
|
|
@@ -128,8 +171,9 @@ export interface GenerationWebhookEvent {
|
|
|
128
171
|
generationId: string;
|
|
129
172
|
status: GenerationWebhookStatus | string;
|
|
130
173
|
hasVideo?: boolean | null;
|
|
131
|
-
|
|
132
|
-
|
|
174
|
+
/** Older generations keep the key of the model that produced them. */
|
|
175
|
+
modelKey?: string | null;
|
|
176
|
+
/** A completed generation may still have a pending or unavailable transcript. */
|
|
133
177
|
transcription?: GenerationTranscription;
|
|
134
178
|
error?: string | null;
|
|
135
179
|
errorDetails?: string | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DiffioUploadError } from "../errors";
|
|
2
|
+
/** The upload session create_project returns; `uploadToken` authorizes edge calls for this one object. */
|
|
3
|
+
export interface EdgeUploadSession {
|
|
4
|
+
uploadSessionId: string;
|
|
5
|
+
edgeBaseUrl: string;
|
|
6
|
+
uploadToken: string;
|
|
7
|
+
objectKey: string;
|
|
8
|
+
partSizeBytes: number;
|
|
9
|
+
maxBytes: number;
|
|
10
|
+
expiresAt: string;
|
|
11
|
+
}
|
|
12
|
+
/** One planned multipart part: bytes [startByte, endByte) of the file. */
|
|
13
|
+
export interface EdgeUploadPartPlan {
|
|
14
|
+
partNumber: number;
|
|
15
|
+
startByte: number;
|
|
16
|
+
endByte: number;
|
|
17
|
+
}
|
|
18
|
+
/** A finished part as the edge acknowledges it, replayed on completion. */
|
|
19
|
+
export interface EdgeUploadPartReceipt {
|
|
20
|
+
partNumber: number;
|
|
21
|
+
etag: string;
|
|
22
|
+
}
|
|
23
|
+
/** The edge's answer to `POST /v1/uploads/complete`. */
|
|
24
|
+
export interface EdgeUploadCompletion {
|
|
25
|
+
objectKey: string;
|
|
26
|
+
sizeBytes: number;
|
|
27
|
+
etag: string;
|
|
28
|
+
}
|
|
29
|
+
/** One HTTP request to the edge; the body is a fixed-length buffer because parts need a Content-Length. */
|
|
30
|
+
export interface EdgeUploadHttpRequest {
|
|
31
|
+
method: "POST" | "PUT";
|
|
32
|
+
url: string;
|
|
33
|
+
bearerToken: string;
|
|
34
|
+
body: Uint8Array | string;
|
|
35
|
+
contentType: string;
|
|
36
|
+
}
|
|
37
|
+
/** The edge's answer to one request. Transport failures are thrown instead. */
|
|
38
|
+
export interface EdgeUploadHttpResponse {
|
|
39
|
+
status: number;
|
|
40
|
+
bodyText: string;
|
|
41
|
+
}
|
|
42
|
+
/** Inputs for one upload of a project's original media through the edge. */
|
|
43
|
+
export interface EdgeUploadOptions {
|
|
44
|
+
session: EdgeUploadSession;
|
|
45
|
+
sizeBytes: number;
|
|
46
|
+
/** Reads bytes [startByte, endByte) of the media; called once per part. */
|
|
47
|
+
readPartBytes: (startByte: number, endByte: number) => Promise<Uint8Array>;
|
|
48
|
+
sendEdgeRequest: (request: EdgeUploadHttpRequest) => Promise<EdgeUploadHttpResponse>;
|
|
49
|
+
partConcurrency?: number;
|
|
50
|
+
maxAttempts?: number;
|
|
51
|
+
sleep?: (delayMs: number) => Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/** Splits a file into fixed-size parts numbered from 1; an empty file is one empty part. */
|
|
54
|
+
export declare function planEdgeUploadParts(sizeBytes: number, partSizeBytes: number): EdgeUploadPartPlan[];
|
|
55
|
+
/** Backoff before retry `attempt` (2 = first retry): 1 s, 2 s, 4 s, capped at 8 s. */
|
|
56
|
+
export declare function resolveEdgeUploadRetryDelayMs(attempt: number): number;
|
|
57
|
+
/** Validates the `upload` object of a create_project response; null when it is missing or incomplete. */
|
|
58
|
+
export declare function parseEdgeUploadSession(value: unknown): EdgeUploadSession | null;
|
|
59
|
+
/** Classifies a non-2xx edge response (`{"error": {"code", "message"}}`) as a typed upload error. */
|
|
60
|
+
export declare function classifyEdgeUploadResponse(response: EdgeUploadHttpResponse): DiffioUploadError;
|
|
61
|
+
/** Uploads media through the edge in parts and completes the multipart object; the caller then confirms it. */
|
|
62
|
+
export declare function uploadProjectMediaToEdge(options: EdgeUploadOptions): Promise<EdgeUploadCompletion>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Client for the edge Worker upload API ("Uploads" in diffio-ui specs/MacFleetArchitecture.md).
|
|
3
|
+
// create_project opens the session and owns every auth decision; the edge only checks the
|
|
4
|
+
// session's upload token and streams fixed-size parts into R2. The flow mirrors the web app's
|
|
5
|
+
// uploader (diffio-ui app/services/edgeUploads.ts): start, PUT parts numbered from 1 with up to
|
|
6
|
+
// four attempts each, complete with the sorted receipts, and abort on a fatal failure.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.planEdgeUploadParts = planEdgeUploadParts;
|
|
9
|
+
exports.resolveEdgeUploadRetryDelayMs = resolveEdgeUploadRetryDelayMs;
|
|
10
|
+
exports.parseEdgeUploadSession = parseEdgeUploadSession;
|
|
11
|
+
exports.classifyEdgeUploadResponse = classifyEdgeUploadResponse;
|
|
12
|
+
exports.uploadProjectMediaToEdge = uploadProjectMediaToEdge;
|
|
13
|
+
const errors_1 = require("../errors");
|
|
14
|
+
const DEFAULT_EDGE_UPLOAD_PART_CONCURRENCY = 3;
|
|
15
|
+
const DEFAULT_EDGE_UPLOAD_MAX_ATTEMPTS = 4;
|
|
16
|
+
const defaultSleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
17
|
+
const isUnknownRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
/** Splits a file into fixed-size parts numbered from 1; an empty file is one empty part. */
|
|
19
|
+
function planEdgeUploadParts(sizeBytes, partSizeBytes) {
|
|
20
|
+
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 0)
|
|
21
|
+
throw new Error("sizeBytes must be a non-negative integer");
|
|
22
|
+
if (!Number.isSafeInteger(partSizeBytes) || partSizeBytes <= 0) {
|
|
23
|
+
throw new Error("partSizeBytes must be a positive integer");
|
|
24
|
+
}
|
|
25
|
+
if (sizeBytes === 0)
|
|
26
|
+
return [{ partNumber: 1, startByte: 0, endByte: 0 }];
|
|
27
|
+
const parts = [];
|
|
28
|
+
for (let startByte = 0, partNumber = 1; startByte < sizeBytes; startByte += partSizeBytes, partNumber += 1) {
|
|
29
|
+
parts.push({ partNumber, startByte, endByte: Math.min(sizeBytes, startByte + partSizeBytes) });
|
|
30
|
+
}
|
|
31
|
+
return parts;
|
|
32
|
+
}
|
|
33
|
+
/** Backoff before retry `attempt` (2 = first retry): 1 s, 2 s, 4 s, capped at 8 s. */
|
|
34
|
+
function resolveEdgeUploadRetryDelayMs(attempt) {
|
|
35
|
+
return Math.min(8000, 1000 * 2 ** Math.max(0, attempt - 2));
|
|
36
|
+
}
|
|
37
|
+
/** Validates the `upload` object of a create_project response; null when it is missing or incomplete. */
|
|
38
|
+
function parseEdgeUploadSession(value) {
|
|
39
|
+
if (!isUnknownRecord(value))
|
|
40
|
+
return null;
|
|
41
|
+
const { uploadSessionId, uploadToken, edgeBaseUrl, objectKey, partSizeBytes, maxBytes, expiresAt } = value;
|
|
42
|
+
if (typeof uploadSessionId !== "string" || !uploadSessionId ||
|
|
43
|
+
typeof uploadToken !== "string" || !uploadToken ||
|
|
44
|
+
typeof edgeBaseUrl !== "string" || !/^https?:\/\//i.test(edgeBaseUrl) ||
|
|
45
|
+
typeof objectKey !== "string" || !objectKey ||
|
|
46
|
+
typeof partSizeBytes !== "number" || !Number.isSafeInteger(partSizeBytes) || partSizeBytes <= 0 ||
|
|
47
|
+
typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
uploadSessionId,
|
|
52
|
+
uploadToken,
|
|
53
|
+
edgeBaseUrl: edgeBaseUrl.replace(/\/+$/, ""),
|
|
54
|
+
objectKey,
|
|
55
|
+
partSizeBytes,
|
|
56
|
+
maxBytes,
|
|
57
|
+
expiresAt: typeof expiresAt === "string" ? expiresAt : ""
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const parseJsonBody = (bodyText) => {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(bodyText);
|
|
63
|
+
return isUnknownRecord(parsed) ? parsed : {};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/** Classifies a non-2xx edge response (`{"error": {"code", "message"}}`) as a typed upload error. */
|
|
70
|
+
function classifyEdgeUploadResponse(response) {
|
|
71
|
+
const body = parseJsonBody(response.bodyText);
|
|
72
|
+
const error = isUnknownRecord(body.error) ? body.error : {};
|
|
73
|
+
const message = typeof error.message === "string" && error.message
|
|
74
|
+
? error.message
|
|
75
|
+
: `Upload failed with HTTP ${response.status}`;
|
|
76
|
+
const edgeErrorCode = typeof error.code === "string" ? error.code : undefined;
|
|
77
|
+
const responseBody = Object.keys(body).length > 0 ? body : response.bodyText || null;
|
|
78
|
+
const base = { statusCode: response.status, responseBody, edgeErrorCode };
|
|
79
|
+
let code = "upload/rejected";
|
|
80
|
+
let retryable = false;
|
|
81
|
+
if (response.status === 413)
|
|
82
|
+
code = "upload/too-large";
|
|
83
|
+
else if (response.status === 401 || response.status === 403)
|
|
84
|
+
code = "upload/unauthorized";
|
|
85
|
+
else if (response.status === 408 || response.status === 429 || response.status >= 500) {
|
|
86
|
+
code = "upload/server";
|
|
87
|
+
retryable = true;
|
|
88
|
+
}
|
|
89
|
+
return new errors_1.DiffioUploadError(code, message, { ...base, retryable });
|
|
90
|
+
}
|
|
91
|
+
const isAbortError = (error) => isUnknownRecord(error) && error.name === "AbortError";
|
|
92
|
+
/** Turns a thrown transport failure into an upload error; caller aborts are final, other failures retryable. */
|
|
93
|
+
function toEdgeUploadError(error) {
|
|
94
|
+
if (error instanceof errors_1.DiffioUploadError)
|
|
95
|
+
return error;
|
|
96
|
+
if (isAbortError(error) || (error instanceof Error && error.name === "AbortError")) {
|
|
97
|
+
return new errors_1.DiffioUploadError("upload/canceled", "Upload was canceled.");
|
|
98
|
+
}
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
return new errors_1.DiffioUploadError("upload/network", message || "Network error during upload", { retryable: true });
|
|
101
|
+
}
|
|
102
|
+
/** Uploads media through the edge in parts and completes the multipart object; the caller then confirms it. */
|
|
103
|
+
async function uploadProjectMediaToEdge(options) {
|
|
104
|
+
const { session, sizeBytes, readPartBytes, sendEdgeRequest } = options;
|
|
105
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
106
|
+
const concurrency = Math.max(1, options.partConcurrency ?? DEFAULT_EDGE_UPLOAD_PART_CONCURRENCY);
|
|
107
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_EDGE_UPLOAD_MAX_ATTEMPTS);
|
|
108
|
+
if (sizeBytes > session.maxBytes) {
|
|
109
|
+
throw new errors_1.DiffioUploadError("upload/too-large", `This file is ${sizeBytes} bytes; the upload limit is ${session.maxBytes} bytes.`);
|
|
110
|
+
}
|
|
111
|
+
const sendOnce = async (request) => {
|
|
112
|
+
let response;
|
|
113
|
+
try {
|
|
114
|
+
response = await sendEdgeRequest({ ...request, bearerToken: session.uploadToken });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throw toEdgeUploadError(error);
|
|
118
|
+
}
|
|
119
|
+
if (response.status < 200 || response.status >= 300)
|
|
120
|
+
throw classifyEdgeUploadResponse(response);
|
|
121
|
+
return parseJsonBody(response.bodyText);
|
|
122
|
+
};
|
|
123
|
+
// Set by the first part that fails for good; the other lanes then stop instead of retrying.
|
|
124
|
+
let fatalError = null;
|
|
125
|
+
// Start, parts, and complete are all safe to repeat: a repeated start only opens a fresh
|
|
126
|
+
// multipart upload, and the edge answers a repeated complete from the stored object.
|
|
127
|
+
const sendWithRetries = async (describe, readResult) => {
|
|
128
|
+
for (let attempt = 1;; attempt += 1) {
|
|
129
|
+
try {
|
|
130
|
+
return readResult(await sendOnce(await describe()));
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const uploadError = toEdgeUploadError(error);
|
|
134
|
+
if (fatalError || !uploadError.retryable || attempt >= maxAttempts)
|
|
135
|
+
throw fatalError ?? uploadError;
|
|
136
|
+
await sleep(resolveEdgeUploadRetryDelayMs(attempt + 1));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const jsonRequest = (path, body) => ({
|
|
141
|
+
method: "POST",
|
|
142
|
+
url: `${session.edgeBaseUrl}${path}`,
|
|
143
|
+
body: JSON.stringify(body),
|
|
144
|
+
contentType: "application/json"
|
|
145
|
+
});
|
|
146
|
+
const started = await sendWithRetries(() => jsonRequest("/v1/uploads/start", {}), (body) => {
|
|
147
|
+
if (typeof body.uploadId !== "string" || !body.uploadId) {
|
|
148
|
+
throw new errors_1.DiffioUploadError("upload/invalid-response", "The edge did not return an uploadId.");
|
|
149
|
+
}
|
|
150
|
+
const partSizeBytes = typeof body.partSizeBytes === "number" && Number.isSafeInteger(body.partSizeBytes) &&
|
|
151
|
+
body.partSizeBytes > 0
|
|
152
|
+
? body.partSizeBytes
|
|
153
|
+
: session.partSizeBytes;
|
|
154
|
+
return { uploadId: body.uploadId, partSizeBytes };
|
|
155
|
+
});
|
|
156
|
+
const { uploadId } = started;
|
|
157
|
+
const parts = planEdgeUploadParts(sizeBytes, started.partSizeBytes);
|
|
158
|
+
const pending = [...parts];
|
|
159
|
+
const receipts = [];
|
|
160
|
+
const uploadPart = async (part) => {
|
|
161
|
+
let bytes = null;
|
|
162
|
+
const receipt = await sendWithRetries(async () => {
|
|
163
|
+
if (fatalError)
|
|
164
|
+
throw fatalError;
|
|
165
|
+
bytes = bytes ?? await readPartBytes(part.startByte, part.endByte);
|
|
166
|
+
return {
|
|
167
|
+
method: "PUT",
|
|
168
|
+
url: `${session.edgeBaseUrl}/v1/uploads/parts/${part.partNumber}?uploadId=${encodeURIComponent(uploadId)}`,
|
|
169
|
+
body: bytes,
|
|
170
|
+
contentType: "application/octet-stream"
|
|
171
|
+
};
|
|
172
|
+
}, (body) => {
|
|
173
|
+
if (typeof body.etag !== "string" || !body.etag) {
|
|
174
|
+
throw new errors_1.DiffioUploadError("upload/invalid-response", `Part ${part.partNumber} returned no etag.`);
|
|
175
|
+
}
|
|
176
|
+
return { partNumber: part.partNumber, etag: body.etag };
|
|
177
|
+
});
|
|
178
|
+
receipts.push(receipt);
|
|
179
|
+
};
|
|
180
|
+
const runLane = async () => {
|
|
181
|
+
while (pending.length > 0 && !fatalError) {
|
|
182
|
+
const part = pending.shift();
|
|
183
|
+
if (!part)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
await uploadPart(part);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
fatalError = fatalError ?? toEdgeUploadError(error);
|
|
190
|
+
throw fatalError;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const lanes = Array.from({ length: Math.min(concurrency, parts.length) }, () => runLane());
|
|
195
|
+
const laneResults = await Promise.allSettled(lanes);
|
|
196
|
+
const failedLane = laneResults.find((result) => result.status === "rejected");
|
|
197
|
+
if (failedLane) {
|
|
198
|
+
const uploadError = fatalError ?? toEdgeUploadError(failedLane.reason);
|
|
199
|
+
// Release the partial upload; a failure here never masks the transfer error.
|
|
200
|
+
await sendOnce(jsonRequest("/v1/uploads/abort", { uploadId })).catch(() => undefined);
|
|
201
|
+
throw uploadError;
|
|
202
|
+
}
|
|
203
|
+
receipts.sort((left, right) => left.partNumber - right.partNumber);
|
|
204
|
+
const completion = await sendWithRetries(() => jsonRequest("/v1/uploads/complete", { uploadId, parts: receipts }), (body) => ({
|
|
205
|
+
objectKey: typeof body.objectKey === "string" && body.objectKey ? body.objectKey : session.objectKey,
|
|
206
|
+
sizeBytes: typeof body.sizeBytes === "number" ? body.sizeBytes : sizeBytes,
|
|
207
|
+
etag: typeof body.etag === "string" ? body.etag : ""
|
|
208
|
+
}));
|
|
209
|
+
if (completion.sizeBytes !== sizeBytes) {
|
|
210
|
+
throw new errors_1.DiffioUploadError("upload/invalid-response", `The edge stored ${completion.sizeBytes} bytes but the file has ${sizeBytes} bytes.`);
|
|
211
|
+
}
|
|
212
|
+
return completion;
|
|
213
|
+
}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -9,6 +9,23 @@ export declare class DiffioApiError extends DiffioError {
|
|
|
9
9
|
responseBody?: unknown;
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
+
/** Stable failure codes for uploads through the edge Worker, modeled on the Diffio web uploader's codes. */
|
|
13
|
+
export type EdgeUploadErrorCode = "upload/too-large" | "upload/unauthorized" | "upload/rejected" | "upload/network" | "upload/server" | "upload/invalid-response" | "upload/canceled";
|
|
14
|
+
/** A failed project media upload; `apiProjectId` names the project whose upload did not finish. */
|
|
15
|
+
export declare class DiffioUploadError extends DiffioApiError {
|
|
16
|
+
uploadErrorCode: EdgeUploadErrorCode;
|
|
17
|
+
/** The edge's machine-readable `error.code`, such as `token_expired`, when it sent one. */
|
|
18
|
+
edgeErrorCode?: string;
|
|
19
|
+
retryable: boolean;
|
|
20
|
+
apiProjectId?: string;
|
|
21
|
+
constructor(uploadErrorCode: EdgeUploadErrorCode, message: string, options?: {
|
|
22
|
+
statusCode?: number;
|
|
23
|
+
responseBody?: unknown;
|
|
24
|
+
edgeErrorCode?: string;
|
|
25
|
+
retryable?: boolean;
|
|
26
|
+
apiProjectId?: string;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
12
29
|
export declare class DiffioTimeoutError extends DiffioError {
|
|
13
30
|
constructor(message: string);
|
|
14
31
|
}
|
package/dist/errors/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DiffioTimeoutError = exports.DiffioApiError = exports.DiffioError = void 0;
|
|
3
|
+
exports.DiffioTimeoutError = exports.DiffioUploadError = exports.DiffioApiError = exports.DiffioError = void 0;
|
|
4
4
|
class DiffioError extends Error {
|
|
5
5
|
constructor(message) {
|
|
6
6
|
super(message);
|
|
@@ -17,6 +17,18 @@ class DiffioApiError extends DiffioError {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
exports.DiffioApiError = DiffioApiError;
|
|
20
|
+
/** A failed project media upload; `apiProjectId` names the project whose upload did not finish. */
|
|
21
|
+
class DiffioUploadError extends DiffioApiError {
|
|
22
|
+
constructor(uploadErrorCode, message, options) {
|
|
23
|
+
super(message, { statusCode: options?.statusCode, responseBody: options?.responseBody });
|
|
24
|
+
this.name = "DiffioUploadError";
|
|
25
|
+
this.uploadErrorCode = uploadErrorCode;
|
|
26
|
+
this.edgeErrorCode = options?.edgeErrorCode;
|
|
27
|
+
this.retryable = options?.retryable ?? false;
|
|
28
|
+
this.apiProjectId = options?.apiProjectId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.DiffioUploadError = DiffioUploadError;
|
|
20
32
|
class DiffioTimeoutError extends DiffioError {
|
|
21
33
|
constructor(message) {
|
|
22
34
|
super(message);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const DIFFIO_SDK_VERSION = "0.1
|
|
1
|
+
export declare const DIFFIO_SDK_VERSION = "0.2.1";
|
package/dist/version.js
CHANGED