@eyepop.ai/eyepop 3.25.1 → 3.25.3

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.
@@ -67,7 +67,7 @@ interface Prediction extends StreamTime {
67
67
  objects?: Array<PredictedObject>;
68
68
  classes?: Array<PredictedClass>;
69
69
  texts?: Array<PredictedText>;
70
- meshs?: Array<PredictedMesh>;
70
+ meshes?: Array<PredictedMesh>;
71
71
  keyPoints?: Array<PredictedKeyPoints>;
72
72
  embeddings?: Array<PredictedEmbedding>;
73
73
  details?: Array<Record<string, any>>;
@@ -171,7 +171,7 @@ interface PredictedObject extends PredictedClass {
171
171
  objects?: Array<PredictedObject>;
172
172
  classes?: Array<PredictedClass>;
173
173
  texts?: Array<PredictedText>;
174
- meshs?: Array<PredictedMesh>;
174
+ meshes?: Array<PredictedMesh>;
175
175
  keyPoints?: Array<PredictedKeyPoints>;
176
176
  details?: Array<Record<string, any>>;
177
177
  }
@@ -1979,12 +1979,6 @@ declare class SessionBackend implements WorkerBackend {
1979
1979
  session(): Promise<WorkerSession>;
1980
1980
  popName(): string | null;
1981
1981
  pop(): Pop | null;
1982
- /**
1983
- * A select forward's results come only at V3. Only a Pop that selects asks
1984
- * for it, so a worker too old to know V3 - which also rejects that Pop - is
1985
- * never sent a version it would not accept.
1986
- */
1987
- private predictionVersion;
1988
1982
  changePop(pop: Pop): Promise<void>;
1989
1983
  private _authenticationHeaders;
1990
1984
  submit(work: WorkerWork): Promise<ResultStream>;
@@ -2075,6 +2069,7 @@ declare class OpenSource {
2075
2069
  readonly id: string;
2076
2070
  readonly inbox: Inbox<InboxItem>;
2077
2071
  readonly uploads: Map<number | null, Credit>;
2072
+ endedBefore: number;
2078
2073
  constructor(id: string);
2079
2074
  close(error: Error): void;
2080
2075
  }
@@ -2083,6 +2078,7 @@ interface Media {
2083
2078
  readonly open: () => Promise<UploadBody$1>;
2084
2079
  readonly contentType: string;
2085
2080
  readonly size: number | undefined;
2081
+ readonly once?: boolean;
2086
2082
  }
2087
2083
  /** A job: one source on the endpoint's socket, its predictions read as an async iterable. */
2088
2084
  declare class PipelineJob implements ResultStream {
@@ -2094,6 +2090,8 @@ declare class PipelineJob implements ResultStream {
2094
2090
  private readonly _results;
2095
2091
  private _consumed;
2096
2092
  private _cancelled;
2093
+ private _markCancelled;
2094
+ readonly whenCancelled: Promise<void>;
2097
2095
  openSource: OpenSource | null;
2098
2096
  stopOnPipeline: (() => void) | null;
2099
2097
  constructor(source: Message, media: Media[], live: boolean, params: ProcessParams, mediaStream?: MediaStream | undefined);
@@ -2147,11 +2145,16 @@ declare class PipelineBackend implements WorkerBackend {
2147
2145
  private jobOf;
2148
2146
  private sourceJob;
2149
2147
  private pathMedia;
2150
- /** Run the job's source to its end; a live source refused overloaded goes once more on a new socket. */
2148
+ /**
2149
+ * Run the job's source to its end. A live source refused overloaded goes once more on a new socket.
2150
+ * A source refused queue_full goes again once another source on the socket ended, so it may go after
2151
+ * jobs submitted later; with none of the endpoint's other sources open there, or with an upload from
2152
+ * the caller's own stream, which cannot be read twice, the refusal stands.
2153
+ */
2151
2154
  private run;
2152
2155
  /** Send the job's setSource; the caller runs it in submission order. */
2153
2156
  private setSource;
2154
- /** Hand the source's predictions to the job until it ends; true when the pipeline API was overloaded. */
2157
+ /** Hand the source's predictions to the job until it ends; the refusal to retry on, overloaded or queue_full. */
2155
2158
  private follow;
2156
2159
  /** Send each upload member's bytes in chunks within its credit, then its uploaded. */
2157
2160
  private upload;
@@ -67,7 +67,7 @@ interface Prediction extends StreamTime {
67
67
  objects?: Array<PredictedObject>;
68
68
  classes?: Array<PredictedClass>;
69
69
  texts?: Array<PredictedText>;
70
- meshs?: Array<PredictedMesh>;
70
+ meshes?: Array<PredictedMesh>;
71
71
  keyPoints?: Array<PredictedKeyPoints>;
72
72
  embeddings?: Array<PredictedEmbedding>;
73
73
  details?: Array<Record<string, any>>;
@@ -171,7 +171,7 @@ interface PredictedObject extends PredictedClass {
171
171
  objects?: Array<PredictedObject>;
172
172
  classes?: Array<PredictedClass>;
173
173
  texts?: Array<PredictedText>;
174
- meshs?: Array<PredictedMesh>;
174
+ meshes?: Array<PredictedMesh>;
175
175
  keyPoints?: Array<PredictedKeyPoints>;
176
176
  details?: Array<Record<string, any>>;
177
177
  }
@@ -1979,12 +1979,6 @@ declare class SessionBackend implements WorkerBackend {
1979
1979
  session(): Promise<WorkerSession>;
1980
1980
  popName(): string | null;
1981
1981
  pop(): Pop | null;
1982
- /**
1983
- * A select forward's results come only at V3. Only a Pop that selects asks
1984
- * for it, so a worker too old to know V3 - which also rejects that Pop - is
1985
- * never sent a version it would not accept.
1986
- */
1987
- private predictionVersion;
1988
1982
  changePop(pop: Pop): Promise<void>;
1989
1983
  private _authenticationHeaders;
1990
1984
  submit(work: WorkerWork): Promise<ResultStream>;
@@ -2075,6 +2069,7 @@ declare class OpenSource {
2075
2069
  readonly id: string;
2076
2070
  readonly inbox: Inbox<InboxItem>;
2077
2071
  readonly uploads: Map<number | null, Credit>;
2072
+ endedBefore: number;
2078
2073
  constructor(id: string);
2079
2074
  close(error: Error): void;
2080
2075
  }
@@ -2083,6 +2078,7 @@ interface Media {
2083
2078
  readonly open: () => Promise<UploadBody$1>;
2084
2079
  readonly contentType: string;
2085
2080
  readonly size: number | undefined;
2081
+ readonly once?: boolean;
2086
2082
  }
2087
2083
  /** A job: one source on the endpoint's socket, its predictions read as an async iterable. */
2088
2084
  declare class PipelineJob implements ResultStream {
@@ -2094,6 +2090,8 @@ declare class PipelineJob implements ResultStream {
2094
2090
  private readonly _results;
2095
2091
  private _consumed;
2096
2092
  private _cancelled;
2093
+ private _markCancelled;
2094
+ readonly whenCancelled: Promise<void>;
2097
2095
  openSource: OpenSource | null;
2098
2096
  stopOnPipeline: (() => void) | null;
2099
2097
  constructor(source: Message, media: Media[], live: boolean, params: ProcessParams, mediaStream?: MediaStream | undefined);
@@ -2147,11 +2145,16 @@ declare class PipelineBackend implements WorkerBackend {
2147
2145
  private jobOf;
2148
2146
  private sourceJob;
2149
2147
  private pathMedia;
2150
- /** Run the job's source to its end; a live source refused overloaded goes once more on a new socket. */
2148
+ /**
2149
+ * Run the job's source to its end. A live source refused overloaded goes once more on a new socket.
2150
+ * A source refused queue_full goes again once another source on the socket ended, so it may go after
2151
+ * jobs submitted later; with none of the endpoint's other sources open there, or with an upload from
2152
+ * the caller's own stream, which cannot be read twice, the refusal stands.
2153
+ */
2151
2154
  private run;
2152
2155
  /** Send the job's setSource; the caller runs it in submission order. */
2153
2156
  private setSource;
2154
- /** Hand the source's predictions to the job until it ends; true when the pipeline API was overloaded. */
2157
+ /** Hand the source's predictions to the job until it ends; the refusal to retry on, overloaded or queue_full. */
2155
2158
  private follow;
2156
2159
  /** Send each upload member's bytes in chunks within its credit, then its uploaded. */
2157
2160
  private upload;
@@ -9329,7 +9329,7 @@ var PredictionVersion = /* @__PURE__ */ ((PredictionVersion3) => {
9329
9329
  PredictionVersion3[PredictionVersion3["V3"] = 3] = "V3";
9330
9330
  return PredictionVersion3;
9331
9331
  })(PredictionVersion || {});
9332
- var DEFAULT_PREDICTION_VERSION = 2 /* V2 */;
9332
+ var DEFAULT_PREDICTION_VERSION = 3 /* V3 */;
9333
9333
  var VideoMode = /* @__PURE__ */ ((VideoMode2) => {
9334
9334
  VideoMode2["STREAM"] = "stream";
9335
9335
  VideoMode2["BUFFER"] = "buffer";
@@ -11040,14 +11040,6 @@ var _SessionBackend = class _SessionBackend {
11040
11040
  }
11041
11041
  return this.ctx.options.pop || null;
11042
11042
  }
11043
- /**
11044
- * A select forward's results come only at V3. Only a Pop that selects asks
11045
- * for it, so a worker too old to know V3 - which also rejects that Pop - is
11046
- * never sent a version it would not accept.
11047
- */
11048
- predictionVersion() {
11049
- return popSelects(this.pop()) ? 3 /* V3 */ : DEFAULT_PREDICTION_VERSION;
11050
- }
11051
11043
  async changePop(pop) {
11052
11044
  if (this.ctx.options.popId == "transient" /* Transient */) {
11053
11045
  await this.changeTransientPop(pop);
@@ -11148,7 +11140,7 @@ var _SessionBackend = class _SessionBackend {
11148
11140
  return { stream: streamSource.stream, mimeType: streamSource.mimeType };
11149
11141
  })
11150
11142
  );
11151
- return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11143
+ return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11152
11144
  } catch (e) {
11153
11145
  limit.release();
11154
11146
  throw e;
@@ -11167,7 +11159,7 @@ var _SessionBackend = class _SessionBackend {
11167
11159
  stream,
11168
11160
  mimeType: mimeTypes?.[i] ?? null
11169
11161
  }));
11170
- return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11162
+ return this.start(new UploadGroupJob(sources, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11171
11163
  } catch (e) {
11172
11164
  limit.release();
11173
11165
  throw e;
@@ -11182,7 +11174,7 @@ var _SessionBackend = class _SessionBackend {
11182
11174
  await limit.acquire();
11183
11175
  try {
11184
11176
  this.ctx.updateState();
11185
- return this.start(new LoadFromGroupJob(urls, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11177
+ return this.start(new LoadFromGroupJob(urls, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11186
11178
  } catch (e) {
11187
11179
  limit.release();
11188
11180
  throw e;
@@ -11226,14 +11218,14 @@ var _SessionBackend = class _SessionBackend {
11226
11218
  client,
11227
11219
  this.ctx.requestLogger,
11228
11220
  void 0,
11229
- this.predictionVersion()
11221
+ DEFAULT_PREDICTION_VERSION
11230
11222
  ),
11231
11223
  true
11232
11224
  );
11233
11225
  }
11234
11226
  async uploadStream(source, params) {
11235
11227
  return await this.startConnected(
11236
- async (client) => new UploadJob(source.stream, source.mimeType, source.size, source.videoMode, params, async () => this.session(), client, this.ctx.requestLogger, void 0, this.predictionVersion()),
11228
+ async (client) => new UploadJob(source.stream, source.mimeType, source.size, source.videoMode, params, async () => this.session(), client, this.ctx.requestLogger, void 0, DEFAULT_PREDICTION_VERSION),
11237
11229
  true
11238
11230
  );
11239
11231
  }
@@ -11258,7 +11250,7 @@ var _SessionBackend = class _SessionBackend {
11258
11250
  client,
11259
11251
  this.ctx.requestLogger,
11260
11252
  uploadBodyFactory,
11261
- this.predictionVersion()
11253
+ DEFAULT_PREDICTION_VERSION
11262
11254
  );
11263
11255
  });
11264
11256
  }
@@ -11266,13 +11258,13 @@ var _SessionBackend = class _SessionBackend {
11266
11258
  if (source.url.startsWith("file://")) {
11267
11259
  return await this.uploadPath({ path: source.url.substring("file://".length) }, params);
11268
11260
  }
11269
- return await this.startConnected(async (client) => new LoadFromJob(source.url, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11261
+ return await this.startConnected(async (client) => new LoadFromJob(source.url, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11270
11262
  }
11271
11263
  async loadFromAssetUuid(source, params) {
11272
- return await this.startConnected(async (client) => new LoadFromAssetUuidJob(source.assetUuid, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11264
+ return await this.startConnected(async (client) => new LoadFromAssetUuidJob(source.assetUuid, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11273
11265
  }
11274
11266
  async loadMediaStream(source, params) {
11275
- return await this.startConnected(async (client) => new LoadMediaStreamJob(source.mediaStream, params, async () => this.session(), client, this.ctx.requestLogger, this.predictionVersion()));
11267
+ return await this.startConnected(async (client) => new LoadMediaStreamJob(source.mediaStream, params, async () => this.session(), client, this.ctx.requestLogger, DEFAULT_PREDICTION_VERSION));
11276
11268
  }
11277
11269
  async reconnect() {
11278
11270
  if (this.ctx.options.popId == "transient" /* Transient */) {
@@ -11722,6 +11714,8 @@ var OpenSource = class {
11722
11714
  this.id = id;
11723
11715
  this.inbox = new Inbox();
11724
11716
  this.uploads = /* @__PURE__ */ new Map();
11717
+ // The socket's ended count when the setSource went out.
11718
+ this.endedBefore = 0;
11725
11719
  }
11726
11720
  close(error) {
11727
11721
  this.uploads.forEach((upload) => upload.end());
@@ -11734,14 +11728,53 @@ var PipelineSocket = class {
11734
11728
  this.sources = /* @__PURE__ */ new Map();
11735
11729
  this.commands = /* @__PURE__ */ new Map();
11736
11730
  this._closed = false;
11731
+ // The ids of sources sent on this socket that have neither ended nor been refused, as the
11732
+ // pipeline API counts them, cancelled ones included.
11733
+ this.held = /* @__PURE__ */ new Set();
11734
+ // The held sources the pipeline API took: only these end, so only these make room.
11735
+ this.accepted = /* @__PURE__ */ new Set();
11736
+ // The ends on this socket so far.
11737
+ this.ended = 0;
11738
+ // How many sources the socket had taken when it last refused queue_full: while as many are held,
11739
+ // the next send waits. Null once none is held.
11740
+ this.cap = null;
11741
+ this._roomWaiters = [];
11737
11742
  this.closed = new Promise((resolve) => this._markClosed = resolve);
11738
11743
  }
11744
+ /** A source ended or the socket closed: wake the send waiting for room. */
11745
+ freed() {
11746
+ this.ended += 1;
11747
+ this.wake();
11748
+ }
11749
+ /** A held source ended or was refused: wake the send waiting for room. */
11750
+ wake() {
11751
+ if (this.held.size === 0) {
11752
+ this.cap = null;
11753
+ }
11754
+ const waiters = this._roomWaiters;
11755
+ this._roomWaiters = [];
11756
+ waiters.forEach((wake) => wake());
11757
+ }
11758
+ get hasRoom() {
11759
+ return this.cap === null || this.held.size < this.cap;
11760
+ }
11761
+ /**
11762
+ * Wait, while the socket is full, until a source ends, it closes or the job is cancelled. The
11763
+ * caller sends in order, so one send waits here and each end lets one go, instead of every
11764
+ * refused source sending its media again at once.
11765
+ */
11766
+ async room(job) {
11767
+ while (!this.hasRoom && this.open && !job.cancelled) {
11768
+ await Promise.race([new Promise((resolve) => this._roomWaiters.push(resolve)), job.whenCancelled]);
11769
+ }
11770
+ }
11739
11771
  get open() {
11740
11772
  return !this._closed && this.ws.readyState === 1;
11741
11773
  }
11742
11774
  markClosed() {
11743
11775
  this._closed = true;
11744
11776
  this._markClosed();
11777
+ this.freed();
11745
11778
  }
11746
11779
  sendJson(message) {
11747
11780
  this.ws.send(JSON.stringify(message));
@@ -11760,6 +11793,8 @@ var PipelineJob = class {
11760
11793
  this._results = new Inbox();
11761
11794
  this._consumed = false;
11762
11795
  this._cancelled = false;
11796
+ // Settles when the job is cancelled, so that a send waiting for room ends at once.
11797
+ this.whenCancelled = new Promise((resolve) => this._markCancelled = resolve);
11763
11798
  this.openSource = null;
11764
11799
  // Stops the job's source on the pipeline, once it was sent.
11765
11800
  this.stopOnPipeline = null;
@@ -11799,6 +11834,7 @@ var PipelineJob = class {
11799
11834
  return;
11800
11835
  }
11801
11836
  this._cancelled = true;
11837
+ this._markCancelled();
11802
11838
  this._results.push(null);
11803
11839
  this.openSource?.inbox.push(CANCELLED);
11804
11840
  this.stopOnPipeline?.();
@@ -12013,6 +12049,16 @@ var PipelineBackend = class {
12013
12049
  const type = message["type"];
12014
12050
  const id = message["id"];
12015
12051
  const source = typeof id === "string" ? socket.sources.get(id) : void 0;
12052
+ if (type === "end") {
12053
+ socket.held.delete(id);
12054
+ socket.accepted.delete(id);
12055
+ socket.freed();
12056
+ } else if (type === "result" && socket.held.has(id)) {
12057
+ socket.accepted.add(id);
12058
+ } else if (type === "error" && message["error"] !== "unknown_command") {
12059
+ socket.held.delete(id);
12060
+ socket.wake();
12061
+ }
12016
12062
  if (type === "send" || type === "credit") {
12017
12063
  const upload = source?.uploads.get(message["member"] ?? null);
12018
12064
  upload?.grant(Number(type === "send" ? message["credit"] : message["bytes"]));
@@ -12171,7 +12217,12 @@ var PipelineBackend = class {
12171
12217
  }
12172
12218
  };
12173
12219
  }
12174
- /** Run the job's source to its end; a live source refused overloaded goes once more on a new socket. */
12220
+ /**
12221
+ * Run the job's source to its end. A live source refused overloaded goes once more on a new socket.
12222
+ * A source refused queue_full goes again once another source on the socket ended, so it may go after
12223
+ * jobs submitted later; with none of the endpoint's other sources open there, or with an upload from
12224
+ * the caller's own stream, which cannot be read twice, the refusal stands.
12225
+ */
12175
12226
  async run(job) {
12176
12227
  let retried = false;
12177
12228
  while (true) {
@@ -12182,9 +12233,9 @@ var PipelineBackend = class {
12182
12233
  const { socket, source } = sent;
12183
12234
  const uploading = source.uploads.size > 0 ? this.upload(socket, source, job) : Promise.resolve();
12184
12235
  let whip = null;
12185
- let overloaded;
12236
+ let refused;
12186
12237
  try {
12187
- overloaded = await this.follow(job, source, () => {
12238
+ refused = await this.follow(job, source, () => {
12188
12239
  if (job.mediaStream !== void 0) {
12189
12240
  const fail = (error) => {
12190
12241
  job.stopOnPipeline?.();
@@ -12202,9 +12253,19 @@ var PipelineBackend = class {
12202
12253
  await uploading;
12203
12254
  await whip?.close();
12204
12255
  }
12205
- if (!overloaded) {
12256
+ if (refused === null) {
12206
12257
  return;
12207
12258
  }
12259
+ if (refused === "queue_full") {
12260
+ if (socket.accepted.size === 0 && socket.ended === source.endedBefore) {
12261
+ throw new PipelineCommandError("queue_full", "the socket refused the source with none of its own open");
12262
+ }
12263
+ if (job.media.some((media) => media.once)) {
12264
+ throw new PipelineCommandError("queue_full", "the socket refused an upload from a stream, which cannot be sent again");
12265
+ }
12266
+ socket.cap = Math.min(socket.accepted.size, socket.cap ?? socket.accepted.size) || null;
12267
+ continue;
12268
+ }
12208
12269
  if (retried || !job.live) {
12209
12270
  throw new PipelineCommandError("overloaded", "the pipeline API has no capacity for another live source");
12210
12271
  }
@@ -12214,16 +12275,23 @@ var PipelineBackend = class {
12214
12275
  }
12215
12276
  /** Send the job's setSource; the caller runs it in submission order. */
12216
12277
  async setSource(job) {
12278
+ if (job.cancelled) {
12279
+ return null;
12280
+ }
12281
+ let socket = await this.ensureSocket();
12282
+ await socket.room(job);
12217
12283
  if (job.cancelled) {
12218
12284
  return null;
12219
12285
  }
12220
12286
  const combined = await combinedMedia(job);
12221
- const socket = await this.ensureSocket();
12287
+ socket = await this.ensureSocket();
12222
12288
  const source = new OpenSource(this.newId("source"));
12223
12289
  if (combined === null) {
12224
12290
  uploadKeys(job).forEach((key) => source.uploads.set(key, new Credit()));
12225
12291
  }
12226
12292
  socket.sources.set(source.id, source);
12293
+ socket.held.add(source.id);
12294
+ source.endedBefore = socket.ended;
12227
12295
  job.openSource = source;
12228
12296
  job.stopOnPipeline = () => {
12229
12297
  if (socket.open) {
@@ -12238,15 +12306,15 @@ var PipelineBackend = class {
12238
12306
  }
12239
12307
  return { socket, source };
12240
12308
  }
12241
- /** Hand the source's predictions to the job until it ends; true when the pipeline API was overloaded. */
12309
+ /** Hand the source's predictions to the job until it ends; the refusal to retry on, overloaded or queue_full. */
12242
12310
  async follow(job, source, onResult) {
12243
12311
  while (true) {
12244
12312
  if (job.cancelled) {
12245
- return false;
12313
+ return null;
12246
12314
  }
12247
12315
  const message = await source.inbox.next();
12248
12316
  if (message === CANCELLED) {
12249
- return false;
12317
+ return null;
12250
12318
  }
12251
12319
  if (message instanceof Error) {
12252
12320
  throw message;
@@ -12260,12 +12328,12 @@ var PipelineBackend = class {
12260
12328
  break;
12261
12329
  case "end":
12262
12330
  if (message["reason"] === "eos") {
12263
- return false;
12331
+ return null;
12264
12332
  }
12265
12333
  throw new PipelineSourceError(source.id, message["reason"] ?? "", message["message"] ?? "");
12266
12334
  case "error":
12267
- if (message["error"] === "overloaded") {
12268
- return true;
12335
+ if (message["error"] === "overloaded" || message["error"] === "queue_full") {
12336
+ return message["error"];
12269
12337
  }
12270
12338
  throw new PipelineCommandError(message["error"] ?? "", message["message"] ?? "");
12271
12339
  case "dropped":
@@ -12409,7 +12477,8 @@ function knownSize(body) {
12409
12477
  return void 0;
12410
12478
  }
12411
12479
  function bodyMedia(body, contentType, size) {
12412
- return { open: async () => body, contentType, size: size ?? knownSize(body) };
12480
+ const once = !(typeof Blob !== "undefined" && body instanceof Blob) && !(body instanceof ArrayBuffer) && !ArrayBuffer.isView(body);
12481
+ return { open: async () => body, contentType, size: size ?? knownSize(body), once };
12413
12482
  }
12414
12483
  function uploadGroup(media) {
12415
12484
  return { group: media.map((m) => ({ upload: { contentType: m.contentType } })) };