@sogni-ai/sogni-creative-agent-skill 3.2.0 → 3.3.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/package.json +1 -1
- package/sogni-agent.mjs +434 -14
- package/version.mjs +1 -1
package/package.json
CHANGED
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 =
|
|
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 =
|
|
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
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
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
|
-
|
|
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.
|
|
1
|
+
export const PACKAGE_VERSION = '3.3.1';
|