vidspotai-shared 1.0.94 → 1.0.96

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.
Files changed (136) hide show
  1. package/lib/globals/aiModels/enums.d.ts +2 -0
  2. package/lib/globals/aiModels/enums.d.ts.map +1 -1
  3. package/lib/globals/aiModels/enums.js +5 -0
  4. package/lib/globals/aiModels/index.d.ts.map +1 -1
  5. package/lib/globals/aiModels/index.js +2 -0
  6. package/lib/globals/aiModels/providers/hostedAvatars.d.ts +22 -0
  7. package/lib/globals/aiModels/providers/hostedAvatars.d.ts.map +1 -0
  8. package/lib/globals/aiModels/providers/hostedAvatars.js +129 -0
  9. package/lib/globals/types.d.ts +58 -1
  10. package/lib/globals/types.d.ts.map +1 -1
  11. package/lib/globals/types.js +61 -1
  12. package/lib/models/demoVideoProject.model.d.ts +244 -0
  13. package/lib/models/demoVideoProject.model.d.ts.map +1 -0
  14. package/lib/models/demoVideoProject.model.js +2 -0
  15. package/lib/models/githubConnection.model.d.ts +35 -0
  16. package/lib/models/githubConnection.model.d.ts.map +1 -0
  17. package/lib/models/githubConnection.model.js +2 -0
  18. package/lib/models/index.d.ts +3 -0
  19. package/lib/models/index.d.ts.map +1 -1
  20. package/lib/models/index.js +3 -0
  21. package/lib/models/outreach.model.d.ts +175 -0
  22. package/lib/models/outreach.model.d.ts.map +1 -0
  23. package/lib/models/outreach.model.js +2 -0
  24. package/lib/models/video.model.d.ts +4 -0
  25. package/lib/models/video.model.d.ts.map +1 -1
  26. package/lib/schemas/brandKit.schema.d.ts +4 -0
  27. package/lib/schemas/brandKit.schema.d.ts.map +1 -1
  28. package/lib/schemas/brandKit.schema.js +10 -0
  29. package/lib/schemas/demoQuestion.schema.d.ts +286 -0
  30. package/lib/schemas/demoQuestion.schema.d.ts.map +1 -0
  31. package/lib/schemas/demoQuestion.schema.js +210 -0
  32. package/lib/schemas/demoSpec.schema.d.ts +109 -6
  33. package/lib/schemas/demoSpec.schema.d.ts.map +1 -1
  34. package/lib/schemas/demoSpec.schema.js +95 -7
  35. package/lib/schemas/index.d.ts +1 -0
  36. package/lib/schemas/index.d.ts.map +1 -1
  37. package/lib/schemas/index.js +1 -0
  38. package/lib/schemas/videoPlan.schema.d.ts +2 -2
  39. package/lib/schemas/videoPlan.schema.d.ts.map +1 -1
  40. package/lib/schemas/videoPlan.schema.js +11 -1
  41. package/lib/services/agent/providerFallback/chains.d.ts.map +1 -1
  42. package/lib/services/agent/providerFallback/chains.js +9 -1
  43. package/lib/services/agent/taskPoller.d.ts.map +1 -1
  44. package/lib/services/agent/taskPoller.js +17 -6
  45. package/lib/services/agent/tools/generateAvatarVideo.tool.d.ts +21 -9
  46. package/lib/services/agent/tools/generateAvatarVideo.tool.d.ts.map +1 -1
  47. package/lib/services/agent/tools/generateAvatarVideo.tool.js +135 -39
  48. package/lib/services/agent/tools/planVideo.tool.d.ts +2 -2
  49. package/lib/services/aiGen/aiGenFactory.service.d.ts +1 -1
  50. package/lib/services/aiGen/aiGenFactory.service.d.ts.map +1 -1
  51. package/lib/services/aiGen/aiGenFactory.service.js +10 -2
  52. package/lib/services/aiGen/helpers.d.ts.map +1 -1
  53. package/lib/services/aiGen/helpers.js +12 -2
  54. package/lib/services/aiGen/providers/kling/kling.service.d.ts.map +1 -1
  55. package/lib/services/aiGen/providers/kling/kling.service.js +47 -8
  56. package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts +8 -0
  57. package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts.map +1 -1
  58. package/lib/services/aiGen/providers/pixverse/pixverse.service.js +55 -2
  59. package/lib/services/avatarGen/IAvatarVideoProvider.d.ts +136 -0
  60. package/lib/services/avatarGen/IAvatarVideoProvider.d.ts.map +1 -0
  61. package/lib/services/avatarGen/IAvatarVideoProvider.js +42 -0
  62. package/lib/services/avatarGen/avatarProviderFactory.d.ts +18 -0
  63. package/lib/services/avatarGen/avatarProviderFactory.d.ts.map +1 -0
  64. package/lib/services/avatarGen/avatarProviderFactory.js +54 -0
  65. package/lib/services/avatarGen/index.d.ts +6 -0
  66. package/lib/services/avatarGen/index.d.ts.map +1 -0
  67. package/lib/services/avatarGen/index.js +21 -0
  68. package/lib/services/avatarGen/providers/heygenAvatarProvider.d.ts +45 -0
  69. package/lib/services/avatarGen/providers/heygenAvatarProvider.d.ts.map +1 -0
  70. package/lib/services/avatarGen/providers/heygenAvatarProvider.js +190 -0
  71. package/lib/services/avatarGen/providers/klingAvatarProvider.d.ts +32 -0
  72. package/lib/services/avatarGen/providers/klingAvatarProvider.d.ts.map +1 -0
  73. package/lib/services/avatarGen/providers/klingAvatarProvider.js +69 -0
  74. package/lib/services/avatarGen/providers/tavusAvatarProvider.d.ts +56 -0
  75. package/lib/services/avatarGen/providers/tavusAvatarProvider.d.ts.map +1 -0
  76. package/lib/services/avatarGen/providers/tavusAvatarProvider.js +183 -0
  77. package/lib/services/demo/demoAuthor.d.ts +45 -0
  78. package/lib/services/demo/demoAuthor.d.ts.map +1 -0
  79. package/lib/services/demo/demoAuthor.js +75 -0
  80. package/lib/services/demo/demoAuthorNormalize.d.ts +49 -0
  81. package/lib/services/demo/demoAuthorNormalize.d.ts.map +1 -0
  82. package/lib/services/demo/demoAuthorNormalize.js +327 -0
  83. package/lib/services/demo/demoAuthorPrompt.d.ts +102 -0
  84. package/lib/services/demo/demoAuthorPrompt.d.ts.map +1 -0
  85. package/lib/services/demo/demoAuthorPrompt.js +243 -0
  86. package/lib/services/demo/demoCredits.d.ts +20 -0
  87. package/lib/services/demo/demoCredits.d.ts.map +1 -0
  88. package/lib/services/demo/demoCredits.js +51 -0
  89. package/lib/services/demo/demoMediaGuard.d.ts +43 -0
  90. package/lib/services/demo/demoMediaGuard.d.ts.map +1 -0
  91. package/lib/services/demo/demoMediaGuard.js +207 -0
  92. package/lib/services/demo/demoMusic.d.ts +13 -0
  93. package/lib/services/demo/demoMusic.d.ts.map +1 -0
  94. package/lib/services/demo/demoMusic.js +93 -0
  95. package/lib/services/demo/demoPrefill.d.ts +76 -0
  96. package/lib/services/demo/demoPrefill.d.ts.map +1 -0
  97. package/lib/services/demo/demoPrefill.js +435 -0
  98. package/lib/services/demo/demoRevisePrompt.d.ts +17 -0
  99. package/lib/services/demo/demoRevisePrompt.d.ts.map +1 -0
  100. package/lib/services/demo/demoRevisePrompt.js +85 -0
  101. package/lib/services/demo/demoReviser.d.ts +63 -0
  102. package/lib/services/demo/demoReviser.d.ts.map +1 -0
  103. package/lib/services/demo/demoReviser.js +81 -0
  104. package/lib/services/demo/demoScriptPrompt.d.ts +63 -0
  105. package/lib/services/demo/demoScriptPrompt.d.ts.map +1 -0
  106. package/lib/services/demo/demoScriptPrompt.js +234 -0
  107. package/lib/services/demo/demoScriptWriter.d.ts +65 -0
  108. package/lib/services/demo/demoScriptWriter.d.ts.map +1 -0
  109. package/lib/services/demo/demoScriptWriter.js +211 -0
  110. package/lib/services/demo/demoTriage.d.ts +65 -0
  111. package/lib/services/demo/demoTriage.d.ts.map +1 -0
  112. package/lib/services/demo/demoTriage.js +130 -0
  113. package/lib/services/demo/githubOAuth.d.ts +78 -0
  114. package/lib/services/demo/githubOAuth.d.ts.map +1 -0
  115. package/lib/services/demo/githubOAuth.js +209 -0
  116. package/lib/services/demo/githubOAuthState.d.ts +10 -0
  117. package/lib/services/demo/githubOAuthState.d.ts.map +1 -0
  118. package/lib/services/demo/githubOAuthState.js +63 -0
  119. package/lib/services/demo/index.d.ts +24 -0
  120. package/lib/services/demo/index.d.ts.map +1 -0
  121. package/lib/services/demo/index.js +39 -0
  122. package/lib/services/firestore.service.d.ts +11 -0
  123. package/lib/services/firestore.service.d.ts.map +1 -1
  124. package/lib/services/firestore.service.js +24 -0
  125. package/lib/services/index.d.ts +2 -0
  126. package/lib/services/index.d.ts.map +1 -1
  127. package/lib/services/index.js +2 -0
  128. package/lib/services/tts/providers/elevenlabs.service.d.ts.map +1 -1
  129. package/lib/services/tts/providers/elevenlabs.service.js +16 -8
  130. package/lib/utils/index.d.ts +1 -0
  131. package/lib/utils/index.d.ts.map +1 -1
  132. package/lib/utils/index.js +1 -0
  133. package/lib/utils/outreachTemplate.d.ts +46 -0
  134. package/lib/utils/outreachTemplate.d.ts.map +1 -0
  135. package/lib/utils/outreachTemplate.js +174 -0
  136. package/package.json +1 -1
