@sogni-ai/sogni-creative-agent-skill 3.3.0 โ†’ 3.3.2

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/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: sogni-creative-agent-skill
3
3
  description: "Sogni Creative Agent Skill: agent skill and CLI for image, video, and music generation using Sogni AI's decentralized GPU network. Supports personas (named people with saved reference photos and voice clips), persistent memories (user preferences across sessions), custom personality, style transfer, angle synthesis, and multi-step creative workflows. Ask the agent to \"draw\", \"generate\", \"create an image\", \"make a video/animate\", \"make music\", \"apply a style\", or \"generate me as a superhero\"."
4
4
  metadata:
5
- version: "3.1.1"
5
+ version: "3.3.2"
6
6
  homepage: https://sogni.ai
7
7
  clawdbot:
8
8
  emoji: "๐ŸŽจ"
@@ -314,12 +314,12 @@ const GATING_POLICIES = [
314
314
  "trigger": {
315
315
  "allOf": [
316
316
  "has_active_persona",
317
- "requests_video_generation",
317
+ "requests_persona_video_generation",
318
318
  "no_persona_image_in_session"
319
319
  ],
320
320
  "sources": {
321
321
  "has_active_persona": "session_state",
322
- "requests_video_generation": "planner",
322
+ "requests_persona_video_generation": "planner",
323
323
  "no_persona_image_in_session": "session_state"
324
324
  }
325
325
  },
@@ -2184,7 +2184,7 @@ const PROMPT_CONTRACTS = [
2184
2184
  "contractId": "generate_video_v1",
2185
2185
  "version": "1.1.0",
2186
2186
  "toolName": "generate_video",
2187
- "baseDescription": "generate_video produces text-to-video clips and Seedance multimodal reference videos.\nUse for text-only video generation with no source image input. For Seedance, also use this\ntool when uploaded/generated images, videos, or audio are loose references. Use animate_photo\nonly when a non-Seedance source image must become the first frame of an LTX/WAN animation.\n\nSEEDANCE UPLOADED STORYBOARD DEFAULT: When the user uploads a storyboard, shot sheet,\nmood board, or trailer concept image and asks to make a movie trailer/video/clip from it,\ndefault to one Seedance generate_video call with referenceImageIndices=[-1]. Do not first\nextract panels with edit_image, do not generate replacement keyframes, and do not make four\nseparate LTX animate_photo clips unless the user explicitly asks for separate clips or LTX.\nUse seedance2 when premium Spark access is available; if premium access is unavailable,\nexplain the limitation or use the best non-Seedance fallback the user accepts.\n\nSTORYTELLING / COMMERCIAL / TRAILER PROMPTS: For creative video requests, turn the brief\ninto timed, causally connected visual beats before writing the final prompt. Default social\nvideo is 15s 9:16 with a strong first 1-2s, visible escalation, payoff, and brand/CTA/final\nimage. Commercials should show audience desire/problem, transformation, proof/benefit, and\nCTA. Trailers should follow hook โ†’ world โ†’ disruption โ†’ escalation โ†’ reveal โ†’ title/CTA.\nEvery beat must be generatable: subject, setting, action, camera, lighting, audio, and text\nrole where relevant. Avoid vague \"cinematic\" filler, feature dumps, and beautiful images with\nno visible change.\n\nVIDEO PROMPT QUOTING: ONLY use double quotes for spoken dialogue in video prompts. Never\nquote on-screen text, titles, captions, or visual text elements โ€” describe them without\nquotes. Quotes signal speech to the model and confuse audio generation.\n\nSTORYBOARD TEXT: Structural headings, section numbers, slide titles, panel titles, and\ncaptions in storyboard references may become short audio-only narration/VO or\nkey-message beats, but they are not subtitles, title cards, lower thirds, or visible\noverlays unless the user explicitly asks for visible text, on-screen text, a title\ncard, subtitle, lower third, signage, or CTA. Keep narration as separate brief phrases\nwith pauses; do not concatenate storyboard labels into run-on voiceover.\n\nDIALOGUE DURATION: Spoken dialogue must fit the clip. Estimate 2.5 words per second\nnatural delivery plus ~1s per acting beat. Hard maximum 3.75 words/second.\nCheck: dialogue words รท 2.5 + beats โ‰ค duration. Do not submit oversized dialogue.\n\nLATEST USER DURATION WINS: In follow-up turns, use the newest duration the user states,\neven if a previous assistant message mentioned a longer script/runtime. For example, if\nhistory says \"the full script is 66 seconds\" but the user now says \"do a 30 second version\",\ngenerate the 30 second version. Do not ask a clarification question just because history\ncontains another duration; treat the latest user request as the override.\n\nSEEDANCE SHORT-DURATION LIMIT: Seedance supports 4-15s clips. If the user explicitly asks\nfor Seedance below 4s, do not silently round up. Ask whether they prefer a 4s Seedance clip\nor an exact-duration LTX clip. If the user did not explicitly ask for Seedance, choose the\nmodel/tool that can satisfy the requested duration exactly.",
2187
+ "baseDescription": "generate_video produces text-to-video clips and Seedance multimodal reference videos.\nUse for text-only video generation with no source image input. For Seedance, also use this\ntool when uploaded/generated images, videos, or audio are loose references. Use animate_photo\nonly when a non-Seedance source image must become the first frame of an LTX/WAN animation.\n\nSEEDANCE UPLOADED STORYBOARD DEFAULT: When the user uploads a storyboard, shot sheet,\nmood board, or trailer concept image and asks to make a movie trailer/video/clip from it,\ndefault to one Seedance generate_video call with referenceImageIndices=[-1]. Do not first\nextract panels with edit_image, do not generate replacement keyframes, and do not make four\nseparate LTX animate_photo clips unless the user explicitly asks for separate clips or LTX.\nUse seedance2 when premium Spark access is available; if premium access is unavailable,\nexplain the limitation or use the best non-Seedance fallback the user accepts.\n\nEXACT / INCLUDED VIDEO PROMPTS: If the user asks for a Seedance video using uploaded or\ngenerated references and says to use a prompt exactly, pass only that literal quoted prompt\nto generate_video and set skipPromptProcessing=true plus expandPrompt=false. Do not treat\nwords inside the literal prompt, such as storyboard, script, thumbnails, or panels, as a\nrequest to create a storyboard image. If the user includes a timecoded script inside a\nvideo request, keep it in the generate_video prompt. Explicit constraints like no storyboard\npanels, no subtitles, or no captions are constraints on the video render, not instructions\nto call edit_image or generate_image.\n\nSTORYTELLING / COMMERCIAL / TRAILER PROMPTS: For creative video requests, turn the brief\ninto timed, causally connected visual beats before writing the final prompt. Default social\nvideo is 15s 9:16 with a strong first 1-2s, visible escalation, payoff, and brand/CTA/final\nimage. Commercials should show audience desire/problem, transformation, proof/benefit, and\nCTA. Trailers should follow hook โ†’ world โ†’ disruption โ†’ escalation โ†’ reveal โ†’ title/CTA.\nEvery beat must be generatable: subject, setting, action, camera, lighting, audio, and text\nrole where relevant. Avoid vague \"cinematic\" filler, feature dumps, and beautiful images with\nno visible change.\n\nVIDEO PROMPT QUOTING: ONLY use double quotes for spoken dialogue in video prompts. Never\nquote on-screen text, titles, captions, or visual text elements โ€” describe them without\nquotes. Quotes signal speech to the model and confuse audio generation.\n\nSTORYBOARD TEXT: Structural headings, section numbers, slide titles, panel titles, and\ncaptions in storyboard references may become short audio-only narration/VO or\nkey-message beats, but they are not subtitles, title cards, lower thirds, or visible\noverlays unless the user explicitly asks for visible text, on-screen text, a title\ncard, subtitle, lower third, signage, or CTA. Keep narration as separate brief phrases\nwith pauses; do not concatenate storyboard labels into run-on voiceover.\n\nDIALOGUE DURATION: Spoken dialogue must fit the clip. Estimate 2.5 words per second\nnatural delivery plus ~1s per acting beat. Hard maximum 3.75 words/second.\nCheck: dialogue words รท 2.5 + beats โ‰ค duration. Do not submit oversized dialogue.\n\nLATEST USER DURATION WINS: In follow-up turns, use the newest duration the user states,\neven if a previous assistant message mentioned a longer script/runtime. For example, if\nhistory says \"the full script is 66 seconds\" but the user now says \"do a 30 second version\",\ngenerate the 30 second version. Do not ask a clarification question just because history\ncontains another duration; treat the latest user request as the override.\n\nSEEDANCE SHORT-DURATION LIMIT: Seedance supports 4-15s clips. If the user explicitly asks\nfor Seedance below 4s, do not silently round up. Ask whether they prefer a 4s Seedance clip\nor an exact-duration LTX clip. If the user did not explicitly ask for Seedance, choose the\nmodel/tool that can satisfy the requested duration exactly.",
2188
2188
  "parameterDocs": {
2189
2189
  "prompt": "Video prompt. Use double quotes ONLY for spoken dialogue. Describe visual text without quotes.",
2190
2190
  "duration": "Clip duration in seconds. Plan dialogue word count against the 3.75 words/second ceiling."
@@ -2216,7 +2216,7 @@ const PROMPT_CONTRACTS = [
2216
2216
  "contractId": "video_to_video_v1",
2217
2217
  "version": "1.0.0",
2218
2218
  "toolName": "video_to_video",
2219
- "baseDescription": "video_to_video transforms an uploaded video. Use for uploaded-video restyling, enhancement,\nupscaling/remastering, motion transfer from video to image, subject replacement, edge/pose/\ndepth-guided restyle, or explicit Seedance V2V transforms.\n\nThis tool requires an uploaded video source. Do not use it for generated video indices. For\ngenerated or uploaded partial edits use replace_video_segment; for appended time use\nextend_video; for logos/text overlays use overlay_video; for stitching use stitch_video.\n\nChoose controlMode by intent. Use detailer for quality-only enhancement without restyling.\nUse seedance-v2v only when the user asks to transform/enhance/remaster an uploaded video\nwith Seedance. For detailer, describe the original scene plus quality terms, not new content.",
2219
+ "baseDescription": "video_to_video transforms an uploaded video. Use for uploaded-video restyling, enhancement,\nupscaling/remastering, motion transfer from video to image, subject replacement, edge/pose/\ndepth-guided restyle, or explicit Seedance V2V transforms.\n\nThis tool requires an uploaded video source. Do not use it for generated video indices. For\ngenerated or uploaded partial edits use replace_video_segment; for appended time use\nextend_video; for logos/text overlays use overlay_video; for stitching use stitch_video.\n\nChoose controlMode by intent. Use detailer for quality-only enhancement without restyling.\nUse seedance-v2v only when the user asks to transform/enhance/remaster an uploaded video\nwith Seedance, including Seedance-fast uploaded-video upscale/remaster requests. For detailer,\ndescribe the original scene plus quality terms, not new content.",
2220
2220
  "parameterDocs": {
2221
2221
  "prompt": "Describe the target appearance in present tense. For detailer, describe the original content plus quality qualifiers only.",
2222
2222
  "videoSourceIndex": "Uploaded video index. Omit when there is one uploaded video; use 0 for first uploaded video or -1 if using negative upload notation.",
@@ -2240,7 +2240,7 @@ const PROMPT_CONTRACTS = [
2240
2240
  "contractId": "replace_video_segment_v1",
2241
2241
  "version": "1.0.0",
2242
2242
  "toolName": "replace_video_segment",
2243
- "baseDescription": "Use replace_video_segment when the user wants to regenerate a specific time range of an\nexisting video: \"regenerate from Xs to Ys\", \"redo the last N seconds\", \"swap out the middle\",\n\"fix the [start/middle/end] of the video\", or \"replace the [bumper/intro/outro/end card/\ntag/sting] at the [start/end] of the video\". Use explicit startSeconds and endSeconds; use\n-1 sentinels when exact base duration is unknown โ€” the handler probes and resolves.\n\nWhen the replacement is already another uploaded or generated video clip, still use\nreplace_video_segment but pass replacementVideoIndex. Example: \"splice video 2 into video 1\nat 5s\" means videoIndex=-1, replacementVideoIndex=-2, startSeconds=5, endSeconds=5.\nUse endSeconds=startSeconds for insertion; use a wider endSeconds only when the user says to\nreplace/remove that base-video range. Do not use stitch_video for \"into the middle\"/\"insert\"\nrequests, because stitch_video only concatenates full clips end-to-end.\n\nFor time-sliced interleaving from existing videos โ€” \"alternate 1s from each video\", \"weave\none-second clips from video 1 and video 2\", \"cut back and forth every N seconds\" โ€” do NOT\nuse stitch_video and do NOT omit replacementVideoIndex. Start with the first requested video\nas the base, then call replace_video_segment once for each window that should come from the\nother video. Set replacementVideoIndex to that other existing video and set\nreplacementStartSeconds/replacementEndSeconds to the next source slice from that\nreplacement video. For ordinary\nalternation, preserve the base duration: set endSeconds=startSeconds+sliceDuration, not\nendSeconds=startSeconds insertion, unless the user explicitly asks to lengthen the output by\ninserting extra slices. Skip no-op windows that already come from the base video; only splice\nwindows that should come from a different source. Example for two 10s uploads alternating every 1s starting with video\n1: replace base windows 1..2, 3..4,\n5..6, 7..8, and 9..10 with slices 0..1, 1..2, 2..3, 3..4, and 4..5 from video 2. After\neach successful splice, target the newest composite video index for the next splice.\nThe -1 time sentinel applies only to base startSeconds/endSeconds when the base duration is\nunknown. Never use -1 for replacementStartSeconds or replacementEndSeconds; source windows\nmust use concrete non-negative seconds. For uploaded/generated videos with duration metadata,\nuse that known duration directly; do not call analyze_video just to learn the clip length for\nroutine alternating slices. Do not add a final tail splice with an unknown source end โ€” stop at\nthe known clip duration or skip a no-op tail window.\n\nDo NOT call generate_video or animate_photo to re-render an existing video just to change\npart of it (the bumper, the intro, the end card, a single scene, the last few seconds, etc.).\nUse replace_video_segment โ€” it preserves the unchanged portion, keeps the original audio\noutside the replaced window, and costs far less.\n\nAuto-detects the base video's model, so OMIT videoModel unless the user explicitly demands\na different model. Short requested windows are supported by rendering with model-specific\nhandles and trimming the rendered clip before splicing, so still pass the user's exact\nstartSeconds/endSeconds.",
2243
+ "baseDescription": "Use replace_video_segment when the user wants to regenerate a specific time range of an\nexisting video: \"regenerate from Xs to Ys\", \"redo the last N seconds\", \"swap out the middle\",\n\"fix the [start/middle/end] of the video\", or \"replace the [bumper/intro/outro/end card/\ntag/sting] at the [start/end] of the video\". Use explicit startSeconds and endSeconds.\nFor relative requests like \"last 3 seconds\", resolve against the known base duration when\nduration metadata or prior tool arguments provide it. For \"bumper/end card/outro at the end\"\nwithout exact seconds, use the known storyboard timing when available; otherwise choose a\nsmall end-card window such as the final 1-3 seconds based on the base duration. If the base\nduration/window is genuinely unknown, inspect the video first or ask for the missing window;\ndo not submit ambiguous placeholder times.\n\nWhen the replacement is already another uploaded or generated video clip, still use\nreplace_video_segment but pass replacementVideoIndex. Example: \"splice video 2 into video 1\nat 5s\" means videoIndex=-1, replacementVideoIndex=-2, startSeconds=5, endSeconds=5.\nUse endSeconds=startSeconds for insertion; use a wider endSeconds only when the user says to\nreplace/remove that base-video range. Do not use stitch_video for \"into the middle\"/\"insert\"\nrequests, because stitch_video only concatenates full clips end-to-end.\n\nFor time-sliced interleaving from existing videos โ€” \"alternate 1s from each video\", \"weave\none-second clips from video 1 and video 2\", \"cut back and forth every N seconds\" โ€” do NOT\nuse stitch_video and do NOT omit replacementVideoIndex. Start with the first requested video\nas the base, then call replace_video_segment once for each window that should come from the\nother video. Set replacementVideoIndex to that other existing video and set\nreplacementStartSeconds/replacementEndSeconds to the next source slice from that\nreplacement video. For ordinary\nalternation, preserve the base duration: set endSeconds=startSeconds+sliceDuration, not\nendSeconds=startSeconds insertion, unless the user explicitly asks to lengthen the output by\ninserting extra slices. Skip no-op windows that already come from the base video; only splice\nwindows that should come from a different source. Example for two 10s uploads alternating every 1s starting with video\n1: replace base windows 1..2, 3..4,\n5..6, 7..8, and 9..10 with slices 0..1, 1..2, 2..3, 3..4, and 4..5 from video 2. After\neach successful splice, target the newest composite video index for the next splice.\nThe -1 time sentinel applies only to base startSeconds/endSeconds when the base duration is\nunknown. Never use -1 for replacementStartSeconds or replacementEndSeconds; source windows\nmust use concrete non-negative seconds. For uploaded/generated videos with duration metadata,\nuse that known duration directly; do not call analyze_video just to learn the clip length for\nroutine alternating slices. Do not add a final tail splice with an unknown source end โ€” stop at\nthe known clip duration or skip a no-op tail window.\n\nDo NOT call generate_video or animate_photo to re-render an existing video just to change\npart of it (the bumper, the intro, the end card, a single scene, the last few seconds, etc.).\nUse replace_video_segment โ€” it preserves the unchanged portion, keeps the original audio\noutside the replaced window, and costs far less.\n\nAuto-detects the base video's model, so OMIT videoModel unless the user explicitly demands\na different model. Short requested windows are supported by rendering with model-specific\nhandles and trimming the rendered clip before splicing, so still pass the user's exact\nstartSeconds/endSeconds.",
2244
2244
  "parameterDocs": {
2245
2245
  "startSeconds": "Start of segment to replace in seconds. Use -1 sentinel if exact base duration is unknown.",
2246
2246
  "endSeconds": "End of segment to replace in seconds. Use the same value as startSeconds for insertion with replacementVideoIndex.",
@@ -2433,9 +2433,9 @@ const PROMPT_CONTRACTS = [
2433
2433
  "contractId": "finalize_response_v1",
2434
2434
  "version": "1.1.0",
2435
2435
  "toolName": "finalize_response",
2436
- "baseDescription": "finalize_response marks the turn complete and stops the tool loop. Use after the requested\nworkflow succeeds, partially succeeds, fails with a surfaced error, or needs no tool action.\n\nWhen the user asked for a script, storyboard, ad concept, trailer, creator video, meme/parody,\nor music prompt and no media tool is required, deliver the final creative in a clean Markdown\ncontract: title, concept/objective, audience if relevant, timed beats or script, audio/text\nnotes, generation prompt(s), CTA, and brief assumptions. For revisions, apply the feedback\ndirectly while preserving approved elements and rejected constraints.\n\nDo not call any other tool after finalize_response. Keep the summary short and grounded in\nactual tool results; do not claim exact metadata that no tool returned.",
2436
+ "baseDescription": "finalize_response marks the turn complete and stops the tool loop. Use after the requested\nworkflow succeeds, partially succeeds, fails with a surfaced error, or needs no tool action.\n\nWhen the user asked for a script, storyboard, ad concept, trailer, creator video, meme/parody,\nor music prompt and no media tool is required, deliver the final creative in a clean Markdown\ncontract: title, concept/objective, audience if relevant, timed beats or script, audio/text\nnotes, generation prompt(s), CTA, and brief assumptions. For revisions, apply the feedback\ndirectly while preserving approved elements and rejected constraints.\n\nDo not call any other tool after finalize_response. Keep the summary short and grounded in\nactual tool results; do not claim exact metadata that no tool returned.\nFor no-action/text-only answers, such as product, feature, model, pricing, or capability\nquestions, the summary is the final answer the user sees. Provide the substantive answer\nthere; never leave it empty and never use a placeholder like \"Done.\"",
2437
2437
  "parameterDocs": {
2438
- "summary": "Short user-visible closeout. Mention produced media or the concrete blocker; avoid duplicating prior tool output.",
2438
+ "summary": "User-visible closeout. For no-action/text-only answers, include the complete substantive answer here. For media workflows, mention produced media or the concrete blocker; avoid duplicating prior tool output.",
2439
2439
  "outcome": "success, partial, asked_user, failed, or no_action based on the actual turn outcome."
2440
2440
  }
2441
2441
  },
@@ -2,7 +2,7 @@
2
2
  "id": "sogni-creative-agent-skill",
3
3
  "name": "Sogni Creative Agent Skill โ€” Image, Video & Music Generation",
4
4
  "description": "Agent skill and CLI for Sogni AI image, video, and music generation.",
5
- "version": "3.1.1",
5
+ "version": "3.3.2",
6
6
  "skills": [
7
7
  "."
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sogni-ai/sogni-creative-agent-skill",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Sogni Creative Agent Skill: agent skill and CLI for Sogni AI image, video, and music generation.",
5
5
  "type": "module",
6
6
  "main": "sogni-agent.mjs",
@@ -67,7 +67,7 @@
67
67
  "sogni-agent.mjs"
68
68
  ],
69
69
  "dependencies": {
70
- "@sogni-ai/sogni-intelligence-client": "^2.4.0",
70
+ "@sogni-ai/sogni-intelligence-client": "^2.4.1",
71
71
  "execa": "^9.6.1",
72
72
  "json5": "^2.2.3",
73
73
  "sharp": "^0.34.5"
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@sogni-ai/sogni-intelligence-client": "^2.4.0",
6
+ "@sogni-ai/sogni-intelligence-client": "^2.4.1",
7
7
  "execa": "^9.6.1",
8
8
  "json5": "^2.2.3",
9
9
  "sharp": "^0.34.5"
package/sogni-agent.mjs CHANGED
@@ -66,9 +66,15 @@ import {
66
66
  import {
67
67
  extractToolCallProgressUpdate
68
68
  } from '@sogni-ai/sogni-intelligence-client/chatRun';
69
+ import {
70
+ SEEDANCE_R2V_REFERENCE_AUDIO_MAX_DURATION_SECONDS,
71
+ prepareSeedanceV2VSourceVideo as prepareSharedSeedanceV2VSourceVideo
72
+ } from '@sogni-ai/sogni-intelligence-client/media';
69
73
  import {
70
74
  SEEDANCE_REFERENCE_LIMITS,
71
75
  SeedanceReferenceLimitError,
76
+ seedanceTerminalGenerationFailurePayloadFromError,
77
+ seedanceTerminalPolicyPayloadFromError,
72
78
  validateSeedanceReferenceCounts
73
79
  } from '@sogni-ai/sogni-intelligence-client/tools';
74
80
 
@@ -220,15 +226,19 @@ function isPathWithinBase(basePath, targetPath) {
220
226
  }
221
227
 
222
228
  function buildCliErrorPayload({ message, code, details, hint, prompt }) {
223
- const classified = classifySkillError({ message, code });
229
+ const classified = classifyCliError({ message, code });
224
230
  const payload = {
225
231
  success: false,
226
- error: message || 'Unknown error',
232
+ error: classified.message || message || 'Unknown error',
227
233
  errorType: classified.error_type,
228
234
  errorCategory: classified.category,
229
235
  retryable: classified.retryable,
230
236
  prompt: prompt ?? null
231
237
  };
238
+ if (classified.metadata) payload.metadata = classified.metadata;
239
+ if (classified.technicalError && classified.technicalError !== payload.error) {
240
+ payload.technicalError = classified.technicalError;
241
+ }
232
242
  if (code) payload.errorCode = code;
233
243
  if (details) payload.errorDetails = details;
234
244
  if (hint) payload.hint = hint;
@@ -239,11 +249,71 @@ function buildCliErrorPayload({ message, code, details, hint, prompt }) {
239
249
  return payload;
240
250
  }
241
251
 
252
+ function cliErrorMessage(error) {
253
+ if (typeof error === 'string') return error;
254
+ if (error instanceof Error) return error.message || String(error);
255
+ if (error && typeof error === 'object') {
256
+ const record = error;
257
+ if (typeof record.message === 'string') return record.message;
258
+ if (typeof record.error === 'string') return record.error;
259
+ }
260
+ return String(error ?? 'Unknown error');
261
+ }
262
+
263
+ function seedanceFriendlyGenerationMessage(payload) {
264
+ const raw = [
265
+ payload?.message,
266
+ payload?.vendorError,
267
+ payload?.vendorErrorCode
268
+ ].filter(Boolean).join(' ');
269
+ if (/\baudio\s+format\b[\s\S]{0,120}\b(?:not valid|invalid)\b/i.test(raw)) {
270
+ return 'Seedance rejected the audio reference format for this model. Try a different audio file, trim/convert the clip, or use a non-Seedance audio-driven workflow such as LTX sound-to-video.';
271
+ }
272
+ return payload?.message || 'Seedance could not complete this video.';
273
+ }
274
+
275
+ function classifyCliError(error) {
276
+ const rawMessage = cliErrorMessage(error);
277
+ const seedancePolicyPayload = seedanceTerminalPolicyPayloadFromError(error);
278
+ if (seedancePolicyPayload) {
279
+ return {
280
+ error_type: 'SAFETY_REJECTED',
281
+ category: 'content_refused',
282
+ message: seedancePolicyPayload.message,
283
+ retryable: false,
284
+ metadata: seedancePolicyPayload,
285
+ technicalError: rawMessage
286
+ };
287
+ }
288
+
289
+ const seedanceGenerationPayload = seedanceTerminalGenerationFailurePayloadFromError(error);
290
+ if (seedanceGenerationPayload) {
291
+ const vendorCode = seedanceGenerationPayload.vendorErrorCode;
292
+ const isInvalidParameter = vendorCode === 'InvalidParameter' ||
293
+ seedanceGenerationPayload.error === 'seedance_reference_audio_too_long';
294
+ return {
295
+ error_type: isInvalidParameter ? 'PARAMETER_INVALID' : 'GPU_WORKER_FAILED',
296
+ category: isInvalidParameter ? 'schema_validation' : 'transient_failure',
297
+ message: seedanceFriendlyGenerationMessage(seedanceGenerationPayload),
298
+ retryable: !isInvalidParameter,
299
+ metadata: seedanceGenerationPayload,
300
+ technicalError: rawMessage
301
+ };
302
+ }
303
+
304
+ return classifySkillError(error);
305
+ }
306
+
242
307
  function addCanonicalErrorFields(payload, error) {
243
- const classified = classifySkillError(error);
308
+ const classified = classifyCliError(error);
309
+ payload.error = classified.message;
244
310
  payload.errorType = classified.error_type;
245
311
  payload.errorCategory = classified.category;
246
312
  payload.retryable = classified.retryable;
313
+ if (classified.metadata) payload.metadata = classified.metadata;
314
+ if (classified.technicalError && classified.technicalError !== classified.message) {
315
+ payload.technicalError = classified.technicalError;
316
+ }
247
317
  return payload;
248
318
  }
249
319
 
@@ -3653,6 +3723,12 @@ function apiMediaReferenceEndpoint(ref, action) {
3653
3723
  : `/v1/media/${action}Url`;
3654
3724
  }
3655
3725
 
3726
+ function apiMediaReferenceV2Endpoint(ref, action) {
3727
+ return ref.kind === 'image'
3728
+ ? `/v2/image/${action}Url`
3729
+ : `/v2/media/${action}Url`;
3730
+ }
3731
+
3656
3732
  function apiMediaReferenceUrlPath(ref, file, index, action, jobId) {
3657
3733
  const params = new URLSearchParams();
3658
3734
  params.set('type', apiMediaReferenceUploadType(ref, index));
@@ -3666,6 +3742,19 @@ function apiMediaReferenceUrlPath(ref, file, index, action, jobId) {
3666
3742
  return `${apiMediaReferenceEndpoint(ref, action)}?${params.toString()}`;
3667
3743
  }
3668
3744
 
3745
+ function apiMediaReferenceV2UrlPath(ref, file, index, action, jobId) {
3746
+ const params = new URLSearchParams();
3747
+ params.set('type', apiMediaReferenceUploadType(ref, index));
3748
+ params.set('jobId', jobId);
3749
+ params.set('contentType', file.mimeType);
3750
+ if (ref.kind === 'image') {
3751
+ params.set('imageId', `media_ref_${index + 1}`);
3752
+ } else {
3753
+ params.set('id', `media_ref_${index + 1}`);
3754
+ }
3755
+ return `${apiMediaReferenceV2Endpoint(ref, action)}?${params.toString()}`;
3756
+ }
3757
+
3669
3758
  function apiStoredMediaUrl(payload, key) {
3670
3759
  const data = extractApiEnvelopeData(payload);
3671
3760
  const value = data?.[key] || payload?.[key];
@@ -3676,6 +3765,41 @@ function apiStoredMediaUrl(payload, key) {
3676
3765
  throw err;
3677
3766
  }
3678
3767
 
3768
+ function apiStoredMediaUploadPost(payload) {
3769
+ const data = extractApiEnvelopeData(payload);
3770
+ const url = data?.url || data?.uploadUrl;
3771
+ if (typeof url === 'string' && url) {
3772
+ const fields = data?.fields && typeof data.fields === 'object' ? data.fields : {};
3773
+ return { url, fields };
3774
+ }
3775
+ const err = new Error('Sogni API did not return a presigned POST URL for media reference upload.');
3776
+ err.code = 'MEDIA_UPLOAD_FAILED';
3777
+ err.details = { payload };
3778
+ throw err;
3779
+ }
3780
+
3781
+ async function postApiMediaUploadForm(uploadPayload, file) {
3782
+ const { url, fields } = apiStoredMediaUploadPost(uploadPayload);
3783
+ const form = new FormData();
3784
+ for (const [key, value] of Object.entries(fields)) {
3785
+ if (value === undefined || value === null) continue;
3786
+ form.append(key, String(value));
3787
+ }
3788
+ const body = file.buffer || readFileSync(file.filePath);
3789
+ form.append('file', new Blob([body], { type: file.mimeType }), file.filename);
3790
+
3791
+ const response = await fetch(url, {
3792
+ method: 'POST',
3793
+ body: form,
3794
+ });
3795
+ if (!response.ok) {
3796
+ const err = new Error(`Failed to upload ${file.filename} (${response.status} ${response.statusText}).`);
3797
+ err.code = 'MEDIA_UPLOAD_FAILED';
3798
+ err.details = { uploadUrl: url, status: response.status, statusText: response.statusText };
3799
+ throw err;
3800
+ }
3801
+ }
3802
+
3679
3803
  async function putApiMediaUpload(uploadUrl, file) {
3680
3804
  const response = await fetch(uploadUrl, {
3681
3805
  method: 'PUT',
@@ -3766,6 +3890,31 @@ async function uploadPreparedApiMediaReference(ref, index, apiKey, file) {
3766
3890
  };
3767
3891
  }
3768
3892
 
3893
+ async function uploadPreparedApiMediaReferenceV2(ref, index, apiKey, file) {
3894
+ if (!apiKey) {
3895
+ const err = new Error(`${ref.flag} media references require SOGNI_API_KEY so the CLI can upload them before execution.`);
3896
+ err.code = 'MISSING_API_KEY';
3897
+ throw err;
3898
+ }
3899
+ const jobId = `sogni-agent-${Date.now()}-${index + 1}-${randomBytes(4).toString('hex')}`;
3900
+ const uploadPayload = await fetchApiJson(apiMediaReferenceV2UrlPath(ref, file, index, 'upload', jobId), { apiKey });
3901
+ await postApiMediaUploadForm(uploadPayload, file);
3902
+ const downloadPayload = await fetchApiJson(apiMediaReferenceV2UrlPath(ref, file, index, 'download', jobId), { apiKey });
3903
+ const url = apiStoredMediaUrl(downloadPayload, 'downloadUrl');
3904
+ return {
3905
+ url,
3906
+ filename: file.filename,
3907
+ byte_length: file.byteLength,
3908
+ mime_type: file.mimeType,
3909
+ prompt_label: file.filename,
3910
+ storage: {
3911
+ jobId,
3912
+ type: apiMediaReferenceUploadType(ref, index),
3913
+ version: 'v2',
3914
+ },
3915
+ };
3916
+ }
3917
+
3769
3918
  async function uploadLocalApiMediaReference(ref, index, apiKey) {
3770
3919
  return uploadPreparedApiMediaReference(ref, index, apiKey, localApiMediaReferenceFile(ref));
3771
3920
  }
@@ -5419,6 +5568,235 @@ async function prepareReferenceAudioForVideoBuffer(buffer, sourceLabel) {
5419
5568
  return prepared;
5420
5569
  }
5421
5570
 
5571
+ function mediaFilenameFromSource(sourceLabel, fallbackName) {
5572
+ const raw = String(sourceLabel || '');
5573
+ try {
5574
+ if (isHttpUrl(raw)) {
5575
+ const pathname = new URL(raw).pathname;
5576
+ const name = basename(decodeURIComponent(pathname));
5577
+ return name || fallbackName;
5578
+ }
5579
+ } catch {
5580
+ // Fall through to path handling.
5581
+ }
5582
+ const name = basename(raw.split('?')[0]);
5583
+ return name || fallbackName;
5584
+ }
5585
+
5586
+ function withMediaExtension(filename, extension) {
5587
+ const cleanExtension = extension.startsWith('.') ? extension : `.${extension}`;
5588
+ const currentExt = extname(filename);
5589
+ const base = currentExt ? filename.slice(0, -currentExt.length) : filename;
5590
+ return `${base || 'reference'}${cleanExtension}`;
5591
+ }
5592
+
5593
+ async function probeLocalMediaDurationSeconds(pathOrUrl) {
5594
+ if (isHttpUrl(pathOrUrl)) return undefined;
5595
+ const ffprobePath = getEnv('FFPROBE_PATH') || 'ffprobe';
5596
+ sanitizePath(ffprobePath, 'FFPROBE_PATH');
5597
+ const result = await runCommand(ffprobePath, [
5598
+ '-v', 'error',
5599
+ '-show_entries', 'format=duration',
5600
+ '-of', 'default=noprint_wrappers=1:nokey=1',
5601
+ pathOrUrl,
5602
+ ], { captureOutput: true });
5603
+ if (result.error || result.status !== 0) return undefined;
5604
+ const parsed = Number(String(result.stdout || '').trim());
5605
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
5606
+ }
5607
+
5608
+ async function transcodeSeedanceReferenceAudioToMp3(request) {
5609
+ const ffmpegPath = await ensureFfmpegAvailable();
5610
+ const tempDir = mkdtempSync(join(tmpdir(), 'sogni-seedance-audio-'));
5611
+ const inputPath = mediaTempInputPath(tempDir, request.filename, '.audio');
5612
+ const outputPath = join(tempDir, 'reference-audio.mp3');
5613
+ try {
5614
+ writeFileSync(inputPath, Buffer.from(request.data));
5615
+ const result = await runCommand(ffmpegPath, [
5616
+ '-hide_banner',
5617
+ '-loglevel', 'error',
5618
+ '-y',
5619
+ '-i', inputPath,
5620
+ '-vn',
5621
+ '-ac', '2',
5622
+ '-ar', '44100',
5623
+ '-c:a', 'libmp3lame',
5624
+ '-b:a', '128k',
5625
+ outputPath
5626
+ ], { captureOutput: true });
5627
+
5628
+ if (result.error || result.status !== 0 || !isNonEmptyFile(outputPath)) {
5629
+ const err = new Error('Failed to convert Seedance reference audio to MP3.');
5630
+ err.code = 'FFMPEG_SEEDANCE_AUDIO_PREP_FAILED';
5631
+ err.hint = 'Seedance accepts MP3 audio references only. Install ffmpeg with MP3 support or provide an MP3 clip.';
5632
+ err.details = { sourceLabel: request.filename, stderr: result.stderr || '', stdout: result.stdout || '', status: result.status };
5633
+ throw err;
5634
+ }
5635
+
5636
+ return { data: readFileSync(outputPath), mimeType: 'audio/mpeg' };
5637
+ } finally {
5638
+ try { if (existsSync(inputPath)) unlinkSync(inputPath); } catch {}
5639
+ try { if (existsSync(outputPath)) unlinkSync(outputPath); } catch {}
5640
+ try { rmdirSync(tempDir); } catch {}
5641
+ }
5642
+ }
5643
+
5644
+ async function trimSeedanceReferenceAudioToMp3(request) {
5645
+ const ffmpegPath = await ensureFfmpegAvailable();
5646
+ const tempDir = mkdtempSync(join(tmpdir(), 'sogni-seedance-audio-'));
5647
+ const inputPath = mediaTempInputPath(tempDir, request.filename, '.audio');
5648
+ const outputPath = join(tempDir, 'reference-audio.mp3');
5649
+ const start = Math.max(0, Number(request.start) || 0);
5650
+ const duration = Math.max(
5651
+ 0.1,
5652
+ Math.min(15, Number(request.duration) || 15),
5653
+ );
5654
+ try {
5655
+ writeFileSync(inputPath, Buffer.from(request.data));
5656
+ const result = await runCommand(ffmpegPath, [
5657
+ '-hide_banner',
5658
+ '-loglevel', 'error',
5659
+ '-y',
5660
+ '-ss', String(start),
5661
+ '-i', inputPath,
5662
+ '-t', String(duration),
5663
+ '-vn',
5664
+ '-ac', '2',
5665
+ '-ar', '44100',
5666
+ '-c:a', 'libmp3lame',
5667
+ '-b:a', '128k',
5668
+ outputPath
5669
+ ], { captureOutput: true });
5670
+
5671
+ if (result.error || result.status !== 0 || !isNonEmptyFile(outputPath)) {
5672
+ const err = new Error('Failed to trim Seedance reference audio to MP3.');
5673
+ err.code = 'FFMPEG_SEEDANCE_AUDIO_TRIM_FAILED';
5674
+ err.hint = 'Seedance accepts MP3 audio references only and short audio windows. Try a shorter MP3 clip.';
5675
+ err.details = { sourceLabel: request.filename, start, duration, stderr: result.stderr || '', stdout: result.stdout || '', status: result.status };
5676
+ throw err;
5677
+ }
5678
+
5679
+ return { data: readFileSync(outputPath), mimeType: 'audio/mpeg' };
5680
+ } finally {
5681
+ try { if (existsSync(inputPath)) unlinkSync(inputPath); } catch {}
5682
+ try { if (existsSync(outputPath)) unlinkSync(outputPath); } catch {}
5683
+ try { rmdirSync(tempDir); } catch {}
5684
+ }
5685
+ }
5686
+
5687
+ async function trimSeedanceV2VSourceVideo(request) {
5688
+ return {
5689
+ data: await trimSeedanceV2VSourceVideoBuffer(
5690
+ Buffer.from(request.data),
5691
+ request.filename,
5692
+ request.start,
5693
+ request.duration,
5694
+ ),
5695
+ mimeType: 'video/mp4',
5696
+ };
5697
+ }
5698
+
5699
+ function seedanceReferenceAudioWindow() {
5700
+ const requestedDuration = options.audioDuration ?? options.duration;
5701
+ const maxDurationSeconds = Math.min(
5702
+ Number.isFinite(Number(requestedDuration)) && Number(requestedDuration) > 0
5703
+ ? Number(requestedDuration)
5704
+ : SEEDANCE_R2V_REFERENCE_AUDIO_MAX_DURATION_SECONDS,
5705
+ 15,
5706
+ );
5707
+ return {
5708
+ maxDurationSeconds,
5709
+ startOffsetSeconds: options.audioStart ?? 0,
5710
+ };
5711
+ }
5712
+
5713
+ async function prepareSeedanceReferenceAudioUploadFile(pathOrUrl, buffer) {
5714
+ const filename = mediaFilenameFromSource(pathOrUrl, 'reference-audio');
5715
+ const rawMimeType = mimeTypeForPath(pathOrUrl, 'application/octet-stream');
5716
+ const mimeType = normalizeReferenceAudioMimeType(rawMimeType) || rawMimeType;
5717
+ const sourceFormat = detectReferenceAudioFormat(buffer, mimeType);
5718
+ const sourceDurationSeconds = await probeLocalMediaDurationSeconds(pathOrUrl);
5719
+ const window = seedanceReferenceAudioWindow();
5720
+ const shouldTrim =
5721
+ window.startOffsetSeconds > 0 ||
5722
+ (Number.isFinite(sourceDurationSeconds) && sourceDurationSeconds > window.maxDurationSeconds);
5723
+ let prepared = { data: buffer, mimeType: 'audio/mpeg' };
5724
+ let action = null;
5725
+ if (shouldTrim) {
5726
+ prepared = await trimSeedanceReferenceAudioToMp3({
5727
+ data: buffer,
5728
+ filename,
5729
+ inputMimeType: mimeType,
5730
+ sourceFormat,
5731
+ duration: window.maxDurationSeconds,
5732
+ start: window.startOffsetSeconds,
5733
+ });
5734
+ action = 'trimmed and converted';
5735
+ } else if (sourceFormat !== 'mp3') {
5736
+ prepared = await transcodeSeedanceReferenceAudioToMp3({
5737
+ data: buffer,
5738
+ filename,
5739
+ inputMimeType: mimeType,
5740
+ sourceFormat,
5741
+ });
5742
+ action = 'converted';
5743
+ }
5744
+ if (!options.quiet && action) {
5745
+ console.error(`Prepared Seedance reference audio as ${action} MP3 before upload.`);
5746
+ }
5747
+ const data = Buffer.from(prepared.data);
5748
+ return {
5749
+ buffer: data,
5750
+ filename: withMediaExtension(filename, 'mp3'),
5751
+ byteLength: data.length,
5752
+ mimeType: 'audio/mpeg',
5753
+ };
5754
+ }
5755
+
5756
+ async function prepareSeedanceReferenceVideoUploadFile(pathOrUrl, buffer) {
5757
+ const filename = mediaFilenameFromSource(pathOrUrl, 'reference-video.mp4');
5758
+ const rawMimeType = mimeTypeForPath(pathOrUrl, 'video/mp4');
5759
+ const sourceDurationSeconds = await probeLocalMediaDurationSeconds(pathOrUrl);
5760
+ const requestedDuration = Number.isFinite(Number(options.duration))
5761
+ ? Number(options.duration)
5762
+ : SEEDANCE_V2V_REFERENCE_MAX_DURATION_SECONDS;
5763
+ const prepared = await prepareSharedSeedanceV2VSourceVideo(
5764
+ buffer,
5765
+ rawMimeType,
5766
+ filename,
5767
+ sourceDurationSeconds,
5768
+ requestedDuration,
5769
+ options.videoStart ?? 0,
5770
+ { trimVideo: trimSeedanceV2VSourceVideo },
5771
+ );
5772
+ if (!options.quiet && prepared.trimmed) {
5773
+ console.error('Prepared Seedance V2V reference video clip before upload.');
5774
+ }
5775
+ const data = Buffer.from(prepared.data);
5776
+ return {
5777
+ buffer: data,
5778
+ filename: withMediaExtension(filename, 'mp4'),
5779
+ byteLength: data.length,
5780
+ mimeType: prepared.mimeType || 'video/mp4',
5781
+ };
5782
+ }
5783
+
5784
+ async function uploadSeedanceReferenceAudioUrl(pathOrUrl, apiKey, index = 0) {
5785
+ const ref = { flag: '--ref-audio', value: pathOrUrl, kind: 'audio' };
5786
+ const buffer = await fetchMediaBuffer(pathOrUrl);
5787
+ const file = await prepareSeedanceReferenceAudioUploadFile(pathOrUrl, buffer);
5788
+ const uploaded = await uploadPreparedApiMediaReferenceV2(ref, index, apiKey, file);
5789
+ return uploaded.url;
5790
+ }
5791
+
5792
+ async function uploadSeedanceReferenceVideoUrl(pathOrUrl, apiKey, index = 0) {
5793
+ const ref = { flag: '--ref-video', value: pathOrUrl, kind: 'video' };
5794
+ const buffer = await fetchMediaBuffer(pathOrUrl);
5795
+ const file = await prepareSeedanceReferenceVideoUploadFile(pathOrUrl, buffer);
5796
+ const uploaded = await uploadPreparedApiMediaReferenceV2(ref, index, apiKey, file);
5797
+ return uploaded.url;
5798
+ }
5799
+
5422
5800
  async function trimSeedanceV2VSourceVideoBuffer(buffer, sourceLabel, startOffset, requestedDuration) {
5423
5801
  const ffmpegPath = await ensureFfmpegAvailable();
5424
5802
  const tempDir = mkdtempSync(join(tmpdir(), 'sogni-seedance-v2v-'));
@@ -6733,12 +7111,41 @@ async function main() {
6733
7111
  || mimeTypeForPath(options.refAudio, 'application/octet-stream')
6734
7112
  )
6735
7113
  : 'unknown';
6736
- const useRefAudioUrl = isSeedanceVideo
6737
- && refAudioFormatByPath !== 'mp3'
6738
- && await appendSafeSeedanceReferenceUrl(seedanceReferenceAudioUrls, options.refAudio, 'Reference audio');
6739
- const useRefVideoUrl = isSeedanceVideo
6740
- && options.videoStart === null
6741
- && await appendSafeSeedanceReferenceUrl(seedanceReferenceVideoUrls, options.refVideo, 'Reference video');
7114
+ let projectVideoStart = options.videoStart;
7115
+ let useRefAudioUrl = false;
7116
+ if (isSeedanceVideo && options.refAudio) {
7117
+ const shouldUploadAudio =
7118
+ !isHttpsUrl(options.refAudio) ||
7119
+ refAudioFormatByPath !== 'mp3' ||
7120
+ options.audioStart !== null ||
7121
+ options.audioDuration !== null;
7122
+ if (shouldUploadAudio) {
7123
+ const uploadedAudioUrl = await uploadSeedanceReferenceAudioUrl(
7124
+ options.refAudio,
7125
+ creds.SOGNI_API_KEY,
7126
+ 0,
7127
+ );
7128
+ seedanceReferenceAudioUrls.push(uploadedAudioUrl);
7129
+ useRefAudioUrl = true;
7130
+ } else {
7131
+ useRefAudioUrl = await appendSafeSeedanceReferenceUrl(seedanceReferenceAudioUrls, options.refAudio, 'Reference audio');
7132
+ }
7133
+ }
7134
+ let useRefVideoUrl = false;
7135
+ if (isSeedanceVideo && options.refVideo) {
7136
+ if (isHttpsUrl(options.refVideo) && options.videoStart === null) {
7137
+ useRefVideoUrl = await appendSafeSeedanceReferenceUrl(seedanceReferenceVideoUrls, options.refVideo, 'Reference video');
7138
+ } else {
7139
+ const uploadedVideoUrl = await uploadSeedanceReferenceVideoUrl(
7140
+ options.refVideo,
7141
+ creds.SOGNI_API_KEY,
7142
+ 0,
7143
+ );
7144
+ seedanceReferenceVideoUrls.push(uploadedVideoUrl);
7145
+ useRefVideoUrl = true;
7146
+ projectVideoStart = null;
7147
+ }
7148
+ }
6742
7149
 
6743
7150
  // Seedance loose-reference extras: -c/--context images beyond start/end,
6744
7151
  // plus repeated --ref-audio / --ref-video entries past the first. The
@@ -6757,7 +7164,7 @@ async function main() {
6757
7164
  }
6758
7165
  await appendSafeSeedanceReferenceUrl(seedanceReferenceImageUrls, ctxImage, 'Seedance image reference');
6759
7166
  }
6760
- for (const extraAudio of options.refAudios) {
7167
+ for (const [extraAudioIndex, extraAudio] of options.refAudios.entries()) {
6761
7168
  if (!isHttpsUrl(extraAudio)) {
6762
7169
  fatalCliError(
6763
7170
  `Additional --ref-audio "${extraAudio}" must be an HTTPS URL. ` +
@@ -6765,7 +7172,21 @@ async function main() {
6765
7172
  { code: 'INVALID_ARGUMENT', details: { flag: '--ref-audio', value: extraAudio } },
6766
7173
  );
6767
7174
  }
6768
- await appendSafeSeedanceReferenceUrl(seedanceReferenceAudioUrls, extraAudio, 'Seedance audio reference');
7175
+ const extraAudioFormat = detectReferenceAudioFormat(
7176
+ new Uint8Array(),
7177
+ normalizeReferenceAudioMimeType(mimeTypeForPath(extraAudio, 'application/octet-stream'))
7178
+ || mimeTypeForPath(extraAudio, 'application/octet-stream')
7179
+ );
7180
+ if (extraAudioFormat !== 'mp3') {
7181
+ const uploadedAudioUrl = await uploadSeedanceReferenceAudioUrl(
7182
+ extraAudio,
7183
+ creds.SOGNI_API_KEY,
7184
+ extraAudioIndex + 1,
7185
+ );
7186
+ seedanceReferenceAudioUrls.push(uploadedAudioUrl);
7187
+ } else {
7188
+ await appendSafeSeedanceReferenceUrl(seedanceReferenceAudioUrls, extraAudio, 'Seedance audio reference');
7189
+ }
6769
7190
  }
6770
7191
  for (const extraVideo of options.refVideos) {
6771
7192
  if (!isHttpsUrl(extraVideo)) {
@@ -6783,7 +7204,6 @@ async function main() {
6783
7204
  let endImageBuffer = options.refImageEnd && !useRefImageEndUrl ? await fetchMediaBuffer(options.refImageEnd) : undefined;
6784
7205
  let audioBuffer = options.refAudio && !useRefAudioUrl ? await fetchMediaBuffer(options.refAudio) : undefined;
6785
7206
  let videoBuffer = options.refVideo && !useRefVideoUrl ? await fetchMediaBuffer(options.refVideo) : undefined;
6786
- let projectVideoStart = options.videoStart;
6787
7207
  if (audioBuffer) {
6788
7208
  audioBuffer = await prepareReferenceAudioForVideoBuffer(audioBuffer, options.refAudio);
6789
7209
  }
@@ -6884,10 +7304,10 @@ async function main() {
6884
7304
  if (audioBuffer) {
6885
7305
  projectConfig.referenceAudio = audioBuffer;
6886
7306
  }
6887
- if (options.audioStart !== null) {
7307
+ if (options.audioStart !== null && !useRefAudioUrl) {
6888
7308
  projectConfig.audioStart = options.audioStart;
6889
7309
  }
6890
- if (options.audioDuration !== null) {
7310
+ if (options.audioDuration !== null && !useRefAudioUrl) {
6891
7311
  projectConfig.audioDuration = options.audioDuration;
6892
7312
  }
6893
7313
  if (audioIdentityMedia) {
package/version.mjs CHANGED
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '3.3.0';
1
+ export const PACKAGE_VERSION = '3.3.2';