@sogni-ai/sogni-client 5.63.2 → 5.63.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +5 -2
- package/dist/Projects/index.d.ts +2 -0
- package/dist/Projects/index.js +26 -60
- package/dist/Projects/index.js.map +1 -1
- package/dist/Projects/utils/fileContentType.d.ts +18 -0
- package/dist/Projects/utils/fileContentType.js +63 -0
- package/dist/Projects/utils/fileContentType.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-esm/Projects/index.js +16 -50
- package/dist-esm/Projects/index.js.map +1 -1
- package/dist-esm/Projects/utils/fileContentType.js +60 -0
- package/dist-esm/Projects/utils/fileContentType.js.map +1 -0
- package/dist-esm/version.js +1 -1
- package/package.json +2 -1
- package/src/Projects/index.ts +18 -44
- package/src/Projects/utils/fileContentType.ts +55 -0
- package/src/version.ts +1 -1
package/src/Projects/index.ts
CHANGED
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
type ResultMediaKind,
|
|
89
89
|
usesReferenceMask
|
|
90
90
|
} from './utils/index.js';
|
|
91
|
+
import { getFileContentType } from './utils/fileContentType.js';
|
|
91
92
|
import { TokenType } from '../types/token.js';
|
|
92
93
|
import type { JobProvenance } from './types/JobProvenance.js';
|
|
93
94
|
import { getMaxContextImages, validateSampler, isGptImageModel } from '../lib/validation.js';
|
|
@@ -249,41 +250,6 @@ function deriveLoraCapableModelIds(loras: LoraCatalogEntry[]): string[] {
|
|
|
249
250
|
return Array.from(modelIds).sort();
|
|
250
251
|
}
|
|
251
252
|
|
|
252
|
-
/**
|
|
253
|
-
* Detect content type from a file object.
|
|
254
|
-
* For File objects in browser, uses the type property.
|
|
255
|
-
* Returns undefined if content type cannot be detected.
|
|
256
|
-
*/
|
|
257
|
-
function getFileContentType(file: File | Buffer | Blob): string | undefined {
|
|
258
|
-
if (file instanceof Blob && 'type' in file && file.type) {
|
|
259
|
-
return file.type;
|
|
260
|
-
}
|
|
261
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
|
|
262
|
-
if (file.length >= 12) {
|
|
263
|
-
if (file[0] === 0xff && file[1] === 0xd8 && file[2] === 0xff) return 'image/jpeg';
|
|
264
|
-
if (file[0] === 0x89 && file[1] === 0x50 && file[2] === 0x4e && file[3] === 0x47) {
|
|
265
|
-
return 'image/png';
|
|
266
|
-
}
|
|
267
|
-
if (file.toString('ascii', 0, 4) === 'RIFF' && file.toString('ascii', 8, 12) === 'WEBP') {
|
|
268
|
-
return 'image/webp';
|
|
269
|
-
}
|
|
270
|
-
if (file.toString('ascii', 0, 3) === 'GIF') return 'image/gif';
|
|
271
|
-
if (file.toString('ascii', 4, 8) === 'ftyp') {
|
|
272
|
-
const brand = file.toString('ascii', 8, 12).toLowerCase();
|
|
273
|
-
if (brand.includes('m4a') || brand.includes('m4b')) return 'audio/mp4';
|
|
274
|
-
if (brand.includes('qt')) return 'video/quicktime';
|
|
275
|
-
return 'video/mp4';
|
|
276
|
-
}
|
|
277
|
-
if (file.toString('ascii', 0, 4) === 'RIFF' && file.toString('ascii', 8, 12) === 'WAVE') {
|
|
278
|
-
return 'audio/wav';
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (file.length >= 3 && file.toString('ascii', 0, 3) === 'ID3') return 'audio/mpeg';
|
|
282
|
-
if (file.length >= 2 && file[0] === 0xff && (file[1] & 0xe0) === 0xe0) return 'audio/mpeg';
|
|
283
|
-
}
|
|
284
|
-
return undefined;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
253
|
/**
|
|
288
254
|
* Convert file to a format compatible with fetch body.
|
|
289
255
|
* Converts Node.js Buffer to Blob for cross-platform compatibility.
|
|
@@ -577,6 +543,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
577
543
|
/** Projects waiting for a reconnect to be resubmitted on. */
|
|
578
544
|
private _awaitingResubmit = new Set<string>();
|
|
579
545
|
private _resubmitWaiters = new Set<() => void>();
|
|
546
|
+
/** Initial sends finish after model lookups and uploads, independently of creation time. */
|
|
547
|
+
private _submittedAt = new Map<string, number>();
|
|
580
548
|
/** When each resubmitted project was last sent, for the recently-created grace. */
|
|
581
549
|
private _resubmittedAt = new Map<string, number>();
|
|
582
550
|
/** Bumped on every transport loss; tells a frame sent before a drop from one sent after. */
|
|
@@ -804,6 +772,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
804
772
|
this._unadmittedRequests.clear();
|
|
805
773
|
for (const stopWaiting of this._resubmitWaiters) stopWaiting();
|
|
806
774
|
this._awaitingResubmit.clear();
|
|
775
|
+
this._submittedAt.clear();
|
|
807
776
|
this._resubmittedAt.clear();
|
|
808
777
|
this._sentOnGeneration.clear();
|
|
809
778
|
this._recoveredCompletedIds.clear();
|
|
@@ -1299,6 +1268,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
1299
1268
|
}
|
|
1300
1269
|
if (project.finished) {
|
|
1301
1270
|
this._unadmittedRequests.delete(project.id);
|
|
1271
|
+
this._submittedAt.delete(project.id);
|
|
1302
1272
|
this._resubmittedAt.delete(project.id);
|
|
1303
1273
|
this._sentOnGeneration.delete(project.id);
|
|
1304
1274
|
// Sync project data with the server and remove it from the list after some time
|
|
@@ -1614,7 +1584,10 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
1614
1584
|
}
|
|
1615
1585
|
|
|
1616
1586
|
private _lastSubmittedAt(project: Project): number {
|
|
1617
|
-
return Math.max(
|
|
1587
|
+
return Math.max(
|
|
1588
|
+
this._submittedAt.get(project.id) ?? project.startedAt.getTime(),
|
|
1589
|
+
this._resubmittedAt.get(project.id) ?? 0
|
|
1590
|
+
);
|
|
1618
1591
|
}
|
|
1619
1592
|
|
|
1620
1593
|
private handleServerConnected() {
|
|
@@ -2409,6 +2382,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
2409
2382
|
private _trackSubmitted(project: Project): Project {
|
|
2410
2383
|
const tracked = this.projects.find((candidate) => candidate.id === project.id);
|
|
2411
2384
|
if (!tracked) {
|
|
2385
|
+
this._submittedAt.set(project.id, Date.now());
|
|
2412
2386
|
this.projects.push(project);
|
|
2413
2387
|
return project;
|
|
2414
2388
|
}
|
|
@@ -2473,7 +2447,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
2473
2447
|
const keyFrame = request.keyFrames?.[0];
|
|
2474
2448
|
if (!keyFrame) return;
|
|
2475
2449
|
if (data.referenceAudio && data.referenceAudio !== true) {
|
|
2476
|
-
keyFrame.referenceAudioContentType = getFileContentType(data.referenceAudio);
|
|
2450
|
+
keyFrame.referenceAudioContentType = getFileContentType(data.referenceAudio, 'audio');
|
|
2477
2451
|
}
|
|
2478
2452
|
}
|
|
2479
2453
|
|
|
@@ -2548,25 +2522,25 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
2548
2522
|
}
|
|
2549
2523
|
const isMinimaxH3R2v = isMinimaxH3ReferenceModel(data.modelId);
|
|
2550
2524
|
if (!isMinimaxH3R2v && data.referenceAudio && data.referenceAudio !== true) {
|
|
2551
|
-
keyFrame.referenceAudioContentType = getFileContentType(data.referenceAudio);
|
|
2525
|
+
keyFrame.referenceAudioContentType = getFileContentType(data.referenceAudio, 'audio');
|
|
2552
2526
|
}
|
|
2553
2527
|
if (data.referenceAudioIdentity && data.referenceAudioIdentity !== true) {
|
|
2554
|
-
const contentType = getFileContentType(data.referenceAudioIdentity);
|
|
2528
|
+
const contentType = getFileContentType(data.referenceAudioIdentity, 'audio');
|
|
2555
2529
|
keyFrame.referenceAudioIdentityContentType = contentType;
|
|
2556
2530
|
keyFrame.referenceAudioContentType ??= contentType;
|
|
2557
2531
|
}
|
|
2558
2532
|
if (!isMinimaxH3R2v && data.referenceVideo && data.referenceVideo !== true) {
|
|
2559
|
-
keyFrame.referenceVideoContentType = getFileContentType(data.referenceVideo);
|
|
2533
|
+
keyFrame.referenceVideoContentType = getFileContentType(data.referenceVideo, 'video');
|
|
2560
2534
|
}
|
|
2561
2535
|
if (isMinimaxH3R2v) {
|
|
2562
2536
|
for (const { slot, media } of getMinimaxH3ReferenceAudioSlots(data)) {
|
|
2563
2537
|
if (typeof media !== 'boolean') {
|
|
2564
|
-
keyFrame[`referenceAudio${slot}ContentType`] = getFileContentType(media);
|
|
2538
|
+
keyFrame[`referenceAudio${slot}ContentType`] = getFileContentType(media, 'audio');
|
|
2565
2539
|
}
|
|
2566
2540
|
}
|
|
2567
2541
|
for (const { slot, media } of getMinimaxH3ReferenceVideoSlots(data)) {
|
|
2568
2542
|
if (typeof media !== 'boolean') {
|
|
2569
|
-
keyFrame[`referenceVideo${slot}ContentType`] = getFileContentType(media);
|
|
2543
|
+
keyFrame[`referenceVideo${slot}ContentType`] = getFileContentType(media, 'video');
|
|
2570
2544
|
}
|
|
2571
2545
|
}
|
|
2572
2546
|
}
|
|
@@ -3233,7 +3207,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
3233
3207
|
const assertSession =
|
|
3234
3208
|
this._preparingSessions.get(projectId) ?? captureRequestSession(this.client.auth);
|
|
3235
3209
|
assertSession();
|
|
3236
|
-
const contentType = getFileContentType(file);
|
|
3210
|
+
const contentType = getFileContentType(file, 'audio');
|
|
3237
3211
|
if (await this.assets.tryBindFile(file, contentType, { projectId, type: 'referenceAudio', id }))
|
|
3238
3212
|
return;
|
|
3239
3213
|
assertSession();
|
|
@@ -3272,7 +3246,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
3272
3246
|
const assertSession =
|
|
3273
3247
|
this._preparingSessions.get(projectId) ?? captureRequestSession(this.client.auth);
|
|
3274
3248
|
assertSession();
|
|
3275
|
-
const contentType = getFileContentType(file);
|
|
3249
|
+
const contentType = getFileContentType(file, 'video');
|
|
3276
3250
|
if (await this.assets.tryBindFile(file, contentType, { projectId, type: 'referenceVideo', id }))
|
|
3277
3251
|
return;
|
|
3278
3252
|
assertSession();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect the content type of a reference file from its bytes.
|
|
3
|
+
*
|
|
4
|
+
* A browser `File`/`Blob` carries its own `type`. A Node `Buffer` is sniffed
|
|
5
|
+
* by magic bytes. `slot` says which reference the buffer fills: a
|
|
6
|
+
* Matroska/WebM container (EBML header) and an Ogg container carry either
|
|
7
|
+
* audio or video, and the magic bytes do not say which, so the slot decides
|
|
8
|
+
* between `audio/webm` and `video/webm` (`audio/ogg` and `video/ogg`).
|
|
9
|
+
*
|
|
10
|
+
* The declared type matters past the upload: the socket keeps audio guides the
|
|
11
|
+
* Mac app's AVFoundation decoder cannot read (OGG, WebM) off Mac app workers
|
|
12
|
+
* only when the type is declared; an undeclared guide is routed blind and the
|
|
13
|
+
* render fails there (sogni-socket constants/appMlxWorker.js
|
|
14
|
+
* APP_MLX_AUDIO_GUIDE_CONTENT_TYPES, 2026-10).
|
|
15
|
+
*
|
|
16
|
+
* Returns undefined when the bytes match nothing known.
|
|
17
|
+
*/
|
|
18
|
+
export function getFileContentType(
|
|
19
|
+
file: File | Buffer | Blob,
|
|
20
|
+
slot?: 'audio' | 'video'
|
|
21
|
+
): string | undefined {
|
|
22
|
+
if (file instanceof Blob && 'type' in file && file.type) {
|
|
23
|
+
return file.type;
|
|
24
|
+
}
|
|
25
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
|
|
26
|
+
if (file.length >= 12) {
|
|
27
|
+
if (file[0] === 0xff && file[1] === 0xd8 && file[2] === 0xff) return 'image/jpeg';
|
|
28
|
+
if (file[0] === 0x89 && file[1] === 0x50 && file[2] === 0x4e && file[3] === 0x47) {
|
|
29
|
+
return 'image/png';
|
|
30
|
+
}
|
|
31
|
+
if (file.toString('ascii', 0, 4) === 'RIFF' && file.toString('ascii', 8, 12) === 'WEBP') {
|
|
32
|
+
return 'image/webp';
|
|
33
|
+
}
|
|
34
|
+
if (file.toString('ascii', 0, 3) === 'GIF') return 'image/gif';
|
|
35
|
+
if (file.toString('ascii', 4, 8) === 'ftyp') {
|
|
36
|
+
const brand = file.toString('ascii', 8, 12).toLowerCase();
|
|
37
|
+
if (brand.includes('m4a') || brand.includes('m4b')) return 'audio/mp4';
|
|
38
|
+
if (brand.includes('qt')) return 'video/quicktime';
|
|
39
|
+
return 'video/mp4';
|
|
40
|
+
}
|
|
41
|
+
if (file.toString('ascii', 0, 4) === 'RIFF' && file.toString('ascii', 8, 12) === 'WAVE') {
|
|
42
|
+
return 'audio/wav';
|
|
43
|
+
}
|
|
44
|
+
if (file.toString('ascii', 0, 4) === 'fLaC') return 'audio/flac';
|
|
45
|
+
if (file.toString('ascii', 0, 4) === 'OggS')
|
|
46
|
+
return slot === 'video' ? 'video/ogg' : 'audio/ogg';
|
|
47
|
+
if (file[0] === 0x1a && file[1] === 0x45 && file[2] === 0xdf && file[3] === 0xa3) {
|
|
48
|
+
return slot === 'audio' ? 'audio/webm' : 'video/webm';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (file.length >= 3 && file.toString('ascii', 0, 3) === 'ID3') return 'audio/mpeg';
|
|
52
|
+
if (file.length >= 2 && file[0] === 0xff && (file[1] & 0xe0) === 0xe0) return 'audio/mpeg';
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
package/src/version.ts
CHANGED