@@ -4,79 +4,177 @@ exports.generateAvatarVideoTool = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const aiModels_1 = require("../../../globals/aiModels");
6
6
  const firebase_1 = require("../../../libs/firebase");
7
- const aiGenFactory_service_1 = require("../../aiGen/aiGenFactory.service");
7
+ const avatarProviderFactory_1 = require("../../avatarGen/avatarProviderFactory");
8
8
  const types_1 = require("../../aiGen/types");
9
9
  const rateLimiter_1 = require("../../rateLimiter");
10
10
  const ttsFactory_service_1 = require("../../tts/ttsFactory.service");
11
11
  const toolRegistry_1 = require("../toolRegistry");
12
12
  /**
13
- * generate_avatar_video — wraps the existing kling-avatar pipeline so the
14
- * planner's `talking-head-avatar` scene strategy is executable. Accepts a
15
- * presenter face image URL plus either a pre-uploaded audio URL or raw TTS
16
- * text (in which case the tool synthesizes via ElevenLabs and uploads to GCS
17
- * before handing off, since Kling avatar requires a public sound_file URL).
13
+ * generate_avatar_video — drives avatar generation through the dedicated avatar
14
+ * provider seam (getAvatarVideoProvider / IAvatarVideoProvider), so the planner's
15
+ * `talking-head-avatar` scene strategy is executable. Avatar gen is handled by
16
+ * the avatar provider NOT the generic video-gen provider. Two engine families:
18
17
  *
19
- * Returns a provider task id; the executor / monitor layer polls completion
20
- * exactly as it does for generate_video.
18
+ * - byo-media (default kling-avatar): caller supplies a presenter face image
19
+ * plus either a pre-uploaded audio URL or raw TTS text (the tool synthesizes
20
+ * via ElevenLabs + uploads to GCS first, since the engine needs a public
21
+ * audio URL).
22
+ * - hosted (heygen-avatar / tavus-avatar, via `model`): caller supplies a
23
+ * provider-hosted avatar id + a script; the engine runs its own TTS/lipsync,
24
+ * so the tool does NO TTS and passes no audio.
25
+ *
26
+ * Returns a provider task id; the executor's TaskPoller polls completion — it
27
+ * routes hosted models through the avatar seam and everything else through the
28
+ * generic video factory, so this works for both families.
21
29
  */
