ugcinc 4.14.0 → 4.15.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 CHANGED
@@ -166,6 +166,28 @@ await client.posts.createVideo({
166
166
  await client.posts.updatePost({ postId: "post_123", post_tag: "campaign-august" });
167
167
  ```
168
168
 
169
+ When a post fails, its category is appended to the tag as `fail reason: <fail_category>` (for
170
+ example `campaign-july fail reason: device_error`), so a post list shows why without a status call
171
+ per post. The suffix is removed again when the post is retried.
172
+
173
+ ## Backup Audio
174
+
175
+ A post can carry a second sound. If the platform refuses the primary sound at posting time, the
176
+ backup becomes the post's `social_audio_id`, the post is re-attempted once, ` switched to backup
177
+ audio` is appended to its `tag`, and `backup_audio_used_at` is set. With no backup left the post
178
+ fails as `audio_unavailable` and is not retried. Set it with `backupSocialAudioId` on
179
+ `posts.createVideo()`, `posts.createSlideshow()`, `posts.createDraft()`, and `posts.updatePost()`
180
+ (`null` clears it on update).
181
+
182
+ ```typescript
183
+ await client.posts.createSlideshow({
184
+ accountId: "acc_123",
185
+ imageUrls: ["https://example.com/img1.jpg"],
186
+ socialAudioId: "audio_primary",
187
+ backupSocialAudioId: "audio_fallback",
188
+ });
189
+ ```
190
+
169
191
  ## Useful Exports
170
192
 
171
193
  - `UGCClient` for API access
package/dist/index.d.ts CHANGED
@@ -27,10 +27,10 @@ export type { InputType } from './automations/nodes/types';
27
27
  export type { ClientConfig } from './base';
28
28
  export type { Account, AccountStatus, AccountStat, AccountTask, QuarantineAccountParams, ReleaseAccountParams, QuarantineAccountResponse, EditProfileInfo, GetAccountsParams, GetAccountStatsParams, GetAccountStatusParams, AccountInfoUpdate, UpdateAccountInfoParams, AccountInfoUpdateResult, UpdateAccountInfoResponse, AccountSocialUpdate, UpdateAccountSocialParams, AccountSocialUpdateResult, UpdateAccountSocialResponse, DeleteAccountPostsParams, DeleteAccountPostsResponse, ResetWarmupParams, ResetWarmupResponse, NicheSwitchUpdate, NicheSwitchParams, NicheSwitchResult, NicheSwitchResponse, CreateAccountInput, CreateAccountsParams, CreateAccountResult, CreateAccountsResponse, TroubleshootFailReason, TroubleshootAccount, TroubleshootParams, } from './accounts';
29
29
  export type { TaskType, Task, GetTasksParams } from './tasks';
30
- export type { PostType, PostStatus, Post, PostStat, CaptionOverlay, GetPostsParams, CreateDraftParams, CreateSlideshowParams, GetPostStatsParams, GetPostStatusParams, CreateVideoParams, UpdatePostParams, DeletePostsParams, DeletePostsResponse, RetryPostsParams, SetPostStatusParams, SetPostStatusResponse, PreviewScheduleEntry, PreviewScheduleParams, PreviewScheduleResult, } from './posts';
30
+ export type { PostType, PostStatus, Post, PostStat, CaptionOverlay, GetPostsParams, GetPostsPageParams, PostsPage, PostSortField, PostSortDirection, CreateDraftParams, CreateSlideshowParams, GetPostStatsParams, GetPostStatusParams, CreateVideoParams, UpdatePostParams, DeletePostsParams, DeletePostsResponse, RetryPostsParams, SetPostStatusParams, SetPostStatusResponse, PreviewScheduleEntry, PreviewScheduleParams, PreviewScheduleResult, } from './posts';
31
31
  export type { RefreshStatsParams, RefreshStatsError, RefreshStatsResponse, RefreshStatsProgressResponse, RefreshStartEvent, RefreshProgressEvent, RefreshDoneEvent, RefreshStreamEvent, DailyAggregatedStat, GetDailyAggregatedStatsParams, DailyAccountStat, GetDailyAccountStatsParams, DailyPostStat, GetDailyPostStatsParams, DashboardDailyStat, GetDashboardDailyStatsParams, TopAccount, GetTopAccountsParams, TopPost, GetTopPostsParams, } from './stats';
32
32
  export type { Org, ApiKey, DeleteApiKeyParams, EditApiKeyParams, IntegrationKey, IntegrationProvider, UpsertIntegrationKeyParams, DeleteIntegrationKeyParams } from './org';
33
- export type { UserMedia, MediaUse, SocialAudio, Media, GetMediaParams, GetSocialAudioParams, UploadMediaParams, UploadMediaResponse, MediaTagUpdate, UpdateMediaTagsParams, MediaTagUpdateResult, UpdateMediaTagsResponse, UpdateMediaTagParams, UpdateMediaNameParams, DeleteMediaParams, DeleteMediaResponse, CreateSocialAudioParams, ImportTextParams, ImportTextResponse, CreateMediaFromUrlParams, GetMediaUseParams, GetMediaUseResponse, FilterMediaParams, FilterMediaResponse, PfpSearchParams, PfpSearchResponse, PfpCandidate, GetUploadTokenParams, UploadTokenResponse, } from './media';
33
+ export type { UserMedia, MediaUse, SocialAudio, Media, GetMediaParams, GetSocialAudioParams, UploadMediaParams, UploadMediaResponse, MediaTagUpdate, UpdateMediaTagsParams, MediaTagUpdateResult, UpdateMediaTagsResponse, UpdateMediaTagParams, UpdateMediaNameParams, DeleteMediaParams, DeleteMediaResponse, CreateSocialAudioParams, ImportTextParams, ImportTextResponse, CreateMediaFromUrlParams, GetMediaUseParams, GetMediaUseResponse, FilterMediaParams, FilterMediaResponse, PfpSearchParams, PfpSearchResponse, PfpCandidate, } from './media';
34
34
  export type { CommentStatus, Comment, CreateCommentParams, CreateCommentResponse, GetCommentsParams, } from './comments';
35
35
  export type { BillingInfo, PortalUrlResponse, CancelAccountParams, CancelAccountResponse, ReactivateAccountParams, ReactivateAccountResponse, CancelSubscriptionResponse, RequestReplacementParams, RequestReplacementResponse, RequestRefundParams, RequestRefundResponse, BillingRequestInfo, PortalParams, } from './billing';
36
36
  export type { RenderJobResponse, RenderJobStatus, SubmitImageRenderJobParams, SubmitVideoRenderJobParams, SubmitScreenshotAnimationRenderJobParams, SubmitAutoCaptionRenderJobParams, SubmitSceneSplitJobParams, SceneSplitDetectorConfig, SubmitInstagramDmRenderJobParams, SubmitIMessageDmRenderJobParams, IgDmMessage, ImDmMessage, RenderVideoEditorConfig, } from './render';
package/dist/media.d.ts CHANGED
@@ -150,13 +150,17 @@ export interface PfpSearchResponse {
150
150
  search_term: string;
151
151
  count: number;
152
152
  }
153
- export interface GetUploadTokenParams {
153
+ export interface OwnedUploadParams {
154
154
  filename: string;
155
- contentType?: string;
155
+ contentType: string;
156
+ size: number;
156
157
  }
157
- export interface UploadTokenResponse {
158
- clientToken: string;
158
+ export interface OwnedUploadResponse {
159
+ url: string;
160
+ fields: Record<string, string>;
159
161
  pathname: string;
162
+ publicUrl: string;
163
+ expiresAt: string;
160
164
  }
161
165
  /**
162
166
  * Client for managing media files
@@ -175,15 +179,12 @@ export declare class MediaClient extends BaseClient {
175
179
  getSocialAudio(params?: GetSocialAudioParams): Promise<ApiResponse<SocialAudio[]>>;
176
180
  /**
177
181
  * Create media from URL(s)
178
- * Creates media records from files already uploaded to blob storage
182
+ * Creates media records from files already uploaded to owned media storage
183
+ * (see getOwnedUpload() for the upload policy)
179
184
  */
180
185
  create(params: CreateMediaFromUrlParams): Promise<ApiResponse<UploadMediaResponse>>;
181
- /**
182
- * Get a client token for direct upload to Vercel Blob storage
183
- * Use this with the `put` function from @vercel/blob/client
184
- * After uploading, call create() with the resulting blob URL
185
- */
186
- getUploadToken(params: GetUploadTokenParams): Promise<ApiResponse<UploadTokenResponse>>;
186
+ /** Request a short-lived Wasabi POST policy for an owned media upload. */
187
+ getOwnedUpload(params: OwnedUploadParams): Promise<ApiResponse<OwnedUploadResponse>>;
187
188
  /**
188
189
  * Update tags on multiple media items in a single request
189
190
  * Works for both user_media and social_audio
@@ -203,7 +204,7 @@ export declare class MediaClient extends BaseClient {
203
204
  updateTag(params: UpdateMediaTagParams): Promise<ApiResponse<UpdateMediaTagsResponse>>;
204
205
  /**
205
206
  * Delete one or more media items
206
- * Also deletes the files from blob storage
207
+ * Also deletes the files from owned media storage
207
208
  */
208
209
  delete(params: DeleteMediaParams): Promise<ApiResponse<DeleteMediaResponse>>;
209
210
  /**
package/dist/media.js CHANGED
@@ -23,18 +23,15 @@ class MediaClient extends base_1.BaseClient {
23
23
  }
24
24
  /**
25
25
  * Create media from URL(s)
26
- * Creates media records from files already uploaded to blob storage
26
+ * Creates media records from files already uploaded to owned media storage
27
+ * (see getOwnedUpload() for the upload policy)
27
28
  */
28
29
  async create(params) {
29
30
  return this.post('/media/create', params);
30
31
  }
31
- /**
32
- * Get a client token for direct upload to Vercel Blob storage
33
- * Use this with the `put` function from @vercel/blob/client
34
- * After uploading, call create() with the resulting blob URL
35
- */
36
- async getUploadToken(params) {
37
- return this.post('/media/create/url', params);
32
+ /** Request a short-lived Wasabi POST policy for an owned media upload. */
33
+ async getOwnedUpload(params) {
34
+ return this.post('/media/create/owned-upload', params);
38
35
  }
39
36
  /**
40
37
  * Update tags on multiple media items in a single request
@@ -61,7 +58,7 @@ class MediaClient extends base_1.BaseClient {
61
58
  }
62
59
  /**
63
60
  * Delete one or more media items
64
- * Also deletes the files from blob storage
61
+ * Also deletes the files from owned media storage
65
62
  */
66
63
  async delete(params) {
67
64
  return this.post('/media/delete', params);
package/dist/posts.d.ts CHANGED
@@ -10,9 +10,13 @@ import type { ApiResponse } from './types';
10
10
  * - `capacity_deferred` — no posting capacity was available; the post will be retried
11
11
  * - `platform_change` — the social app's interface changed and the post could not complete
12
12
  * - `unconfirmed` — posting finished but publication could not be confirmed
13
+ * - `audio_unavailable` — the chosen sound was not available on the platform and no backup audio was left
13
14
  * - `unknown` — the post failed for an unrecognized reason
15
+ *
16
+ * A failed post also carries its category on its `tag`, as a trailing
17
+ * `fail reason: <category>` (removed again when the post is retried).
14
18
  */
15
- export type PostFailCategory = 'device_error' | 'network_error' | 'upload_interrupted' | 'account_issue' | 'capacity_deferred' | 'platform_change' | 'unconfirmed' | 'unknown';
19
+ export type PostFailCategory = 'device_error' | 'network_error' | 'upload_interrupted' | 'account_issue' | 'capacity_deferred' | 'platform_change' | 'unconfirmed' | 'audio_unavailable' | 'unknown';
16
20
  export type PostType = 'video' | 'slideshow';
17
21
  export type PostStatus = 'draft' | 'scheduled' | 'pending' | 'complete' | 'failed' | 'retrying' | 'deleting' | 'deleted' | 'hidden' | 'require-approval';
18
22
  /**
@@ -42,6 +46,10 @@ export interface Post {
42
46
  thumbnail_url: string | null;
43
47
  music_post_id: string | null;
44
48
  social_audio_id: string | null;
49
+ /** Fallback sound. Moved into `social_audio_id` (and cleared) if the platform refuses the primary sound. */
50
+ backup_social_audio_id: string | null;
51
+ /** When the backup sound was swapped in (ISO 8601), or null if it never was. */
52
+ backup_audio_used_at: string | null;
45
53
  scheduled_at: string | null;
46
54
  postUrl?: string;
47
55
  }
@@ -101,6 +109,8 @@ export interface CreateSlideshowParams {
101
109
  caption?: string;
102
110
  title?: string;
103
111
  socialAudioId?: string;
112
+ /** Fallback sound, used once if the platform refuses the primary sound at posting time */
113
+ backupSocialAudioId?: string;
104
114
  postTime?: string;
105
115
  imageUrls: string[];
106
116
  strict?: boolean;
@@ -126,6 +136,8 @@ export interface CreateVideoParams {
126
136
  caption?: string;
127
137
  captionOverlays?: CaptionOverlay[];
128
138
  socialAudioId?: string;
139
+ /** Fallback sound, used once if the platform refuses the primary sound at posting time */
140
+ backupSocialAudioId?: string;
129
141
  postTime?: string;
130
142
  videoUrl: string;
131
143
  strict?: boolean;
@@ -158,6 +170,8 @@ export interface UpdatePostParams {
158
170
  caption?: string;
159
171
  title?: string;
160
172
  socialAudioId?: string;
173
+ /** Fallback sound, used once if the platform refuses the primary sound at posting time; `null` clears it */
174
+ backupSocialAudioId?: string | null;
161
175
  postTime?: string;
162
176
  accountId?: string;
163
177
  mediaUrls?: string[];
@@ -184,6 +198,8 @@ export interface CreateDraftParams {
184
198
  caption?: string;
185
199
  title?: string;
186
200
  socialAudioId?: string;
201
+ /** Fallback sound, used once if the platform refuses the primary sound at posting time */
202
+ backupSocialAudioId?: string;
187
203
  /** Tag stored on the created POST itself (returned as `tag` on the Post object) */
188
204
  post_tag?: string;
189
205
  }
@@ -59,7 +59,7 @@ const defaultAutoCaptionProps = {
59
59
  },
60
60
  };
61
61
  const defaultScreenshotAnimationProps = {
62
- imageUrl: 'https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/IMG_3163-RY0CFNrbnSnqZpaJzi9VdslzfYLLKf.png',
62
+ imageUrl: 'https://media.ugc.inc/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/IMG_3163-RY0CFNrbnSnqZpaJzi9VdslzfYLLKf.png',
63
63
  width: 1206,
64
64
  height: 2622,
65
65
  pauseDurationMs: 1000,
@@ -213,7 +213,7 @@ const instagramDmSchema = zod_1.z.object({
213
213
  // Debug overlay
214
214
  showDebugOverlay: zod_1.z.boolean().optional().default(true).describe('Show debug overlay with mouse coordinates'),
215
215
  // Reference overlay
216
- referenceImageUrl: zod_1.z.string().optional().default('https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/IMG_3153-TiJrpzJKKvsTsjmP9zIX5LpoT8s2hF.png').describe('Reference screenshot URL'),
216
+ referenceImageUrl: zod_1.z.string().optional().default('https://media.ugc.inc/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/IMG_3153-TiJrpzJKKvsTsjmP9zIX5LpoT8s2hF.png').describe('Reference screenshot URL'),
217
217
  showReferenceImage: zod_1.z.boolean().optional().default(true).describe('Show/hide reference image'),
218
218
  referenceOpacity: zod_1.z.number().min(0).max(100).optional().default(50).describe('Reference overlay opacity (0-100)'),
219
219
  // Canvas
@@ -326,7 +326,7 @@ const instagramDmSchema = zod_1.z.object({
326
326
  backArrowLeft: zod_1.z.number().optional().default(78).describe('Back arrow left X'),
327
327
  backArrowRight: zod_1.z.number().optional().default(111).describe('Back arrow right X'),
328
328
  // Header - User profile picture
329
- userPfpUrl: zod_1.z.string().optional().default('https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/884a81ce74fa88e229f6be5f76f29385-3P4rjQqfbLidxtZ7OmvgaDQNMSSIGi.jpg').describe('User profile picture URL'),
329
+ userPfpUrl: zod_1.z.string().optional().default('https://media.ugc.inc/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/884a81ce74fa88e229f6be5f76f29385-3P4rjQqfbLidxtZ7OmvgaDQNMSSIGi.jpg').describe('User profile picture URL'),
330
330
  userPfpTop: zod_1.z.number().optional().default(220).describe('User pfp top Y'),
331
331
  userPfpBottom: zod_1.z.number().optional().default(320).describe('User pfp bottom Y'),
332
332
  userPfpLeft: zod_1.z.number().optional().default(179).describe('User pfp left X'),
@@ -25,7 +25,7 @@ Object.defineProperty(exports, "convertPropsToElements", { enumerable: true, get
25
25
  */
26
26
  exports.defaultIMessageDmProps = {
27
27
  showDebugOverlay: false,
28
- referenceImageUrl: 'https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/06c3ed1c-eefb-4a60-b0c4-5fb872811ba0/IMG_3189-hUoucNOmBoT8fihIL7ybuEXwoRrngz.png',
28
+ referenceImageUrl: 'https://media.ugc.inc/media/06c3ed1c-eefb-4a60-b0c4-5fb872811ba0/IMG_3189-hUoucNOmBoT8fihIL7ybuEXwoRrngz.png',
29
29
  showReferenceImage: false,
30
30
  referenceOpacity: 50,
31
31
  lightMode: false,
@@ -38,7 +38,7 @@ exports.defaultIMessageDmProps = {
38
38
  headerBottom: 424,
39
39
  headerBackgroundColor: '#171717',
40
40
  // Profile section defaults
41
- profilePicUrl: 'https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/06c3ed1c-eefb-4a60-b0c4-5fb872811ba0/pinterest-1766175215011-3-ftQGbunlgHOFdfFtr1PdxxmHD9TAf9.jpg',
41
+ profilePicUrl: 'https://media.ugc.inc/media/06c3ed1c-eefb-4a60-b0c4-5fb872811ba0/pinterest-1766175215011-3-ftQGbunlgHOFdfFtr1PdxxmHD9TAf9.jpg',
42
42
  profilePicTop: 195,
43
43
  profilePicBottom: 347,
44
44
  profilePicLeft: 528,
@@ -225,7 +225,7 @@ exports.defaultIMessageDmProps = {
225
225
  readReceiptRight: 1158,
226
226
  // Example messages
227
227
  messages: [
228
- { id: 'msg-1', sender: 'user', text: 'heyyy fae, help me slide on this baddie pls 🙏', imageUrl: 'https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/06c3ed1c-eefb-4a60-b0c4-5fb872811ba0/pinterest-1766175214139-0-JpXmGIxVWdjEJ3qxGyoC4DAi2KAZrg.jpg' },
228
+ { id: 'msg-1', sender: 'user', text: 'heyyy fae, help me slide on this baddie pls 🙏', imageUrl: 'https://media.ugc.inc/media/06c3ed1c-eefb-4a60-b0c4-5fb872811ba0/pinterest-1766175214139-0-JpXmGIxVWdjEJ3qxGyoC4DAi2KAZrg.jpg' },
229
229
  { id: 'msg-2', sender: 'recipient', text: 'don\'t worry king, i gotchu' },
230
230
  { id: 'msg-3', sender: 'recipient', text: 'that is indeed a cute kitty', groupWithPrevious: true },
231
231
  { id: 'msg-4', sender: 'recipient', text: 'try this line out for speed: are you a kitty? because you deserve pets', groupWithPrevious: true },
@@ -26,7 +26,7 @@ Object.defineProperty(exports, "convertPropsToElements", { enumerable: true, get
26
26
  */
27
27
  exports.defaultInstagramDmProps = {
28
28
  showDebugOverlay: false,
29
- referenceImageUrl: 'https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/IMG_3186-L1M2HgmhYFpnI5uKDrhcKYpRQSzwoz.png',
29
+ referenceImageUrl: 'https://media.ugc.inc/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/IMG_3186-L1M2HgmhYFpnI5uKDrhcKYpRQSzwoz.png',
30
30
  showReferenceImage: false,
31
31
  referenceOpacity: 50,
32
32
  width: 1206,
@@ -89,7 +89,7 @@ exports.defaultInstagramDmProps = {
89
89
  // Message defaults
90
90
  messages: [
91
91
  // Sender group 1 (with story reply)
92
- { id: '1', sender: 'user', text: "if you were a disney princess you'd be jasmine", storyReplyImageUrl: 'https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/ddafc0af3ae404d8223d191bcd424a4a-IMBUblDJvuj2g8LkUZXQ3pJZzfBSTc.jpg' },
92
+ { id: '1', sender: 'user', text: "if you were a disney princess you'd be jasmine", storyReplyImageUrl: 'https://media.ugc.inc/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/ddafc0af3ae404d8223d191bcd424a4a-IMBUblDJvuj2g8LkUZXQ3pJZzfBSTc.jpg' },
93
93
  // Receiver group 1
94
94
  { id: '2', sender: 'recipient', text: 'aww ur sweet' },
95
95
  { id: '3', sender: 'recipient', text: "but i'm clearly more of a mulan 🙄", groupWithPrevious: true },
@@ -131,7 +131,7 @@ exports.defaultInstagramDmProps = {
131
131
  recipientBubbleLeft: 168,
132
132
  senderBubbleRight: 1182,
133
133
  bubbleMaxWidth: 866,
134
- profilePicUrl: 'https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/884a81ce74fa88e229f6be5f76f29385-3P4rjQqfbLidxtZ7OmvgaDQNMSSIGi.jpg',
134
+ profilePicUrl: 'https://media.ugc.inc/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/884a81ce74fa88e229f6be5f76f29385-3P4rjQqfbLidxtZ7OmvgaDQNMSSIGi.jpg',
135
135
  profilePicSize: 83,
136
136
  profilePicGap: 37,
137
137
  // Header - Back arrow
@@ -140,7 +140,7 @@ exports.defaultInstagramDmProps = {
140
140
  backArrowLeft: 78,
141
141
  backArrowRight: 111,
142
142
  // Header - User profile picture
143
- userPfpUrl: 'https://n14dcpakf8w1fekl.public.blob.vercel-storage.com/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/884a81ce74fa88e229f6be5f76f29385-3P4rjQqfbLidxtZ7OmvgaDQNMSSIGi.jpg',
143
+ userPfpUrl: 'https://media.ugc.inc/media/8ff1462b-13d7-4abe-9325-0a6ad19004f0/884a81ce74fa88e229f6be5f76f29385-3P4rjQqfbLidxtZ7OmvgaDQNMSSIGi.jpg',
144
144
  userPfpTop: 220,
145
145
  userPfpBottom: 320,
146
146
  userPfpLeft: 179,
package/dist/render.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  /**
2
- * Render functions for submitting jobs to the Modal renderer.
3
- * These functions call Modal endpoints directly, not the UGC Inc API.
2
+ * Render functions: submit image, video, deduplication, scene-split and DM
3
+ * jobs to the UGC render worker and poll them.
4
+ *
5
+ * The worker's origin is read from `UGC_RENDER_URL` at call time. It is a
6
+ * private origin (the owned render worker is reachable only from the UGC API's
7
+ * network); there is no public default, so calling these functions without the
8
+ * variable set fails fast.
4
9
  */
5
10
  import type { ApiResponse } from './types';
6
11
  import type { VideoEditorNodeConfig, DeduplicationInput } from './render/types';
@@ -156,45 +161,45 @@ export interface SubmitIMessageDmRenderJobParams {
156
161
  dynamicCrop?: import('./render/types/crop').DynamicCropConfig;
157
162
  }
158
163
  /**
159
- * Submit an image render job to the Modal renderer.
164
+ * Submit an image render job to the render worker.
160
165
  * Uses the element-based format for single-source-of-truth rendering.
161
166
  */
162
167
  export declare function submitImageRenderJob(params: SubmitImageRenderJobParams): Promise<ApiResponse<RenderJobResponse>>;
163
168
  /**
164
- * Submit a video render job to the Modal renderer.
169
+ * Submit a video render job to the render worker.
165
170
  */
166
171
  export declare function submitVideoRenderJob(params: SubmitVideoRenderJobParams): Promise<ApiResponse<RenderJobResponse>>;
167
172
  /**
168
- * Get render job status from the Modal renderer.
173
+ * Get render job status from the render worker.
169
174
  */
170
175
  export declare function getRenderJobStatus(jobId: string): Promise<ApiResponse<RenderJobStatus>>;
171
176
  /**
172
- * Submit a deduplication job to the Modal renderer.
177
+ * Submit a deduplication job to the render worker.
173
178
  * Applies hash-breaking, metadata injection, and trace removal to a video.
174
179
  */
175
180
  export declare function submitDeduplicationJob(params: SubmitDeduplicationJobParams): Promise<ApiResponse<RenderJobResponse>>;
176
181
  /**
177
- * Submit a scene split job to the Modal renderer.
182
+ * Submit a scene split job to the render worker.
178
183
  * Detects scene boundaries and splits a video into individual clips.
179
184
  */
180
185
  export declare function submitSceneSplitJob(params: SubmitSceneSplitJobParams): Promise<ApiResponse<RenderJobResponse>>;
181
186
  /**
182
- * Submit a screenshot animation render job to the Modal renderer.
187
+ * Submit a screenshot animation render job to the render worker.
183
188
  * Renders an iPhone screenshot animation video from a source image.
184
189
  */
185
190
  export declare function submitScreenshotAnimationRenderJob(params: SubmitScreenshotAnimationRenderJobParams): Promise<ApiResponse<RenderJobResponse>>;
186
191
  /**
187
- * Submit an auto-caption render job to the Modal renderer.
192
+ * Submit an auto-caption render job to the render worker.
188
193
  * Renders a video with captions overlaid.
189
194
  */
190
195
  export declare function submitAutoCaptionRenderJob(params: SubmitAutoCaptionRenderJobParams): Promise<ApiResponse<RenderJobResponse>>;
191
196
  /**
192
- * Submit an Instagram DM render job to the Modal renderer.
197
+ * Submit an Instagram DM render job to the render worker.
193
198
  * Renders a fake Instagram DM conversation image.
194
199
  */
195
200
  export declare function submitInstagramDmRenderJob(params: SubmitInstagramDmRenderJobParams): Promise<ApiResponse<RenderJobResponse>>;
196
201
  /**
197
- * Submit an iMessage DM render job to the Modal renderer.
202
+ * Submit an iMessage DM render job to the render worker.
198
203
  * Renders a fake iMessage conversation image.
199
204
  */
200
205
  export declare function submitIMessageDmRenderJob(params: SubmitIMessageDmRenderJobParams): Promise<ApiResponse<RenderJobResponse>>;
package/dist/render.js CHANGED
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
2
  /**
3
- * Render functions for submitting jobs to the Modal renderer.
4
- * These functions call Modal endpoints directly, not the UGC Inc API.
3
+ * Render functions: submit image, video, deduplication, scene-split and DM
4
+ * jobs to the UGC render worker and poll them.
5
+ *
6
+ * The worker's origin is read from `UGC_RENDER_URL` at call time. It is a
7
+ * private origin (the owned render worker is reachable only from the UGC API's
8
+ * network); there is no public default, so calling these functions without the
9
+ * variable set fails fast.
5
10
  */
6
11
  Object.defineProperty(exports, "__esModule", { value: true });
7
12
  exports.submitImageRenderJob = submitImageRenderJob;
@@ -13,72 +18,40 @@ exports.submitScreenshotAnimationRenderJob = submitScreenshotAnimationRenderJob;
13
18
  exports.submitAutoCaptionRenderJob = submitAutoCaptionRenderJob;
14
19
  exports.submitInstagramDmRenderJob = submitInstagramDmRenderJob;
15
20
  exports.submitIMessageDmRenderJob = submitIMessageDmRenderJob;
16
- // Modal renderer endpoints
17
- const RENDER_BASE_URL = "https://render.ugc.inc";
18
- const RENDER_SUBMIT_URL = `${RENDER_BASE_URL}/submit-job`;
19
- const RENDER_STATUS_URL = `${RENDER_BASE_URL}/get-status`;
20
- // =============================================================================
21
- // Rate Limiting & Retry (Modal workspace limit: 200 req/s)
22
- // =============================================================================
23
- const MODAL_RATE_LIMIT = 150; // per second, safety margin below 200
24
- const MAX_RETRIES = 3;
25
- // Sliding window: timestamps of recent requests within the last second
26
- const requestTimestamps = [];
27
- function acquireRateSlot() {
28
- const now = Date.now();
29
- // Evict timestamps older than 1 second
30
- while (requestTimestamps.length > 0 && requestTimestamps[0] < now - 1000) {
31
- requestTimestamps.shift();
32
- }
33
- if (requestTimestamps.length < MODAL_RATE_LIMIT) {
34
- requestTimestamps.push(now);
35
- return 0; // no wait needed
36
- }
37
- // Calculate ms until the oldest request exits the window
38
- return 1000 - (now - requestTimestamps[0]) + 10;
39
- }
40
- async function waitForRateSlot() {
41
- let waitMs = acquireRateSlot();
42
- while (waitMs > 0) {
43
- await new Promise(resolve => setTimeout(resolve, waitMs));
44
- waitMs = acquireRateSlot();
21
+ function renderBaseUrl() {
22
+ const url = process.env.UGC_RENDER_URL?.trim();
23
+ if (!url) {
24
+ throw new Error('UGC_RENDER_URL is not set: the render functions need the render worker origin');
45
25
  }
26
+ return url.replace(/\/+$/, '');
46
27
  }
28
+ const MAX_RETRIES = 3;
47
29
  /**
48
- * Fetch wrapper for all Modal API calls.
49
- * Applies rate limiting (150 req/s) and retries on 429/5xx with exponential backoff.
30
+ * Fetch wrapper for every render worker call: retries network errors, 429 and
31
+ * 5xx with exponential backoff; returns 4xx responses to the caller unchanged.
50
32
  */
51
- async function modalFetch(url, options) {
33
+ async function renderFetch(path, options) {
34
+ const url = `${renderBaseUrl()}${path}`;
35
+ let response;
52
36
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
53
- await waitForRateSlot();
54
- let response;
55
37
  try {
56
38
  response = await fetch(url, options);
57
39
  }
58
40
  catch (error) {
59
- // Network error — retry with backoff
60
41
  if (attempt < MAX_RETRIES) {
61
- const backoff = Math.pow(2, attempt) * 500 + Math.random() * 500;
62
- await new Promise(resolve => setTimeout(resolve, backoff));
42
+ await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 500 + Math.random() * 500));
63
43
  continue;
64
44
  }
65
45
  throw error;
66
46
  }
67
- // Success or non-retryable client error (400, 401, 403, 404, etc.)
68
47
  if (response.ok || (response.status >= 400 && response.status < 500 && response.status !== 429)) {
69
48
  return response;
70
49
  }
71
- // Retryable: 429 (rate limit) or 5xx (server error)
72
50
  if (attempt < MAX_RETRIES) {
73
- const backoff = Math.pow(2, attempt) * 500 + Math.random() * 500;
74
- await new Promise(resolve => setTimeout(resolve, backoff));
75
- continue;
51
+ await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 500 + Math.random() * 500));
76
52
  }
77
- return response; // Return error response on final attempt
78
53
  }
79
- // Unreachable, but satisfies TypeScript
80
- await waitForRateSlot();
81
- return fetch(url, options);
54
+ return response;
82
55
  }
83
56
  // =============================================================================
84
57
  // DM Message Transformation Helpers
@@ -131,12 +104,12 @@ function transformToIMessageMessages(messages, imageAttachmentUrl) {
131
104
  // Render Functions
132
105
  // =============================================================================
133
106
  /**
134
- * Submit an image render job to the Modal renderer.
107
+ * Submit an image render job to the render worker.
135
108
  * Uses the element-based format for single-source-of-truth rendering.
136
109
  */
137
110
  async function submitImageRenderJob(params) {
138
111
  try {
139
- const response = await modalFetch(RENDER_SUBMIT_URL, {
112
+ const response = await renderFetch('/submit-job', {
140
113
  method: 'POST',
141
114
  headers: { 'Content-Type': 'application/json' },
142
115
  body: JSON.stringify({
@@ -164,7 +137,7 @@ async function submitImageRenderJob(params) {
164
137
  data = JSON.parse(text);
165
138
  }
166
139
  catch {
167
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
140
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
168
141
  }
169
142
  if (data.status === 'error' || !response.ok) {
170
143
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to submit render job' };
@@ -177,11 +150,11 @@ async function submitImageRenderJob(params) {
177
150
  }
178
151
  }
179
152
  /**
180
- * Submit a video render job to the Modal renderer.
153
+ * Submit a video render job to the render worker.
181
154
  */
182
155
  async function submitVideoRenderJob(params) {
183
156
  try {
184
- const response = await modalFetch(RENDER_SUBMIT_URL, {
157
+ const response = await renderFetch('/submit-job', {
185
158
  method: 'POST',
186
159
  headers: { 'Content-Type': 'application/json' },
187
160
  body: JSON.stringify({
@@ -202,7 +175,7 @@ async function submitVideoRenderJob(params) {
202
175
  data = JSON.parse(text);
203
176
  }
204
177
  catch {
205
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
178
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
206
179
  }
207
180
  if (data.status === 'error' || !response.ok) {
208
181
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to submit render job' };
@@ -215,11 +188,11 @@ async function submitVideoRenderJob(params) {
215
188
  }
216
189
  }
217
190
  /**
218
- * Get render job status from the Modal renderer.
191
+ * Get render job status from the render worker.
219
192
  */
220
193
  async function getRenderJobStatus(jobId) {
221
194
  try {
222
- const response = await modalFetch(RENDER_STATUS_URL, {
195
+ const response = await renderFetch('/get-status', {
223
196
  method: 'POST',
224
197
  headers: { 'Content-Type': 'application/json' },
225
198
  body: JSON.stringify({ job_id: jobId }),
@@ -230,7 +203,7 @@ async function getRenderJobStatus(jobId) {
230
203
  data = JSON.parse(text);
231
204
  }
232
205
  catch {
233
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
206
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
234
207
  }
235
208
  if (data.status === 'error' || !response.ok) {
236
209
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to get job status' };
@@ -247,12 +220,12 @@ async function getRenderJobStatus(jobId) {
247
220
  }
248
221
  }
249
222
  /**
250
- * Submit a deduplication job to the Modal renderer.
223
+ * Submit a deduplication job to the render worker.
251
224
  * Applies hash-breaking, metadata injection, and trace removal to a video.
252
225
  */
253
226
  async function submitDeduplicationJob(params) {
254
227
  try {
255
- const response = await modalFetch(RENDER_SUBMIT_URL, {
228
+ const response = await renderFetch('/submit-job', {
256
229
  method: 'POST',
257
230
  headers: { 'Content-Type': 'application/json' },
258
231
  body: JSON.stringify({
@@ -267,7 +240,7 @@ async function submitDeduplicationJob(params) {
267
240
  data = JSON.parse(text);
268
241
  }
269
242
  catch {
270
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
243
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
271
244
  }
272
245
  if (data.status === 'error' || !response.ok) {
273
246
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to submit deduplication job' };
@@ -280,12 +253,12 @@ async function submitDeduplicationJob(params) {
280
253
  }
281
254
  }
282
255
  /**
283
- * Submit a scene split job to the Modal renderer.
256
+ * Submit a scene split job to the render worker.
284
257
  * Detects scene boundaries and splits a video into individual clips.
285
258
  */
286
259
  async function submitSceneSplitJob(params) {
287
260
  try {
288
- const response = await modalFetch(RENDER_SUBMIT_URL, {
261
+ const response = await renderFetch('/submit-job', {
289
262
  method: 'POST',
290
263
  headers: { 'Content-Type': 'application/json' },
291
264
  body: JSON.stringify({
@@ -300,7 +273,7 @@ async function submitSceneSplitJob(params) {
300
273
  data = JSON.parse(text);
301
274
  }
302
275
  catch {
303
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
276
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
304
277
  }
305
278
  if (data.status === 'error' || !response.ok) {
306
279
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to submit scene split job' };
@@ -313,12 +286,12 @@ async function submitSceneSplitJob(params) {
313
286
  }
314
287
  }
315
288
  /**
316
- * Submit a screenshot animation render job to the Modal renderer.
289
+ * Submit a screenshot animation render job to the render worker.
317
290
  * Renders an iPhone screenshot animation video from a source image.
318
291
  */
319
292
  async function submitScreenshotAnimationRenderJob(params) {
320
293
  try {
321
- const response = await modalFetch(RENDER_SUBMIT_URL, {
294
+ const response = await renderFetch('/submit-job', {
322
295
  method: 'POST',
323
296
  headers: { 'Content-Type': 'application/json' },
324
297
  body: JSON.stringify({
@@ -337,7 +310,7 @@ async function submitScreenshotAnimationRenderJob(params) {
337
310
  data = JSON.parse(text);
338
311
  }
339
312
  catch {
340
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
313
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
341
314
  }
342
315
  if (data.status === 'error' || !response.ok) {
343
316
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to submit screenshot animation job' };
@@ -350,12 +323,12 @@ async function submitScreenshotAnimationRenderJob(params) {
350
323
  }
351
324
  }
352
325
  /**
353
- * Submit an auto-caption render job to the Modal renderer.
326
+ * Submit an auto-caption render job to the render worker.
354
327
  * Renders a video with captions overlaid.
355
328
  */
356
329
  async function submitAutoCaptionRenderJob(params) {
357
330
  try {
358
- const response = await modalFetch(RENDER_SUBMIT_URL, {
331
+ const response = await renderFetch('/submit-job', {
359
332
  method: 'POST',
360
333
  headers: { 'Content-Type': 'application/json' },
361
334
  body: JSON.stringify({
@@ -379,7 +352,7 @@ async function submitAutoCaptionRenderJob(params) {
379
352
  data = JSON.parse(text);
380
353
  }
381
354
  catch {
382
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
355
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
383
356
  }
384
357
  if (data.status === 'error' || !response.ok) {
385
358
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to submit auto-caption job' };
@@ -392,14 +365,14 @@ async function submitAutoCaptionRenderJob(params) {
392
365
  }
393
366
  }
394
367
  /**
395
- * Submit an Instagram DM render job to the Modal renderer.
368
+ * Submit an Instagram DM render job to the render worker.
396
369
  * Renders a fake Instagram DM conversation image.
397
370
  */
398
371
  async function submitInstagramDmRenderJob(params) {
399
372
  try {
400
373
  const transformedMessages = transformToInstagramMessages(params.messages, params.imageAttachmentUrl);
401
374
  const hasStoryReply = params.messages.some(msg => msg.hasImage);
402
- const response = await modalFetch(RENDER_SUBMIT_URL, {
375
+ const response = await renderFetch('/submit-job', {
403
376
  method: 'POST',
404
377
  headers: { 'Content-Type': 'application/json' },
405
378
  body: JSON.stringify({
@@ -424,7 +397,7 @@ async function submitInstagramDmRenderJob(params) {
424
397
  data = JSON.parse(text);
425
398
  }
426
399
  catch {
427
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
400
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
428
401
  }
429
402
  if (data.status === 'error' || !response.ok) {
430
403
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to submit Instagram DM job' };
@@ -437,13 +410,13 @@ async function submitInstagramDmRenderJob(params) {
437
410
  }
438
411
  }
439
412
  /**
440
- * Submit an iMessage DM render job to the Modal renderer.
413
+ * Submit an iMessage DM render job to the render worker.
441
414
  * Renders a fake iMessage conversation image.
442
415
  */
443
416
  async function submitIMessageDmRenderJob(params) {
444
417
  try {
445
418
  const transformedMessages = transformToIMessageMessages(params.messages, params.imageAttachmentUrl);
446
- const response = await modalFetch(RENDER_SUBMIT_URL, {
419
+ const response = await renderFetch('/submit-job', {
447
420
  method: 'POST',
448
421
  headers: { 'Content-Type': 'application/json' },
449
422
  body: JSON.stringify({
@@ -473,7 +446,7 @@ async function submitIMessageDmRenderJob(params) {
473
446
  data = JSON.parse(text);
474
447
  }
475
448
  catch {
476
- return { ok: false, code: response.status || 500, message: `Modal endpoint error: ${text.substring(0, 100)}` };
449
+ return { ok: false, code: response.status || 500, message: `Render endpoint error: ${text.substring(0, 100)}` };
477
450
  }
478
451
  if (data.status === 'error' || !response.ok) {
479
452
  return { ok: false, code: response.status, message: data.error ?? data.message ?? 'Failed to submit iMessage DM job' };
@@ -26,6 +26,7 @@ exports.postTools = [
26
26
  videoUrl: zod_1.z.string().describe('URL of the video file'),
27
27
  caption: zod_1.z.string().optional().describe('Post caption'),
28
28
  socialAudioId: zod_1.z.string().optional().describe('Social audio ID to use'),
29
+ backupSocialAudioId: zod_1.z.string().optional().describe('Fallback social audio ID, used once if the platform refuses the primary sound'),
29
30
  postTime: zod_1.z.string().optional().describe('Scheduled time (ISO 8601). Omit for next available slot.'),
30
31
  strict: zod_1.z.boolean().optional().describe('If true with auto-selection, must post at exact time or error'),
31
32
  mustPostBy: zod_1.z.string().optional().describe('Latest allowed scheduled time (ISO 8601). Errors if no slot is available before this deadline.'),
@@ -47,6 +48,7 @@ exports.postTools = [
47
48
  caption: zod_1.z.string().optional().describe('Post caption'),
48
49
  title: zod_1.z.string().optional().describe('Slideshow title'),
49
50
  socialAudioId: zod_1.z.string().optional().describe('Social audio ID to use'),
51
+ backupSocialAudioId: zod_1.z.string().optional().describe('Fallback social audio ID, used once if the platform refuses the primary sound'),
50
52
  postTime: zod_1.z.string().optional().describe('Scheduled time (ISO 8601)'),
51
53
  strict: zod_1.z.boolean().optional().describe('If true with auto-selection, must post at exact time or error'),
52
54
  mustPostBy: zod_1.z.string().optional().describe('Latest allowed scheduled time (ISO 8601). Errors if no slot is available before this deadline.'),
@@ -68,6 +70,7 @@ exports.postTools = [
68
70
  caption: zod_1.z.string().optional().describe('Post caption'),
69
71
  title: zod_1.z.string().optional().describe('Slideshow title'),
70
72
  socialAudioId: zod_1.z.string().optional().describe('Social audio ID'),
73
+ backupSocialAudioId: zod_1.z.string().optional().describe('Fallback social audio ID, used once if the platform refuses the primary sound'),
71
74
  post_tag: zod_1.z.string().optional().describe('Tag stored on the created post'),
72
75
  }),
73
76
  execute: async (client, params) => {
@@ -92,6 +95,7 @@ exports.postTools = [
92
95
  caption: zod_1.z.string().optional().describe('New caption'),
93
96
  title: zod_1.z.string().optional().describe('New title (slideshows only)'),
94
97
  socialAudioId: zod_1.z.string().optional().describe('New social audio ID'),
98
+ backupSocialAudioId: zod_1.z.string().nullable().optional().describe('Fallback social audio ID, used once if the platform refuses the primary sound; null clears it'),
95
99
  postTime: zod_1.z.string().optional().describe('New scheduled time (ISO 8601)'),
96
100
  accountId: zod_1.z.string().optional().describe('Move post to different account'),
97
101
  mediaUrls: zod_1.z.array(zod_1.z.string()).optional().describe('New media URLs'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugcinc",
3
- "version": "4.14.0",
3
+ "version": "4.15.1",
4
4
  "description": "TypeScript/JavaScript client for the UGC Inc API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",