@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.
@@ -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(project.startedAt.getTime(), this._resubmittedAt.get(project.id) ?? 0);
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
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/generate-version.mjs — do not edit.
2
2
  // Source of truth: package.json version.
3
- export const LIB_VERSION = '5.63.2';
3
+ export const LIB_VERSION = '5.63.4';