22
30
  const InputSchema = zod_1.z
23
31
  .object({
32
+ model: zod_1.z
33
+ .nativeEnum(aiModels_1.EVideoGenModels)
34
+ .optional()
35
+ .describe("Avatar engine. Default kling-avatar (byo-media: animate a face image). heygen-avatar / tavus-avatar are hosted (animate a provider-hosted avatar + native TTS)."),
24
36
  inputImageUrl: zod_1.z
25
37
  .string()
26
38
  .url()
27
- .describe("Presenter face still — Kling animates this image to lipsync the audio."),
39
+ .optional()
40
+ .describe("byo-media: presenter face still the engine animates to lipsync the audio. Required for byo-media; omit for hosted."),
41
+ hostedAvatarId: zod_1.z
42
+ .string()
43
+ .optional()
44
+ .describe("hosted: provider-hosted avatar id (HeyGen) or replica_id (Tavus) to animate. Required for hosted engines."),
45
+ hostedVoiceId: zod_1.z
46
+ .string()
47
+ .optional()
48
+ .describe("hosted: provider-hosted voice id (HeyGen). Unused for Tavus (the replica owns its voice)."),
28
49
  ttsText: zod_1.z
29
50
  .string()
30
51
  .min(1)
31
52
  .optional()
32
- .describe("VO line to synthesize. Either ttsText or inputAudioUrl must be set; if both are set, inputAudioUrl wins."),
53
+ .describe("VO line / script. byo-media: synthesized to audio (or use inputAudioUrl; if both set, inputAudioUrl wins). hosted: spoken natively by the engine (required)."),
33
54
  inputAudioUrl: zod_1.z
34
55
  .string()
35
56
  .url()
36
57
  .optional()
37
- .describe("Pre-uploaded audio URL (mp3/wav). Skips internal TTS+upload."),
58
+ .describe("byo-media only: pre-uploaded audio URL (mp3/wav). Skips internal TTS+upload."),
38
59
  voiceId: zod_1.z.string().optional(),
39
60
  languageCode: zod_1.z.string().optional(),
40
61
  mode: zod_1.z.enum(["std", "pro"]).default("std"),
41
62
  prompt: zod_1.z
42
63
  .string()
43
64
  .optional()
44
- .describe("Optional motion/emotion hint passed through to Kling."),
65
+ .describe("byo-media only: optional motion/emotion hint passed through to the engine."),
45
66
  })
