@hypit/hypit 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<
|
|
19
|
+
<img alt="Stars" src="https://img.shields.io/github/stars/hypit-ai/hypit?style=flat-square&color=FFD700&logo=github&logoColor=white&label=Stars">
|
|
20
20
|
<a href="https://github.com/hypit-ai/hypit/blob/main/package.json"><img alt="Node 22.15+" src="https://img.shields.io/badge/Node.js-22.15%2B-5FA04E?style=flat-square&logo=nodedotjs&logoColor=white"></a>
|
|
21
21
|
<a href="https://github.com/hypit-ai/hypit/blob/main/package.json"><img alt="pnpm 10.33" src="https://img.shields.io/badge/pnpm-10.33-F69220?style=flat-square&logo=pnpm&logoColor=white"></a>
|
|
22
22
|
<a href="https://github.com/hypit-ai/hypit/blob/main/package.json"><img alt="TypeScript 5.9" src="https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square&logo=typescript&logoColor=white"></a>
|
package/package.json
CHANGED
|
@@ -74,9 +74,13 @@ to the selected deployment's origin or an existing `/v1`/`/v1beta` base. The Run
|
|
|
74
74
|
normalizes it to `/v1`; missing or insufficient user
|
|
75
75
|
credentials should be resolved at [hypit.ai](https://hypit.ai). Referenced image, audio and video
|
|
76
76
|
Resources are uploaded through a session from `POST /v1/files/uploads`, followed by the private
|
|
77
|
-
regional multipart instructions
|
|
77
|
+
regional multipart instructions or `api_multipart` file POST selected by HypiHub. The latter sends
|
|
78
|
+
one multipart/form-data file to the selected service's `/v1/files`, preserving reference purpose
|
|
79
|
+
and person classification; an uncertain file POST is not repeated automatically.
|
|
80
|
+
For direct multipart uploads, the Provider follows the server-selected part
|
|
78
81
|
size and part concurrency, retries a failed part with a fresh signed URL, completes or cancels that
|
|
79
|
-
one upload, and then passes the returned HTTPS URL to generation or transcription.
|
|
82
|
+
one upload, and then passes the returned HTTPS URL to generation or transcription. Signing requests
|
|
83
|
+
contain at most the service's 128-part limit; all batches belong to the same upload. One
|
|
80
84
|
Resource identity with the same declared person-reference classification is uploaded once within one Runtime operation. Hypit keeps no upload catalog or
|
|
81
85
|
cross-Build cache. Seedance visual references can carry `personReference` in their media fields;
|
|
82
86
|
the mapping declares it as a resource-transport field and the upload session receives
|
|
@@ -97,13 +101,23 @@ selected Store is writable. Its Endpoint receives only the credential slot it de
|
|
|
97
101
|
operation for replacing that same slot; it cannot enumerate the Store, choose another key or read
|
|
98
102
|
another Endpoint's credentials. A raw credential remains an ordinary static API key.
|
|
99
103
|
|
|
104
|
+
Refresh uses `oauthRequestTimeoutMs` during generation, transcription, uploads and pricing too.
|
|
105
|
+
It completes before the subsequent API request starts its own deadline. A stalled refresh reports
|
|
106
|
+
an OAuth refresh timeout without starting that API request.
|
|
107
|
+
|
|
100
108
|
The service currently requires whole-file and per-part SHA-256 values as fields of its signed upload
|
|
101
109
|
protocol. They exist only while transferring bytes; Hypit never uses them as Resource identity,
|
|
102
110
|
Result metadata, lookup keys or reuse evidence. Signed URLs and their query credentials are removed
|
|
103
111
|
from surfaced upload errors.
|
|
104
112
|
|
|
105
113
|
The default remote alignment model is `victor-upmeet/whisperx`; `transcriptionModel` may select another
|
|
106
|
-
HypiHub model that exposes the `transcriptions` route.
|
|
114
|
+
HypiHub model that exposes the `transcriptions` route. When transcription response headers include
|
|
115
|
+
`X-Request-Id`, the Provider records it in the existing execution diagnostics before reading the body.
|
|
116
|
+
A matching same-service `Location` is retained as the authenticated result lookup URL, including on
|
|
117
|
+
an HTTP failure or interrupted response body. This is a receipt for investigation, not automatic
|
|
118
|
+
resubmission or Build restoration. No receipt can be recorded if no response headers arrive.
|
|
119
|
+
|
|
120
|
+
`hypit doctor` reads the authenticated model
|
|
107
121
|
catalog to verify configured capabilities; ordinary preflight never makes that request. The package
|
|
108
122
|
declares HypiHub's public pricing page, `https://hypit.ai/commercial/pricing/`, as its price source.
|
|
109
123
|
For each selected Need, `readPricing` resolves the corresponding HypiHub model and returns the service's
|
|
@@ -65,7 +65,7 @@ export function createHypiHubAuth(options: {
|
|
|
65
65
|
throw new Error("HypiHub OAuth credential is read-only; run hypit auth login with a writable Credential Store");
|
|
66
66
|
}
|
|
67
67
|
refreshing = (async () => {
|
|
68
|
-
const deadline = requestDeadline(options.requestTimeoutMs);
|
|
68
|
+
const deadline = requestDeadline(options.requestTimeoutMs, () => new Error("HypiHub OAuth refresh timed out"));
|
|
69
69
|
try {
|
|
70
70
|
const response = await deadline.wait(options.fetch(tokenEndpoint, {
|
|
71
71
|
method: "POST",
|
|
@@ -100,12 +100,14 @@ class HypiHubHttpError extends Error {
|
|
|
100
100
|
class HypiHubClient {
|
|
101
101
|
readonly baseUrl: string;
|
|
102
102
|
readonly timeout: number;
|
|
103
|
+
readonly oauthTimeout: number;
|
|
103
104
|
readonly downloadAttempts: number;
|
|
104
105
|
readonly fetcher: typeof globalThis.fetch;
|
|
105
106
|
readonly uploader: HypiHubUploader;
|
|
106
107
|
constructor(options: {
|
|
107
108
|
readonly baseUrl: string;
|
|
108
109
|
readonly timeout: number;
|
|
110
|
+
readonly oauthTimeout: number;
|
|
109
111
|
readonly uploadConcurrency: number;
|
|
110
112
|
readonly uploadPartTimeout: number;
|
|
111
113
|
readonly uploadPartAttempts: number;
|
|
@@ -114,6 +116,7 @@ class HypiHubClient {
|
|
|
114
116
|
}) {
|
|
115
117
|
this.baseUrl = options.baseUrl.replace(/\/$/u, "");
|
|
116
118
|
this.timeout = options.timeout;
|
|
119
|
+
this.oauthTimeout = options.oauthTimeout;
|
|
117
120
|
this.downloadAttempts = options.downloadAttempts;
|
|
118
121
|
this.fetcher = options.fetcher;
|
|
119
122
|
this.uploader = new HypiHubUploader({
|
|
@@ -125,14 +128,18 @@ class HypiHubClient {
|
|
|
125
128
|
fetch: this.fetcher,
|
|
126
129
|
});
|
|
127
130
|
}
|
|
128
|
-
async json(path: string, auth: HypiHubAuth, init: RequestInit = {}, refreshOnUnauthorized = true
|
|
131
|
+
async json(path: string, auth: HypiHubAuth, init: RequestInit = {}, refreshOnUnauthorized = true,
|
|
132
|
+
onResponse?: (response: Response) => Promise<void>): Promise<Record<string, unknown>> {
|
|
133
|
+
const token = await auth.token();
|
|
129
134
|
const deadline = requestDeadline(this.timeout);
|
|
130
135
|
try {
|
|
131
|
-
const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, { ...init, signal: deadline.signal, headers: { authorization: `Bearer ${
|
|
136
|
+
const response = await deadline.wait(this.fetcher(`${this.baseUrl}${path}`, { ...init, signal: deadline.signal, headers: { authorization: `Bearer ${token}`, ...(init.headers ?? {}) } }));
|
|
137
|
+
if (response.status !== 401 && onResponse !== undefined) await deadline.wait(onResponse(response));
|
|
132
138
|
const text = await deadline.wait(response.text()); let body: unknown = {};
|
|
133
139
|
if (response.status === 401 && refreshOnUnauthorized && auth.canRefresh()) {
|
|
140
|
+
deadline.finish();
|
|
134
141
|
await auth.refresh();
|
|
135
|
-
return await this.json(path, auth, init, false);
|
|
142
|
+
return await this.json(path, auth, init, false, onResponse);
|
|
136
143
|
}
|
|
137
144
|
if (!response.ok) throw new HypiHubHttpError(response.status, `HypiHub returned HTTP ${response.status}: ${text.slice(0, 300)}`);
|
|
138
145
|
try { body = text.length === 0 ? {} : JSON.parse(text); } catch { throw new Error(`HypiHub returned invalid JSON (${response.status})`); }
|
|
@@ -165,24 +172,40 @@ class HypiHubClient {
|
|
|
165
172
|
...(personReference === undefined ? {} : { isPersonReference: personReference }) }, auth);
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
async transcribe(body: Record<string, unknown>, auth: HypiHubAuth
|
|
175
|
+
async transcribe(body: Record<string, unknown>, auth: HypiHubAuth,
|
|
176
|
+
report?: (message: string) => Promise<void>): Promise<Record<string, unknown>> {
|
|
169
177
|
return await this.json("/audio/transcriptions", auth, {
|
|
170
178
|
method: "POST",
|
|
171
179
|
headers: { "content-type": "application/json" },
|
|
172
180
|
body: JSON.stringify(body),
|
|
181
|
+
}, true, async (response) => {
|
|
182
|
+
const requestId = response.headers.get("x-request-id");
|
|
183
|
+
if (requestId === null || !/^[A-Za-z0-9_-]{1,200}$/u.test(requestId)) return;
|
|
184
|
+
let retrieval = "";
|
|
185
|
+
const location = response.headers.get("location");
|
|
186
|
+
if (location !== null) {
|
|
187
|
+
try {
|
|
188
|
+
const url = new URL(location, this.baseUrl);
|
|
189
|
+
const expected = new URL(`${this.baseUrl}/audio/transcriptions/${encodeURIComponent(requestId)}`);
|
|
190
|
+
if (url.href === expected.href) retrieval = `; result lookup: ${url.href}`;
|
|
191
|
+
} catch { /* A malformed Location does not erase the request identifier. */ }
|
|
192
|
+
}
|
|
193
|
+
await report?.(`HypiHub transcription request ${requestId} (HTTP ${response.status})${retrieval}`);
|
|
173
194
|
});
|
|
174
195
|
}
|
|
175
196
|
async speech(auth: HypiHubAuth, body: Record<string, unknown>, refreshOnUnauthorized = true): Promise<readonly { readonly bytes: Uint8Array; readonly mediaType: string }[]> {
|
|
197
|
+
const token = await auth.token();
|
|
176
198
|
const deadline = requestDeadline(this.timeout);
|
|
177
199
|
try {
|
|
178
200
|
const response = await deadline.wait(this.fetcher(`${this.baseUrl}/audio/speech`, {
|
|
179
201
|
method: "POST",
|
|
180
|
-
headers: { authorization: `Bearer ${
|
|
202
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
181
203
|
body: JSON.stringify({ ...body, output: "b64_json" }),
|
|
182
204
|
signal: deadline.signal,
|
|
183
205
|
}));
|
|
184
206
|
const bytes = new Uint8Array(await deadline.wait(response.arrayBuffer()));
|
|
185
207
|
if (response.status === 401 && refreshOnUnauthorized && auth.canRefresh()) {
|
|
208
|
+
deadline.finish();
|
|
186
209
|
await auth.refresh();
|
|
187
210
|
return await this.speech(auth, body, false);
|
|
188
211
|
}
|
|
@@ -226,7 +249,7 @@ function authFor(context: EndpointInvocationContext, client: HypiHubClient): Hyp
|
|
|
226
249
|
return createHypiHubAuth({
|
|
227
250
|
credential: credential(context.credentials),
|
|
228
251
|
baseUrl: client.baseUrl,
|
|
229
|
-
requestTimeoutMs: client.
|
|
252
|
+
requestTimeoutMs: client.oauthTimeout,
|
|
230
253
|
fetch: client.fetcher,
|
|
231
254
|
});
|
|
232
255
|
}
|
|
@@ -235,7 +258,7 @@ function pricingAuth(credentials: Readonly<Record<string, EndpointCredential>>,
|
|
|
235
258
|
return createHypiHubAuth({
|
|
236
259
|
credential: credential(credentials),
|
|
237
260
|
baseUrl: client.baseUrl,
|
|
238
|
-
requestTimeoutMs: client.
|
|
261
|
+
requestTimeoutMs: client.oauthTimeout,
|
|
239
262
|
fetch: client.fetcher,
|
|
240
263
|
});
|
|
241
264
|
}
|
|
@@ -311,6 +334,7 @@ export async function diagnoseHypiHubProvider(
|
|
|
311
334
|
const client = new HypiHubClient({
|
|
312
335
|
baseUrl: apiBaseUrl(options.baseUrl ?? "https://hypit.ai/v1"),
|
|
313
336
|
timeout: options.requestTimeoutMs ?? 30_000,
|
|
337
|
+
oauthTimeout: options.oauthRequestTimeoutMs ?? 30_000,
|
|
314
338
|
uploadConcurrency: options.uploadConcurrency ?? 8,
|
|
315
339
|
uploadPartTimeout: options.uploadPartTimeoutMs ?? 5 * 60_000,
|
|
316
340
|
uploadPartAttempts: options.uploadPartAttempts ?? 3,
|
|
@@ -320,7 +344,7 @@ export async function diagnoseHypiHubProvider(
|
|
|
320
344
|
const auth = createHypiHubAuth({
|
|
321
345
|
credential: { secret: apiKey },
|
|
322
346
|
baseUrl: client.baseUrl,
|
|
323
|
-
requestTimeoutMs: client.
|
|
347
|
+
requestTimeoutMs: client.oauthTimeout,
|
|
324
348
|
fetch: client.fetcher,
|
|
325
349
|
});
|
|
326
350
|
const response = await client.json("/models", auth);
|
|
@@ -487,6 +511,7 @@ export function createHypiHubProvider(options: CreateHypiHubProviderOptions = {}
|
|
|
487
511
|
const client = new HypiHubClient({
|
|
488
512
|
baseUrl: apiBaseUrl(options.baseUrl ?? "https://hypit.ai/v1"),
|
|
489
513
|
timeout: requestTimeoutMs,
|
|
514
|
+
oauthTimeout: oauthRequestTimeoutMs,
|
|
490
515
|
uploadConcurrency: options.uploadConcurrency ?? 8,
|
|
491
516
|
uploadPartTimeout: uploadPartTimeoutMs,
|
|
492
517
|
uploadPartAttempts,
|
|
@@ -496,6 +521,7 @@ export function createHypiHubProvider(options: CreateHypiHubProviderOptions = {}
|
|
|
496
521
|
const pricingClient = new HypiHubClient({
|
|
497
522
|
baseUrl: apiBaseUrl(options.baseUrl ?? "https://hypit.ai/v1"),
|
|
498
523
|
timeout: pricingRequestTimeoutMs,
|
|
524
|
+
oauthTimeout: oauthRequestTimeoutMs,
|
|
499
525
|
uploadConcurrency: options.uploadConcurrency ?? 8,
|
|
500
526
|
uploadPartTimeout: uploadPartTimeoutMs,
|
|
501
527
|
uploadPartAttempts,
|
|
@@ -531,7 +557,7 @@ export function createHypiHubProvider(options: CreateHypiHubProviderOptions = {}
|
|
|
531
557
|
response_format: "verbose_json",
|
|
532
558
|
language: request.language,
|
|
533
559
|
timestamp_granularities: ["segment", "word"],
|
|
534
|
-
}, auth);
|
|
560
|
+
}, auth, async (message) => { await context.reportDiagnostic?.({ level: "info", message }); });
|
|
535
561
|
const evidence = sealAlignedTranscriptEvidence({
|
|
536
562
|
passages: interpretWhisperXTranscript(response as WhisperXTranscriptResponse, request.sampleFrames),
|
|
537
563
|
});
|
|
@@ -120,7 +120,7 @@ async function acquireUploadSlot(origin: string, auth: UploadAuth, limit: number
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/** Uploads media
|
|
123
|
+
/** Uploads media using the transfer mode negotiated by the selected HypiHub service. */
|
|
124
124
|
export class HypiHubUploader {
|
|
125
125
|
readonly baseUrl: string;
|
|
126
126
|
readonly requestTimeout: number;
|
|
@@ -179,16 +179,18 @@ export class HypiHubUploader {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
private async jsonOnce(path: string, auth: UploadAuth, init: RequestInit, timeoutMs: number, retryAuth = true): Promise<Record<string, unknown>> {
|
|
182
|
+
const token = await uploadToken(auth);
|
|
182
183
|
const request = requestDeadline(timeoutMs);
|
|
183
184
|
const deadline = Date.now() + timeoutMs;
|
|
184
185
|
try {
|
|
185
186
|
const response = await request.wait(this.fetcher(`${this.baseUrl}${path}`, {
|
|
186
187
|
...init,
|
|
187
188
|
signal: request.signal,
|
|
188
|
-
headers: { authorization: `Bearer ${
|
|
189
|
+
headers: { authorization: `Bearer ${token}`, ...(init.headers ?? {}) },
|
|
189
190
|
}));
|
|
190
191
|
const text = await request.wait(response.text());
|
|
191
192
|
if (response.status === 401 && retryAuth && uploadCanRefresh(auth)) {
|
|
193
|
+
request.finish();
|
|
192
194
|
await (auth as HypiHubAuth).refresh();
|
|
193
195
|
return await this.jsonOnce(path, auth, init, Math.max(1, deadline - Date.now()), false);
|
|
194
196
|
}
|
|
@@ -214,6 +216,16 @@ export class HypiHubUploader {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
private async signParts(uploadId: string, declarations: readonly PartDeclaration[], auth: UploadAuth): Promise<Map<number, Record<string, unknown>>> {
|
|
219
|
+
// HypiHub accepts at most 128 part declarations per signing request.
|
|
220
|
+
if (declarations.length > 128) {
|
|
221
|
+
const signed = new Map<number, Record<string, unknown>>();
|
|
222
|
+
for (let offset = 0; offset < declarations.length; offset += 128) {
|
|
223
|
+
for (const [number, part] of await this.signParts(uploadId, declarations.slice(offset, offset + 128), auth)) {
|
|
224
|
+
signed.set(number, part);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return signed;
|
|
228
|
+
}
|
|
217
229
|
const startedAt = Date.now();
|
|
218
230
|
this.log(`part signing started upload=${uploadId} parts=${declarations.length}`);
|
|
219
231
|
const response = await this.json(`/files/uploads/${encodeURIComponent(uploadId)}/parts`, auth, {
|
|
@@ -396,9 +408,30 @@ export class HypiHubUploader {
|
|
|
396
408
|
this.log(`upload session request failed elapsed=${this.elapsed(policyStartedAt)} reason=${this.safeReason(error)}`);
|
|
397
409
|
throw error;
|
|
398
410
|
}
|
|
399
|
-
assert(policy.upload_mode === "s3_multipart"
|
|
400
|
-
|
|
411
|
+
assert(policy.upload_mode === "s3_multipart" || policy.upload_mode === "api_multipart",
|
|
412
|
+
"HypiHub returned an unknown upload mode");
|
|
413
|
+
const result = policy.upload_mode === "api_multipart"
|
|
414
|
+
? await this.uploadApi(input, auth, policy)
|
|
415
|
+
: await this.uploadDirect(input.bytes, auth, policy);
|
|
401
416
|
this.log(`upload finished bytes=${input.bytes.byteLength} mime=${input.mediaType} elapsed=${this.elapsed(startedAt)}`);
|
|
402
417
|
return result;
|
|
403
418
|
}
|
|
419
|
+
|
|
420
|
+
private async uploadApi(input: HypiHubUploadInput, auth: UploadAuth, policy: Record<string, unknown>): Promise<string> {
|
|
421
|
+
const endpoint = new URL(requiredString(policy.endpoint, "HypiHub file upload endpoint"), this.baseUrl);
|
|
422
|
+
assert(endpoint.href === `${this.baseUrl}/files`, "HypiHub file upload endpoint must belong to the selected service");
|
|
423
|
+
if (policy.max_bytes !== undefined) {
|
|
424
|
+
assert(input.bytes.byteLength <= requiredInteger(policy.max_bytes, "HypiHub file upload byte limit"),
|
|
425
|
+
"HypiHub reference exceeds the negotiated file upload byte limit");
|
|
426
|
+
}
|
|
427
|
+
const form = new FormData();
|
|
428
|
+
form.set("file", new Blob([new Uint8Array(input.bytes)], { type: input.mediaType }),
|
|
429
|
+
input.filename ?? `reference.${extension(input.mediaType)}`);
|
|
430
|
+
form.set("purpose", input.purpose ?? "reference");
|
|
431
|
+
if (input.isPersonReference !== undefined) form.set("is_person_reference", String(input.isPersonReference));
|
|
432
|
+
const response = await this.json("/files", auth, { method: "POST", body: form });
|
|
433
|
+
const url = requiredString(response.url, "HypiHub file upload URL");
|
|
434
|
+
assertHTTPS(url, "HypiHub file upload URL");
|
|
435
|
+
return url;
|
|
436
|
+
}
|
|
404
437
|
}
|