46
67
  .superRefine((v, ctx) => {
47
- if (!v.ttsText && !v.inputAudioUrl) {
48
- ctx.addIssue({
49
- code: zod_1.z.ZodIssueCode.custom,
50
- message: "Either ttsText or inputAudioUrl is required.",
51
- });
68
+ if (v.model && (0, avatarProviderFactory_1.isHostedAvatarModel)(v.model)) {
69
+ if (!v.hostedAvatarId) {
70
+ ctx.addIssue({
71
+ code: zod_1.z.ZodIssueCode.custom,
72
+ message: "hostedAvatarId is required for hosted avatar engines.",
73
+ path: ["hostedAvatarId"],
74
+ });
75
+ }
76
+ if (!v.ttsText) {
77
+ ctx.addIssue({
78
+ code: zod_1.z.ZodIssueCode.custom,
79
+ message: "ttsText (the script) is required for hosted avatar engines.",
80
+ path: ["ttsText"],
81
+ });
82
+ }
83
+ }
84
+ else {
85
+ if (!v.inputImageUrl) {
86
+ ctx.addIssue({
87
+ code: zod_1.z.ZodIssueCode.custom,
88
+ message: "inputImageUrl is required for byo-media avatar engines.",
89
+ path: ["inputImageUrl"],
90
+ });
91
+ }
92
+ if (!v.ttsText && !v.inputAudioUrl) {
93
+ ctx.addIssue({
94
+ code: zod_1.z.ZodIssueCode.custom,
95
+ message: "Either ttsText or inputAudioUrl is required.",
96
+ });
97
+ }
52
98
  }
53
99
  });
54
100
  const OutputSchema = zod_1.z.object({
55
101
  status: zod_1.z.literal("triggered"),
56
102
  taskId: zod_1.z.string(),
57
- audioUrl: zod_1.z.string().url().describe("Audio URL fed to Kling — host can reuse for caption sync."),
103
+ audioUrl: zod_1.z
104
+ .string()
105
+ .url()
106
+ .optional()
107
+ .describe("byo-media: audio URL fed to the engine — host can reuse for caption sync. Absent for hosted engines (native TTS)."),
58
108
  });
59
109
  exports.generateAvatarVideoTool = {
60
110
  name: "generate_avatar_video",
61
111
  description: [
62
- "Generate a talking-head avatar video by animating a presenter face still",
63
- "to lipsync a voiceover. Wraps the kling-avatar provider.",
112
+ "Generate a talking-head avatar video that lipsyncs a voiceover. Routed",
113
+ "through the dedicated avatar provider seam. Two engine families: byo-media",
114
+ "(default kling-avatar) animates a presenter face still you supply; hosted",
115
+ "(heygen-avatar / tavus-avatar, via `model`) animates a provider-hosted",
116
+ "avatar and runs its own TTS from a script.",
64
117
  "Use this for the `talking-head-avatar` scene strategy. Do NOT use it for",
65
118
  "general motion shots — generate_video is cheaper and more flexible there.",
66
119
  "Examples:",
67
120
  ' { inputImageUrl: "https://.../face.png", ttsText: "Hi, today we\'re testing the new serum." }',
68
121
  ' { inputImageUrl: "https://.../face.png", inputAudioUrl: "https://.../vo.mp3", mode: "pro" }',
122
+ ' { model: "heygen-avatar", hostedAvatarId: "Daisy-inskirt-20220818", hostedVoiceId: "2d5b0e6cf36f460aa7fc47e3eee4ba54", ttsText: "Welcome back!" }',
69
123
  ].join(" "),
70
124
  inputSchema: InputSchema,
71
125
  outputSchema: OutputSchema,
72
126
  idempotent: false,
73
127
  estimateCost: async (input) => {
74
- // Kling avatar bills per audio second. Without a length we assume 6s (typical scene).
128
+ // Without a length we assume 6s (typical scene).
75
129
  const approxSec = input.ttsText ? Math.max(2, Math.ceil(input.ttsText.length / 15)) : 6;
130
+ const modelKey = input.model ?? aiModels_1.EVideoGenModels.KLING_AVATAR;
131
+ if ((0, avatarProviderFactory_1.isHostedAvatarModel)(modelKey)) {
132
+ // Hosted engines bill per generated MINUTE (min 1 minute).
133
+ const perMinute = aiModels_1.aiModelConfigs[modelKey]?.cost?.perMinute ?? 0.5;
134
+ const billedMinutes = Math.max(1, Math.ceil(approxSec / 60));
135
+ return Math.ceil(perMinute * billedMinutes * 100); // rough credit conversion
136
+ }
137
+ // byo-media (Kling) bills per audio second.
76
138
  const perSec = input.mode === "pro" ? 0.028 : 0.014;
77
- return Math.ceil(approxSec * perSec * 100); // rough credit conversion
139
+ return Math.ceil(approxSec * perSec * 100);
78
140
  },
79
141
  handler: async (input, ctx) => {
142
+ const modelKey = input.model ?? aiModels_1.EVideoGenModels.KLING_AVATAR;
143
+ const renderMode = input.mode === "pro" ? types_1.EVideoMode.PROFESSIONAL : types_1.EVideoMode.STANDARD;
144
+ // ── Hosted engines (HeyGen/Tavus) ────────────────────────────────────────
145
+ // The engine does its own TTS + lipsync from the script, so there's no TTS
146
+ // synthesis / GCS upload here. Submit and hand the task id to the poller,
147
+ // which routes hosted models through the avatar seam.
148
+ if ((0, avatarProviderFactory_1.isHostedAvatarModel)(modelKey)) {
149
+ const limiter = (0, rateLimiter_1.getAiGenModelRateLimiter)(modelKey);
150
+ await limiter.waitUntilAvailable();
151
+ let heldSlot = true;
152
+ try {
153
+ const provider = (0, avatarProviderFactory_1.getAvatarVideoProvider)(modelKey);
154
+ const result = await provider.generate({
155
+ mode: "hosted",
156
+ hostedAvatarId: input.hostedAvatarId,
157
+ hostedVoiceId: input.hostedVoiceId ?? "",
158
+ script: input.ttsText,
159
+ locale: input.languageCode,
160
+ renderMode,
161
+ });
162
+ const taskId = result.providerTaskId?.taskId ??
163
+ result.providerTaskId?.id ??
164
+ (typeof result.providerTaskId === "string" ? result.providerTaskId : "unknown");
165
+ if (taskId === "unknown") {
166
+ limiter.releaseSlot();
167
+ heldSlot = false;
168
+ }
169
+ return { status: "triggered", taskId };
170
+ }
171
+ catch (err) {
172
+ if (heldSlot)
173
+ limiter.releaseSlot();
174
+ throw err;
175
+ }
176
+ }
177
+ // ── byo-media engines (Kling) ─────────────────────────────────────────────
80
178
  let audioUrl = input.inputAudioUrl;
81
179
  if (!audioUrl) {
82
180
  const ttsLimiter = (0, rateLimiter_1.getTtsProviderRateLimiter)("elevenlabs");
@@ -100,29 +198,27 @@ exports.generateAvatarVideoTool = {
100
198
  const [signedUrl] = await file.getSignedUrl({ action: "read", expires: "03-09-2491" });
101
199
  audioUrl = signedUrl;
102
200
  }
103
- // Hold the Kling-avatar slot through return — host's TaskPoller releases
201
+ // Hold the byo-media model slot through return — host's TaskPoller releases
104
202
  // when the task settles. Release here on submit error.
105
- const limiter = (0, rateLimiter_1.getAiGenModelRateLimiter)(aiModels_1.EVideoGenModels.KLING_AVATAR);
203
+ const limiter = (0, rateLimiter_1.getAiGenModelRateLimiter)(modelKey);
106
204
  await limiter.waitUntilAvailable();
107
205
  let heldSlot = true;
108
206
  try {
109
- const provider = (0, aiGenFactory_service_1.getAiGenProviderService)(aiModels_1.EVideoGenModels.KLING_AVATAR);
110
- // No `type` kling-avatar's modelConfig.type is ["avatar-video"], which is
111
- // outside the VideoGenerationParams.type union. Setting type:"image-to-video"
112
- // here trips validateParams (image-to-video branch requires the model's
113
- // config.type to include "image-to-video", which avatar's doesn't). The
114
- // kling provider routes by modelKey === "kling-avatar" before any
115
- // type-based dispatch, so omitting type is correct.
116
- const result = await provider.generateVideo({
117
- modelKey: aiModels_1.EVideoGenModels.KLING_AVATAR,
207
+ // byo-media avatar request: a face still + the audio we just resolved. The
208
+ // avatar provider owns endpoint/type dispatch internally (Kling routes on
209
+ // modelKey === "kling-avatar"), so no generic-video `type` is passed here.
210
+ // inputImageUrl presence is guaranteed by the byo-media superRefine branch.
211
+ const provider = (0, avatarProviderFactory_1.getAvatarVideoProvider)(modelKey);
212
+ const result = await provider.generate({
213
+ mode: "byo-media",
214
+ faceImageUrl: input.inputImageUrl,
215
+ audioUrl,
118
216
  prompt: input.prompt,
119
- inputImageUrl: input.inputImageUrl,
120
- inputAudioUrl: audioUrl,
121
- mode: input.mode === "pro" ? types_1.EVideoMode.PROFESSIONAL : types_1.EVideoMode.STANDARD,
217
+ renderMode,
122
218
  });
123
- const taskId = result.task?.taskId ??
124
- result.task?.id ??
125
- (typeof result.task === "string" ? result.task : "unknown");
219
+ const taskId = result.providerTaskId?.taskId ??
220
+ result.providerTaskId?.id ??
221
+ (typeof result.providerTaskId === "string" ? result.providerTaskId : "unknown");
126
222
  if (taskId === "unknown") {
127
223
  limiter.releaseSlot();
128
224
  heldSlot = false;
@@ -332,12 +332,12 @@ declare const OutputSchema: z.ZodObject<{
332
332
  estimatedDurationMs: z.ZodOptional<z.ZodNumber>;
333
333
  sceneIndices: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
334
334
  }, z.core.$strip>>>;
335
- script: z.ZodOptional<z.ZodObject<{
335
+ script: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodObject<{
336
336
  fullText: z.ZodString;
337
337
  wordCount: z.ZodOptional<z.ZodNumber>;
338
338
  estimatedDurationMs: z.ZodOptional<z.ZodNumber>;
339
339
  wpm: z.ZodOptional<z.ZodNumber>;
340
- }, z.core.$strip>>;
340
+ }, z.core.$strip>>>;
341
341
  music: z.ZodOptional<z.ZodObject<{
342
342
  strategy: z.ZodEnum<{
343
343
  none: "none";
@@ -1,7 +1,7 @@
1
1
  import { BaseAiGenProviderService } from "./providers/baseAiGenProvider.service";
2
2
  import { EImageGenModelKey, EMusicGenModels, ESfxGenModels, ETextGenModels, EVideoGenModels, TAiGenModel } from "../../globals/aiModels";
3
3
  type AiGenServiceConstructor = new () => BaseAiGenProviderService;
4
- export declare const videoServicesMap: Record<EVideoGenModels, AiGenServiceConstructor>;
4
+ export declare const videoServicesMap: Partial<Record<EVideoGenModels, AiGenServiceConstructor>>;
5
5
  /**
6
6
  * Image-gen factory. Each provider service implements `generateImage()` (sync;
7
7
  * returns a URL or base64 in a single API roundtrip — no BullMQ worker needed).
@@ -1 +1 @@
1
- {"version":3,"file":"aiGenFactory.service.d.ts","sourceRoot":"","sources":["../../../src/services/aiGen/aiGenFactory.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EACL,iBAAiB,EAEjB,eAAe,EACf,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAchC,KAAK,uBAAuB,GAAG,UAAU,wBAAwB,CAAC;AAElE,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,eAAe,EACf,uBAAuB,CA+DxB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,iBAAiB,EACjB,uBAAuB,CA2CxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,eAAe,EACf,uBAAuB,CAMxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CACjC,aAAa,EACb,uBAAuB,CAGxB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,cAAc,EAAE,uBAAuB,CAsBzE,CAAC;AAEJ,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,GACpB,wBAAwB,CAkB1B"}
1
+ {"version":3,"file":"aiGenFactory.service.d.ts","sourceRoot":"","sources":["../../../src/services/aiGen/aiGenFactory.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EACL,iBAAiB,EAEjB,eAAe,EACf,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAchC,KAAK,uBAAuB,GAAG,UAAU,wBAAwB,CAAC;AASlE,eAAO,MAAM,gBAAgB,EAAE,OAAO,CACpC,MAAM,CAAC,eAAe,EAAE,uBAAuB,CAAC,CA+DjD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,iBAAiB,EACjB,uBAAuB,CA2CxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,eAAe,EACf,uBAAuB,CAMxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CACjC,aAAa,EACb,uBAAuB,CAGxB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,cAAc,EAAE,uBAAuB,CAsBzE,CAAC;AAEJ,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,GACpB,wBAAwB,CAmB1B"}
@@ -14,6 +14,13 @@ const pixverse_service_1 = require("./providers/pixverse/pixverse.service");
14
14
  const bytedance_service_1 = require("./providers/bytedance/bytedance.service");
15
15
  const elevenlabs_service_1 = require("./providers/elevenlabs/elevenlabs.service");
16
16
  const anthropic_service_1 = require("./providers/anthropic/anthropic.service");
17
+ // Partial, intentionally: hosted avatar engines (heygen-avatar, tavus-avatar)
18
+ // are NOT generic video models — they're handled by the dedicated avatar seam
19
+ // (getAvatarVideoProvider / IAvatarVideoProvider). getAiGenProviderService gates
20
+ // on `modelKey in videoServicesMap` and throws for anything absent, so these
21
+ // keys correctly resolve through the avatar factory instead. (kling-avatar stays
22
+ // here because KlingService still backs both its generic-credit path and the
23
+ // KlingAvatarProvider adapter.)
17
24
  exports.videoServicesMap = {
18
25
  // --- Pixverse ---
19
26
  "pixverse-v6": pixverse_service_1.PixVerseService,
@@ -157,8 +164,9 @@ exports.textServicesMap = {
157
164
  // "openai-gpt-3.5-turbo": OpenaiService,
158
165
  };
159
166
  function getAiGenProviderService(modelKey) {
160
- if (modelKey in exports.videoServicesMap) {
161
- return new exports.videoServicesMap[modelKey]();
167
+ const VideoService = exports.videoServicesMap[modelKey];
168
+ if (VideoService) {
169
+ return new VideoService();
162
170
  }
163
171
  if (modelKey in exports.imageServicesMap) {
164
172
  return new exports.imageServicesMap[modelKey]();
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/services/aiGen/helpers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGhE;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,EAAE,CAEvD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAG3E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAqJrE"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/services/aiGen/helpers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGhE;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,EAAE,CAEvD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAG3E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAiKrE"}
@@ -5,6 +5,7 @@ exports.getModelsSupportingLastFrame = getModelsSupportingLastFrame;
5
5
  exports.modelDeclaresField = modelDeclaresField;
6
6
  exports.validateParams = validateParams;
7
7
  const aiModels_1 = require("../../globals/aiModels");
8
+ const errors_1 = require("../../utils/errors");
8
9
  /**
9
10
  * Whether a model supports first-last-frame (FLF) interpolation, i.e. accepts a
10
11
  * `lastFrameImageUrl`. The source of truth is the model config: a model declares
@@ -125,9 +126,18 @@ function validateParams(params) {
125
126
  }
126
127
  }
127
128
  }
128
- // 4. Fail fast if validation fails
129
+ // 4. Fail fast if validation fails.
130
+ // These are DETERMINISTIC input/capability mismatches (a required input the
131
+ // model needs is absent, an unsupported value/combo, etc.) — an expected
132
+ // user-input outcome, NOT a server fault. Throw a typed UserFacingError with
133
+ // CAPABILITY_MISMATCH so it (a) short-circuits the scene retry loop, (b) stays
134
+ // OFF the pager (errorPaging non-paging set) instead of alerting on-call with
135
+ // a raw "INTERNAL Validation failed" — e.g. a kling-motion-control job
136
+ // submitted without its required inputVideoUrl (2026-07-09). The job surfaces
137
+ // the friendly CAPABILITY_MISMATCH message; the technical detail stays on the
138
+ // scene for debugging.
129
139
  if (errors.length) {
130
- throw new Error(`INTERNAL Validation failed: ${errors.join("; ")}`);
140
+ throw new errors_1.UserFacingError(`Validation failed: ${errors.join("; ")}`, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
131
141
  }
132
142
  return true;
133
143
  }
@@ -1 +1 @@
1
- {"version":3,"file":"kling.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/kling/kling.service.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAsBlB;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,wBAAwB;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAW;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IASxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkD;IAC9E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmD;;YAWlE,OAAO;IAgCf,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAoDjC;;;;OAIG;YACW,WAAW;YA6BX,iBAAiB;YA6BjB,qBAAqB;YAgCrB,eAAe;YAkDf,mBAAmB;YAgCnB,uBAAuB;IAwF/B,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAW7E,cAAc,IAAI,MAAM;IAexB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAGjD"}
1
+ {"version":3,"file":"kling.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/kling/kling.service.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAsBlB;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,wBAAwB;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAW;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IASxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkD;IAC9E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmD;;YAWlE,OAAO;IAgCf,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAoDjC;;;;OAIG;YACW,WAAW;YA6BX,iBAAiB;YA6BjB,qBAAqB;YAgCrB,eAAe;YAkDf,mBAAmB;YAgCnB,uBAAuB;IAqI/B,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAW7E,cAAc,IAAI,MAAM;IAexB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAGjD"}
@@ -219,25 +219,64 @@ class KlingService extends baseAiGenProvider_service_1.BaseAiGenProviderService
219
219
  // True rate-limit (HTTP 429 without the balance code) — worth retrying with backoff.
220
220
  const isRateLimit = httpStatus === 429 && !isInsufficientBalance;
221
221
  const isRetryable = (isTimeout || isNetworkError || isRateLimit) && !isInsufficientBalance;
222
- // Insufficient balance is a critical ops issue surface immediately to Slack.
222
+ // Insufficient balance is a billing/account fault an operator must act on
223
+ // (top up Kling). Classify as ACCOUNT_QUOTA_EXCEEDED so the shared paging
224
+ // policy (errorPaging.ts) pages ONCE at the scene boundary — consistent
225
+ // with the Veo billing-depletion path. The top-up detail is logged at
226
+ // warn (Loki only, no duplicate Slack page) so it stays searchable
227
+ // without the previous double-page (raw throw here + generic scene page).
223
228
  if (isInsufficientBalance) {
224
- logger_1.logger.error("Kling account balance too low — top up required to continue video generation", {
229
+ logger_1.logger.warn("Kling account balance too low — top up required to continue video generation", {
225
230
  endpoint,
226
231
  klingCode,
227
232
  klingMessage: err.response?.data?.message,
228
233
  httpStatus,
229
234
  });
230
- throw err;
235
+ throw new errors_1.UserFacingError("This video provider is temporarily unavailable. Please try a different model or retry shortly.", errors_1.USER_FACING_ERROR_CODES.ACCOUNT_QUOTA_EXCEEDED);
231
236
  }
232
- // Audio rejected by Kling pick the user message based on which 1201 variant it is.
237
+ // Kling code 1201 is a broad "input rejected" family a user-fixable
238
+ // input constraint, not a system fault. It fires for AUDIO (avatar /
239
+ // lip-sync `sound_file`) AND for IMAGE inputs (e.g. an oversized
240
+ // end-frame `image_tail` on a plain image2video — see the kling-v3.0
241
+ // end-frame case, 2026-07-09). So only blame audio when audio was
242
+ // actually part of the request; otherwise the message misleads the
243
+ // user (and whoever debugs the log) into hunting a non-existent audio
244
+ // clip. CAPABILITY_MISMATCH keeps it off the pager (errorPaging set).
233
245
  if (isAudioError) {
234
246
  const klingMsg = (err.response?.data?.message ?? "").toLowerCase();
235
- const userMessage = klingMsg.includes("size")
236
- ? "Your audio file is too large for this avatar generation. Please use a shorter audio clip and try again."
237
- : "Your audio format is not supported. Please upload an MP3 or WAV file and try again.";
238
- throw new errors_1.UserFacingError(userMessage);
247
+ const isSize = klingMsg.includes("size");
248
+ const hasAudioInput = endpoint.includes("avatar") ||
249
+ endpoint.includes("lip") ||
250
+ !!(body && typeof body === "object" && body.sound_file);
251
+ let userMessage;
252
+ if (hasAudioInput) {
253
+ userMessage = isSize
254
+ ? "Your audio clip is too large for this video. Please use a shorter audio clip and try again."
255
+ : "Your audio format is not supported. Please upload an MP3 or WAV file and try again.";
256
+ }
257
+ else {
258
+ userMessage = isSize
259
+ ? "One of your input images is too large for this model. Please use a smaller image (or remove the end-frame) and try again."
260
+ : "One of your inputs was rejected by this model. Please adjust your image or settings and try again.";
261
+ }
262
+ throw new errors_1.UserFacingError(userMessage, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
239
263
  }
240
264
  if (!isRetryable || attempt === maxAttempts) {
265
+ // A rate-limit that exhausted its retries is an expected, un-actionable
266
+ // condition (Kling throughput saturation) — surface it as a non-paging
267
+ // UserFacingError rather than a raw axios 429, so it doesn't page
268
+ // on-call. (Genuine 1102 balance depletion is handled above and DOES
269
+ // page; ambiguous bare 429s land here and read as rate-limit, which is
270
+ // the safe default — rate limits shouldn't page.)
271
+ if (isRateLimit) {
272
+ logger_1.logger.warn("Kling rate-limited — retries exhausted", {
273
+ endpoint,
274
+ attempt,
275
+ maxAttempts,
276
+ httpStatus,
277
+ });
278
+ throw new errors_1.UserFacingError("This video provider is busy right now. Please retry in a little while.", errors_1.USER_FACING_ERROR_CODES.VIDEO_PROVIDER_RATE_LIMITED);
279
+ }
241
280
  logger_1.logger.error("Kling API request failed (non-retryable or max attempts reached)", {
242
281
  endpoint,
243
282
  attempt,
@@ -2,6 +2,14 @@ import { BaseAiGenProviderService } from "../baseAiGenProvider.service";
2
2
  import { CreditUsageParams, VideoGenerationParams, VideoGenerationResult, VideoStatusParams, VideoStatusResult } from "../types";
3
3
  export declare class PixVerseService extends BaseAiGenProviderService {
4
4
  private readonly baseUrl;
5
+ /**
6
+ * Uploads a source image to PixVerse and returns the integer `img_id` that
7
+ * /img/generate requires. The Upload-Image endpoint accepts either a binary
8
+ * `image` file or an `image_url` string (multipart/form-data) — we pass the
9
+ * URL we already have. Note: NO Content-Type header here; fetch sets the
10
+ * multipart boundary itself when the body is a FormData.
11
+ */
12
+ private uploadImage;
5
13
  generateVideo(params: VideoGenerationParams): Promise<VideoGenerationResult>;
6
14
  checkVideoStatus({ task, outputFilename, outputFilePath, }: VideoStatusParams): Promise<VideoStatusResult>;
7
15
  getCreditUsed(params: CreditUsageParams): number;
@@ -1 +1 @@
1
- {"version":3,"file":"pixverse.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/pixverse/pixverse.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA0FlB,qBAAa,eAAgB,SAAQ,wBAAwB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkD;IAKpE,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyK3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0FjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAkDjD"}
1
+ {"version":3,"file":"pixverse.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/pixverse/pixverse.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA0FlB,qBAAa,eAAgB,SAAQ,wBAAwB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkD;IAK1E;;;;;;OAMG;YACW,WAAW;IA6CnB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAgL3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0FjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAkDjD"}
@@ -82,6 +82,52 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
82
82
  this.baseUrl = "https://app-api.pixverse.ai/openapi/v2/video";
83
83
  }
84
84
  // =========================================================
85
+ // UPLOAD IMAGE (image-to-video prerequisite)
86
+ // =========================================================
87
+ /**
88
+ * Uploads a source image to PixVerse and returns the integer `img_id` that
89
+ * /img/generate requires. The Upload-Image endpoint accepts either a binary
90
+ * `image` file or an `image_url` string (multipart/form-data) — we pass the
91
+ * URL we already have. Note: NO Content-Type header here; fetch sets the
92
+ * multipart boundary itself when the body is a FormData.
93
+ */
94
+ async uploadImage(imageUrl, traceId) {
95
+ const form = new FormData();
96
+ form.append("image_url", imageUrl);
97
+ return (0, transientRetry_1.withTransientRetry)("pixverse:uploadImage", async () => {
98
+ const resp = await fetch(`https://app-api.pixverse.ai/openapi/v2/image/upload`, {
99
+ method: "POST",
100
+ headers: {
101
+ "API-KEY": process.env.PIXVERSE_API_KEY,
102
+ "Ai-trace-id": traceId,
103
+ },
104
+ body: form,
105
+ });
106
+ if (!resp.ok) {
107
+ const errText = await resp.text();
108
+ throw classifyPixVerseHttpError(resp.status, errText, "uploadImage");
109
+ }
110
+ const data = await resp.json();
111
+ if (data.ErrCode !== undefined && data.ErrCode !== 0) {
112
+ throw classifyPixVerseApiError(data.ErrCode, data.ErrMsg ?? "", "uploadImage");
113
+ }
114
+ const imgId = data?.Resp?.img_id;
115
+ if (imgId === undefined || imgId === null) {
116
+ throw new Error("PixVerse image upload did not return img_id");
117
+ }
118
+ return Number(imgId);
119
+ }, {
120
+ onRetry: ({ attempt, maxAttempts, backoffMs, err }) => {
121
+ logger_1.logger.warn(`PixVerse uploadImage transient error — retrying`, {
122
+ attempt,
123
+ maxAttempts,
124
+ backoffMs,
125
+ message: err.message,
126
+ });
127
+ },
128
+ });
129
+ }
130
+ // =========================================================
85
131
  // GENERATE VIDEO
86
132
  // =========================================================
87
133
  async generateVideo(params) {
@@ -206,10 +252,17 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
206
252
  if (params.lipSync.ttsSpeakerId)
207
253
  body.lip_sync_tts_speaker_id = params.lipSync.ttsSpeakerId;
208
254
  }
209
- // I2V switches endpoint and adds the image reference.
255
+ // I2V switches endpoint and adds the image reference. The /img/generate
256
+ // endpoint does NOT accept a raw image_url — it requires an integer
257
+ // `img_id` from the Upload-Image API (verified 2026-06-30 against
258
+ // https://docs.platform.pixverse.ai/image-to-video-generation-13016633e0).
259
+ // Sending image_url instead surfaced in prod as ErrCode 400017 "Either
260
+ // img_id or api_gen_img_ids must be provided." So we upload first, then
261
+ // pass the returned img_id. (Distinct from the /template/generate "effect"
262
+ // path above, which DOES take a customer_img_url directly.)
210
263
  const isImageToVideo = !!params.inputImageUrl;
211
264
  if (isImageToVideo) {
212
- body.image_url = params.inputImageUrl;
265
+ body.img_id = await this.uploadImage(params.inputImageUrl, traceId);
213
266
  }
214
267
  // I2V path is `/img/generate` — NOT `/image/generate`, which returns a bare
215
268
  // "404 page not found" (verified 2026-06-15 via PixVerse OpenAPI smoke test