@aiyiran/myclaw 1.0.243 → 1.0.245
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/assets/myclaw-artifacts.js +15 -5
- package/assets/myclaw-inject.js +254 -21
- package/package.json +1 -1
- package/skills/yiran-skill-media/SKILL.md +55 -20
- package/skills/yiran-skill-media/config.json +36 -9
- package/skills/yiran-skill-media/scripts/generate.py +43 -8
- package/skills/yiran-skill-media/scripts/i2v.sh +47 -0
- package/skills/yiran-skill-media/scripts/providers/__init__.py +8 -0
- package/skills/yiran-skill-media/scripts/providers/jimeng_image.py +158 -0
- package/skills/yiran-skill-media/scripts/providers/jimeng_video.py +115 -0
- package/skills/yiran-skill-media/scripts/providers/minimax_video.py +115 -0
- package/skills/yiran-skill-media/scripts/video.sh +47 -0
|
@@ -290,17 +290,27 @@
|
|
|
290
290
|
});
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
function fetchArtifactsFromServerAPI(wsPrefix) {
|
|
294
|
+
var url = window.location.origin + '/cmd/api/artifacts?workspace=' + encodeURIComponent(wsPrefix) + '&t=' + Date.now();
|
|
295
|
+
return fetch(url).then(function (res) {
|
|
296
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
297
|
+
return res.json();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
293
301
|
function fetchArtifacts(contentEl) {
|
|
302
|
+
// cachedConfig 未就绪时跳过,等 initConfig 完成后的 startPolling 重试
|
|
303
|
+
if (!cachedConfig) return;
|
|
304
|
+
|
|
294
305
|
var wsPrefix = getWorkspaceId();
|
|
295
306
|
var fetcher;
|
|
296
307
|
|
|
297
308
|
if (envInfo && envInfo.remote) {
|
|
298
|
-
//
|
|
299
|
-
fetcher =
|
|
309
|
+
// 远程服务器 → 走 /cmd/api(服务器直接提供 JSON)
|
|
310
|
+
fetcher = fetchArtifactsFromServerAPI(wsPrefix);
|
|
300
311
|
} else {
|
|
301
|
-
// 本地环境 →
|
|
302
|
-
fetcher =
|
|
303
|
-
.catch(function () { return fetchArtifactsFromCDN(wsPrefix); });
|
|
312
|
+
// 本地环境 → 走 CDN
|
|
313
|
+
fetcher = fetchArtifactsFromCDN(wsPrefix);
|
|
304
314
|
}
|
|
305
315
|
|
|
306
316
|
fetcher
|
package/assets/myclaw-inject.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
var committedText = ""; // 已经提交到 textarea 的文字(上一轮累积)
|
|
28
28
|
var cursorOffset = 0; // 录音开始时光标在 textarea 中的位置
|
|
29
29
|
var injected = false;
|
|
30
|
+
var stopping = false; // 正在等待最终识别结果(stopVoice 的 2 秒窗口)
|
|
30
31
|
|
|
31
32
|
// ═══ 1. 右下角版本标签(点击测试麦克风) ═══
|
|
32
33
|
function createVersionBar() {
|
|
@@ -323,7 +324,8 @@
|
|
|
323
324
|
'</svg>',
|
|
324
325
|
].join("");
|
|
325
326
|
|
|
326
|
-
|
|
327
|
+
btn.addEventListener("click", function () {
|
|
328
|
+
console.log("[myclaw-voice] 按钮点击, recording=", recording);
|
|
327
329
|
if (recording) {
|
|
328
330
|
stopVoice();
|
|
329
331
|
} else {
|
|
@@ -367,8 +369,8 @@
|
|
|
367
369
|
|
|
368
370
|
voice = new window.VoiceInput({
|
|
369
371
|
onResult: function (text) {
|
|
370
|
-
//
|
|
371
|
-
if (!recording) return;
|
|
372
|
+
// 完全停止后才忽略;stopping 期间(2秒等待窗口)仍允许写入
|
|
373
|
+
if (!recording && !stopping) return;
|
|
372
374
|
// 讯飞实时返回识别文字,替换到光标位置
|
|
373
375
|
pendingText = text;
|
|
374
376
|
updateTextAtCursor(pendingText);
|
|
@@ -377,7 +379,7 @@
|
|
|
377
379
|
onStatusChange: function (oldStatus, newStatus) {
|
|
378
380
|
console.log("[myclaw-voice] \u72b6\u6001:", oldStatus, "->", newStatus);
|
|
379
381
|
|
|
380
|
-
if (newStatus === "idle" && recording) {
|
|
382
|
+
if (newStatus === "idle" && recording && !stopping) {
|
|
381
383
|
// 讯飞 60 秒断开,但用户没有点停止 → 自动重连
|
|
382
384
|
// 把当前识别的文字提交,并更新光标位置
|
|
383
385
|
committedText = getTextareaValue();
|
|
@@ -435,23 +437,54 @@
|
|
|
435
437
|
console.log("[myclaw-voice] \u5f00\u59cb\u5f55\u97f3\uff0c\u5149\u6807\u4f4d\u7f6e:", cursorOffset);
|
|
436
438
|
}
|
|
437
439
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 语音录入结束时,等待 2 秒后关闭录音资源
|
|
443
|
+
* @param {Function} [onDone] - 等待完成后执行的回调(如发送)
|
|
444
|
+
*/
|
|
445
|
+
var voiceStopTimer = null;
|
|
446
|
+
|
|
447
|
+
function stopVoice(onDone) {
|
|
448
|
+
console.log("[myclaw-voice] stopVoice called, recording=", recording, "onDone=", onDone ? "yes" : "no");
|
|
449
|
+
if (!recording) {
|
|
450
|
+
console.log("[myclaw-voice] stopVoice early return — not recording");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 进入 stopping 态:UI 立即更新,但 onResult 仍允许在 2 秒内写入文字
|
|
455
|
+
stopping = true;
|
|
440
456
|
recording = false;
|
|
441
457
|
updateButtonUI();
|
|
458
|
+
console.log("[myclaw-voice] stopping=true, UI updated, starting 2s timer...");
|
|
442
459
|
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
voice.stop();
|
|
460
|
+
// 延迟 2 秒后关闭录音资源(等讯飞把剩余识别结果全部推过来)
|
|
461
|
+
if (voiceStopTimer) {
|
|
462
|
+
console.log("[myclaw-voice] clearing previous timer");
|
|
463
|
+
clearTimeout(voiceStopTimer);
|
|
448
464
|
}
|
|
465
|
+
voiceStopTimer = setTimeout(function () {
|
|
466
|
+
voiceStopTimer = null;
|
|
467
|
+
stopping = false;
|
|
468
|
+
console.log("[myclaw-voice] 2s timer fired, closing resources...");
|
|
469
|
+
|
|
470
|
+
// 快照当前 textarea 值(2 秒内 onResult 可能已更新)
|
|
471
|
+
var finalText = getTextareaValue();
|
|
472
|
+
console.log("[myclaw-voice] finalText:", JSON.stringify(finalText.substring(0, 50)));
|
|
473
|
+
if (voice) {
|
|
474
|
+
console.log("[myclaw-voice] calling voice.stop()");
|
|
475
|
+
voice.stop();
|
|
476
|
+
}
|
|
477
|
+
committedText = finalText;
|
|
478
|
+
pendingText = "";
|
|
449
479
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
480
|
+
// 等待完成后执行回调(如发送)
|
|
481
|
+
if (onDone) {
|
|
482
|
+
console.log("[myclaw-voice] executing onDone callback...");
|
|
483
|
+
onDone();
|
|
484
|
+
}
|
|
453
485
|
|
|
454
|
-
|
|
486
|
+
console.log("[myclaw-voice] 停止录音完成");
|
|
487
|
+
}, 2000);
|
|
455
488
|
}
|
|
456
489
|
|
|
457
490
|
// ═══ 5. DOM 注入 ═══
|
|
@@ -522,6 +555,28 @@
|
|
|
522
555
|
}
|
|
523
556
|
// ═══ 6. 拦截发送按钮 ═══
|
|
524
557
|
|
|
558
|
+
/**
|
|
559
|
+
* 拦截 Enter 键:语音态下按回车 → 等待 2 秒后发送
|
|
560
|
+
*/
|
|
561
|
+
function hookVoiceEnter() {
|
|
562
|
+
document.addEventListener("keydown", function (e) {
|
|
563
|
+
if (e.key !== "Enter") return;
|
|
564
|
+
if (!recording) return;
|
|
565
|
+
|
|
566
|
+
// 语音录入中,无论焦点在哪里(textarea 或语音按钮),Enter 统一触发"停止并发送"
|
|
567
|
+
e.preventDefault();
|
|
568
|
+
e.stopPropagation();
|
|
569
|
+
|
|
570
|
+
console.log("[myclaw-voice] Enter按下, recording=", recording);
|
|
571
|
+
stopVoice(function () {
|
|
572
|
+
console.log("[myclaw-voice] Enter stopVoice callback firing...");
|
|
573
|
+
var sendBtn = document.querySelector("button.chat-send-btn, button[title=\"Send\"]");
|
|
574
|
+
if (sendBtn) sendBtn.click();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
}, true);
|
|
578
|
+
}
|
|
579
|
+
|
|
525
580
|
var sendHooked = false;
|
|
526
581
|
|
|
527
582
|
function hookSendButton() {
|
|
@@ -536,9 +591,17 @@
|
|
|
536
591
|
var text = getTextareaValue();
|
|
537
592
|
if (!text || !text.trim()) return; // 空文字不处理
|
|
538
593
|
|
|
539
|
-
// 1)
|
|
594
|
+
// 1) 停止语音输入(等待 2 秒后关闭,关闭后触发发送)
|
|
540
595
|
if (recording) {
|
|
541
|
-
|
|
596
|
+
e.preventDefault();
|
|
597
|
+
e.stopPropagation();
|
|
598
|
+
console.log("[myclaw-voice] 发送按钮点击(语音态), recording=", recording);
|
|
599
|
+
stopVoice(function () {
|
|
600
|
+
console.log("[myclaw-voice] 发送按钮 stopVoice callback firing...");
|
|
601
|
+
var sendBtn = document.querySelector("button.chat-send-btn, button[title=\"Send\"]");
|
|
602
|
+
if (sendBtn) sendBtn.click();
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
542
605
|
}
|
|
543
606
|
|
|
544
607
|
// 2) 复制到剪贴板
|
|
@@ -546,7 +609,6 @@
|
|
|
546
609
|
navigator.clipboard.writeText(text).then(function () {
|
|
547
610
|
console.log("[myclaw-send] 📋 已复制到剪贴板:", text.substring(0, 50) + (text.length > 50 ? "..." : ""));
|
|
548
611
|
}).catch(function () {
|
|
549
|
-
// fallback: 老方法
|
|
550
612
|
fallbackCopy(text);
|
|
551
613
|
});
|
|
552
614
|
} catch (ex) {
|
|
@@ -554,9 +616,8 @@
|
|
|
554
616
|
}
|
|
555
617
|
|
|
556
618
|
// 3) 让原生 click 继续走(发送消息)
|
|
557
|
-
// 不 preventDefault,不 stopPropagation
|
|
558
619
|
|
|
559
|
-
// 4) 延迟清空 textarea
|
|
620
|
+
// 4) 延迟清空 textarea
|
|
560
621
|
setTimeout(function () {
|
|
561
622
|
setTextareaValue("");
|
|
562
623
|
committedText = "";
|
|
@@ -853,7 +914,7 @@
|
|
|
853
914
|
{ label: "\uD83D\uDCAC \u6DFB\u52A0\u5BF9\u8BDD", desc: "\u6253\u5F00\u5DF2\u6709\u4F19\u4F34\u7684\u5BF9\u8BDD\u7A97\u53E3", hasInput: true, inputTitle: "\u6DFB\u52A0\u5BF9\u8BDD", placeholder: "\u8F93\u5165\u4F19\u4F34\u540D\u79F0\uFF0C\u5982 kakaxi", hint: "\u8F93\u5165\u4F60\u7684\u4F19\u4F34\u7684\u540D\u79F0\uFF08\u82F1\u6587\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\uFF09\uFF0C\u70B9\u51FB\u540E\u4F1A\u6253\u5F00\u5BF9\u8BDD\u7A97\u53E3", cmd: "mc tui {name}", color: "#10b981" },
|
|
854
915
|
{ label: "\uD83D\uDE80 \u5347\u7EA7", desc: "\u5347\u7EA7 myclaw \u5230\u6700\u65B0\u7248\u672C", hasInput: false, cmd: "mc up", color: "#8b5cf6" },
|
|
855
916
|
{ label: "\uD83D\uDD04 \u91CD\u542F", desc: "\u91CD\u542F\u670D\u52A1\uFF0C\u4FEE\u590D\u5927\u591A\u6570\u95EE\u9898", hasInput: false, cmd: "mc restart", color: "#ef4444" },
|
|
856
|
-
{ label: "\uD83E\uDD1D \u65B0\u4F19\u4F34", desc: "\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684 AI \u4F19\u4F34", hasInput: true, inputTitle: "\u65B0\u5EFA\u4F19\u4F34", placeholder: "\u8F93\u5165\u65B0\u4F19\u4F34\u540D\u79F0\uFF0C\u5982 my-cat", hint: "\u7ED9\u4F60\u7684\u65B0 AI \u4F19\u4F34\u8D77\u4E2A\u540D\u5B57\uFF08\u82F1\u6587\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\uFF09\uFF0C\u70B9\u51FB\u540E\u4F1A\u81EA\u52A8\u521B\u5EFA", cmd: "mc
|
|
917
|
+
{ label: "\uD83E\uDD1D \u65B0\u4F19\u4F34", desc: "\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684 AI \u4F19\u4F34", hasInput: true, inputTitle: "\u65B0\u5EFA\u4F19\u4F34", placeholder: "\u8F93\u5165\u65B0\u4F19\u4F34\u540D\u79F0\uFF0C\u5982 my-cat", hint: "\u7ED9\u4F60\u7684\u65B0 AI \u4F19\u4F34\u8D77\u4E2A\u540D\u5B57\uFF08\u82F1\u6587\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\uFF09\uFF0C\u70B9\u51FB\u540E\u4F1A\u81EA\u52A8\u521B\u5EFA", cmd: "mc new {name}", color: "#3b82f6" },
|
|
857
918
|
];
|
|
858
919
|
|
|
859
920
|
btns.forEach(function (item) {
|
|
@@ -908,6 +969,175 @@
|
|
|
908
969
|
form.appendChild(row);
|
|
909
970
|
});
|
|
910
971
|
|
|
972
|
+
// ── 删除伙伴按钮 ──
|
|
973
|
+
var delRow = document.createElement("div");
|
|
974
|
+
delRow.style.cssText = [
|
|
975
|
+
"padding:10px 14px",
|
|
976
|
+
"background:#252536",
|
|
977
|
+
"border-radius:6px",
|
|
978
|
+
"cursor:pointer",
|
|
979
|
+
"transition:background 0.15s",
|
|
980
|
+
"display:flex",
|
|
981
|
+
"align-items:center",
|
|
982
|
+
"gap:10px",
|
|
983
|
+
].join(";");
|
|
984
|
+
delRow.onmouseenter = function () { delRow.style.background = "#2f2f4a"; };
|
|
985
|
+
delRow.onmouseleave = function () { delRow.style.background = "#252536"; };
|
|
986
|
+
|
|
987
|
+
var delBar = document.createElement("div");
|
|
988
|
+
delBar.style.cssText = "width:3px;height:28px;border-radius:2px;background:#ef4444;flex-shrink:0;";
|
|
989
|
+
delRow.appendChild(delBar);
|
|
990
|
+
|
|
991
|
+
var delInfo = document.createElement("div");
|
|
992
|
+
delInfo.style.cssText = "flex:1;display:flex;flex-direction:column;gap:2px;";
|
|
993
|
+
var delName = document.createElement("div");
|
|
994
|
+
delName.textContent = "\uD83D\uDDD1 \u5220\u9664\u4F19\u4F34";
|
|
995
|
+
delName.style.cssText = "font-size:13px;font-weight:bold;color:#ef4444;";
|
|
996
|
+
delInfo.appendChild(delName);
|
|
997
|
+
var delDesc = document.createElement("div");
|
|
998
|
+
delDesc.textContent = "\u5220\u9664\u4E00\u4E2A AI \u4F19\u4F34\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u6062\u590D";
|
|
999
|
+
delDesc.style.cssText = "font-size:11px;color:#888;";
|
|
1000
|
+
delInfo.appendChild(delDesc);
|
|
1001
|
+
delRow.appendChild(delInfo);
|
|
1002
|
+
|
|
1003
|
+
var delArrow = document.createElement("div");
|
|
1004
|
+
delArrow.textContent = "\u25B6";
|
|
1005
|
+
delArrow.style.cssText = "color:#555;font-size:10px;";
|
|
1006
|
+
delRow.appendChild(delArrow);
|
|
1007
|
+
|
|
1008
|
+
delRow.onclick = function () {
|
|
1009
|
+
showDeleteConfirm();
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
form.appendChild(delRow);
|
|
1013
|
+
|
|
1014
|
+
// 删除伙伴 - 双重确认弹框
|
|
1015
|
+
function showDeleteConfirm() {
|
|
1016
|
+
var mask = document.createElement("div");
|
|
1017
|
+
mask.style.cssText = [
|
|
1018
|
+
"position:fixed",
|
|
1019
|
+
"top:0;left:0;width:100vw;height:100vh",
|
|
1020
|
+
"background:rgba(0,0,0,0.3)",
|
|
1021
|
+
"z-index:999999",
|
|
1022
|
+
"display:flex",
|
|
1023
|
+
"align-items:center",
|
|
1024
|
+
"justify-content:center",
|
|
1025
|
+
"animation:myclaw-fade-in 0.15s ease",
|
|
1026
|
+
].join(";");
|
|
1027
|
+
|
|
1028
|
+
var box = document.createElement("div");
|
|
1029
|
+
box.style.cssText = [
|
|
1030
|
+
"width:360px",
|
|
1031
|
+
"background:#1e1e2e",
|
|
1032
|
+
"border-radius:8px",
|
|
1033
|
+
"overflow:hidden",
|
|
1034
|
+
"box-shadow:0 8px 32px rgba(0,0,0,0.5)",
|
|
1035
|
+
].join(";");
|
|
1036
|
+
|
|
1037
|
+
// 标题
|
|
1038
|
+
var h = document.createElement("div");
|
|
1039
|
+
h.style.cssText = "padding:10px 14px;background:#ef4444;color:#fff;font-size:13px;display:flex;justify-content:space-between;align-items:center;";
|
|
1040
|
+
h.innerHTML = '<span>\uD83D\uDDD1 \u5220\u9664\u4F19\u4F34</span>';
|
|
1041
|
+
var x = document.createElement("span");
|
|
1042
|
+
x.textContent = "\u2715";
|
|
1043
|
+
x.style.cssText = "cursor:pointer;padding:2px 6px;border-radius:3px;";
|
|
1044
|
+
x.onclick = function () { mask.remove(); };
|
|
1045
|
+
h.appendChild(x);
|
|
1046
|
+
box.appendChild(h);
|
|
1047
|
+
|
|
1048
|
+
// body
|
|
1049
|
+
var body = document.createElement("div");
|
|
1050
|
+
body.style.cssText = "padding:16px;display:flex;flex-direction:column;gap:12px;";
|
|
1051
|
+
|
|
1052
|
+
var hint1 = document.createElement("div");
|
|
1053
|
+
hint1.textContent = "\u8BF7\u8F93\u5165\u8981\u5220\u9664\u7684\u4F19\u4F34 ID\uFF1A";
|
|
1054
|
+
hint1.style.cssText = "font-size:12px;color:#888;";
|
|
1055
|
+
body.appendChild(hint1);
|
|
1056
|
+
|
|
1057
|
+
var input1 = document.createElement("input");
|
|
1058
|
+
input1.type = "text";
|
|
1059
|
+
input1.placeholder = "\u4F19\u4F34 ID";
|
|
1060
|
+
input1.style.cssText = "padding:8px 10px;background:#252536;border:1px solid #3d3d5c;border-radius:4px;color:#cdd6f4;font-size:13px;font-family:monospace;outline:none;";
|
|
1061
|
+
body.appendChild(input1);
|
|
1062
|
+
|
|
1063
|
+
var warn = document.createElement("div");
|
|
1064
|
+
warn.textContent = "\u26A0 \u6B64\u64CD\u4F5C\u65E0\u6CD5\u6062\u590D\uFF0C\u786E\u8BA4\u540E\u5C06\u6C38\u4E45\u5220\u9664\uFF01";
|
|
1065
|
+
warn.style.cssText = "font-size:11px;color:#ef4444;padding:8px;background:rgba(239,68,68,0.1);border-radius:4px;";
|
|
1066
|
+
body.appendChild(warn);
|
|
1067
|
+
|
|
1068
|
+
var confirmHint = document.createElement("div");
|
|
1069
|
+
confirmHint.textContent = '\u8BF7\u8F93\u5165 "YES" \u786E\u8BA4\u5220\u9664\uFF1A';
|
|
1070
|
+
confirmHint.style.cssText = "font-size:12px;color:#888;";
|
|
1071
|
+
confirmHint.style.display = "none";
|
|
1072
|
+
body.appendChild(confirmHint);
|
|
1073
|
+
|
|
1074
|
+
var input2 = document.createElement("input");
|
|
1075
|
+
input2.type = "text";
|
|
1076
|
+
input2.placeholder = "YES";
|
|
1077
|
+
input2.style.cssText = "padding:8px 10px;background:#252536;border:1px solid #3d3d5c;border-radius:4px;color:#cdd6f4;font-size:13px;font-family:monospace;outline:none;";
|
|
1078
|
+
input2.style.display = "none";
|
|
1079
|
+
body.appendChild(input2);
|
|
1080
|
+
|
|
1081
|
+
var submitBtn = document.createElement("button");
|
|
1082
|
+
submitBtn.textContent = "\u7EE7\u7EED";
|
|
1083
|
+
submitBtn.style.cssText = "padding:8px 16px;background:#ef4444;border:none;border-radius:4px;color:#fff;font-size:12px;font-family:monospace;cursor:pointer;";
|
|
1084
|
+
body.appendChild(submitBtn);
|
|
1085
|
+
|
|
1086
|
+
var cancelBtn = document.createElement("button");
|
|
1087
|
+
cancelBtn.textContent = "\u53D6\u6D88";
|
|
1088
|
+
cancelBtn.style.cssText = "padding:8px 16px;background:#3d3d5c;border:none;border-radius:4px;color:#cdd6f4;font-size:12px;font-family:monospace;cursor:pointer;";
|
|
1089
|
+
body.appendChild(cancelBtn);
|
|
1090
|
+
|
|
1091
|
+
box.appendChild(body);
|
|
1092
|
+
mask.appendChild(box);
|
|
1093
|
+
document.body.appendChild(mask);
|
|
1094
|
+
|
|
1095
|
+
input1.focus();
|
|
1096
|
+
|
|
1097
|
+
var agentId = "";
|
|
1098
|
+
submitBtn.onclick = function () {
|
|
1099
|
+
if (!agentId) {
|
|
1100
|
+
// 第一步:输入 agent ID
|
|
1101
|
+
agentId = input1.value.trim();
|
|
1102
|
+
if (!agentId) {
|
|
1103
|
+
input1.style.borderColor = "#ef4444";
|
|
1104
|
+
input1.focus();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
// 显示第二步确认
|
|
1108
|
+
input1.style.display = "none";
|
|
1109
|
+
hint1.style.display = "none";
|
|
1110
|
+
confirmHint.style.display = "block";
|
|
1111
|
+
input2.style.display = "block";
|
|
1112
|
+
submitBtn.textContent = "\u786E\u8BA4\u5220\u9664";
|
|
1113
|
+
input2.value = "";
|
|
1114
|
+
input2.focus();
|
|
1115
|
+
} else {
|
|
1116
|
+
// 第二步:确认删除
|
|
1117
|
+
var confirmVal = input2.value.trim();
|
|
1118
|
+
if (confirmVal.toUpperCase() !== "YES") {
|
|
1119
|
+
input2.style.borderColor = "#ef4444";
|
|
1120
|
+
input2.focus();
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
submitBtn.disabled = true;
|
|
1124
|
+
submitBtn.textContent = "\u6267\u884C\u4E2D...";
|
|
1125
|
+
runCommand("openclaw agents delete " + agentId + " --force");
|
|
1126
|
+
setTimeout(function () {
|
|
1127
|
+
mask.remove();
|
|
1128
|
+
}, 1000);
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
cancelBtn.onclick = function () {
|
|
1133
|
+
mask.remove();
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
input2.onkeydown = function (e) {
|
|
1137
|
+
if (e.key === "Enter") submitBtn.click();
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
911
1141
|
box.appendChild(header);
|
|
912
1142
|
box.appendChild(form);
|
|
913
1143
|
overlay.appendChild(box);
|
|
@@ -939,6 +1169,9 @@
|
|
|
939
1169
|
// 初始化 VoiceInput SDK
|
|
940
1170
|
initVoice();
|
|
941
1171
|
|
|
1172
|
+
// 拦截语音态 Enter 键
|
|
1173
|
+
hookVoiceEnter();
|
|
1174
|
+
|
|
942
1175
|
// 持续监听 DOM 变化,确保按钮始终在
|
|
943
1176
|
new MutationObserver(function () {
|
|
944
1177
|
if (!document.querySelector("#myclaw-voice-btn")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yiran-skill-media
|
|
3
|
-
description:
|
|
3
|
+
description: 统一多媒体生成技能。支持图片、音乐、文生视频和图生视频,按资源类型自动路由到最优 provider,支持多级降级。资源生成规范:所有生成的资源必须存放在当前工作目录下,调用时通过 --output-dir 传入当前工作目录的绝对路径,通过 --name 传入资源的中文名称。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 统一多媒体生成
|
|
@@ -12,10 +12,6 @@ description: 统一多媒体生成技能。支持图片和音乐生成,按资
|
|
|
12
12
|
|
|
13
13
|
**所有生成的资源文件必须存放在当前工作目录下。** 不允许省略,不允许猜测路径。
|
|
14
14
|
|
|
15
|
-
例如:
|
|
16
|
-
- 你当前在 `/root/.openclaw/workspace` → 传入 `--output-dir /root/.openclaw/workspace`
|
|
17
|
-
- 生成一张日落图片 → 传入 `--name 日落风景`
|
|
18
|
-
|
|
19
15
|
## 一键脚本
|
|
20
16
|
|
|
21
17
|
```bash
|
|
@@ -24,6 +20,12 @@ description: 统一多媒体生成技能。支持图片和音乐生成,按资
|
|
|
24
20
|
|
|
25
21
|
# 音乐生成
|
|
26
22
|
./music.sh --output-dir "$(pwd)" --name 开场音乐 "epic opening" [--instrumental]
|
|
23
|
+
|
|
24
|
+
# 文生视频 (Text-to-Video)
|
|
25
|
+
./video.sh --output-dir "$(pwd)" --name 日落延时 "a sunset timelapse" [--duration 6] [--resolution 768P] [--aspect-ratio 16:9]
|
|
26
|
+
|
|
27
|
+
# 图生视频 (Image-to-Video)
|
|
28
|
+
./i2v.sh --output-dir "$(pwd)" --name 猫咪跑步 --first-frame-image "https://example.com/cat.jpg" "cat running toward camera"
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
## 参数说明
|
|
@@ -32,40 +34,73 @@ description: 统一多媒体生成技能。支持图片和音乐生成,按资
|
|
|
32
34
|
|
|
33
35
|
| 参数 | 必填 | 说明 |
|
|
34
36
|
|------|------|------|
|
|
35
|
-
| `--output-dir` | 是 |
|
|
37
|
+
| `--output-dir` | 是 | 输出目录的绝对路径 |
|
|
36
38
|
| `--name` | 是 | 资源中文名称(如:日落风景、产品封面) |
|
|
37
39
|
| `prompt` | 是 | 图片描述 |
|
|
38
|
-
| `--aspect-ratio` | 否 | 比例,默认 1:1
|
|
40
|
+
| `--aspect-ratio` | 否 | 比例,默认 16:9。可选:1:1, 16:9, 9:16, 4:3, 3:4 等 |
|
|
39
41
|
|
|
40
42
|
### music.sh
|
|
41
43
|
|
|
42
44
|
| 参数 | 必填 | 说明 |
|
|
43
45
|
|------|------|------|
|
|
44
|
-
| `--output-dir` | 是 |
|
|
45
|
-
| `--name` | 是 |
|
|
46
|
+
| `--output-dir` | 是 | 输出目录的绝对路径 |
|
|
47
|
+
| `--name` | 是 | 资源中文名称 |
|
|
46
48
|
| `prompt` | 是 | 音乐风格/情绪描述 |
|
|
47
49
|
| `--lyrics` | 否 | 歌词文本 |
|
|
48
50
|
| `--instrumental` | 否 | 纯音乐模式 |
|
|
49
51
|
|
|
52
|
+
### video.sh — 文生视频
|
|
53
|
+
|
|
54
|
+
| 参数 | 必填 | 说明 |
|
|
55
|
+
|------|------|------|
|
|
56
|
+
| `--output-dir` | 是 | 输出目录的绝对路径 |
|
|
57
|
+
| `--name` | 是 | 资源中文名称 |
|
|
58
|
+
| `prompt` | 是 | 视频内容描述(支持运镜指令如 `[推进]`、`[左摇]`) |
|
|
59
|
+
| `--duration` | 否 | 视频时长(秒),默认 6,可选 6 或 10 |
|
|
60
|
+
| `--resolution` | 否 | MiniMax 分辨率:768P/1080P(默认 768P) |
|
|
61
|
+
| `--aspect-ratio` | 否 | 即梦降级时使用:16:9/9:16/4:3/1:1(默认 16:9) |
|
|
62
|
+
|
|
63
|
+
### i2v.sh — 图生视频
|
|
64
|
+
|
|
65
|
+
| 参数 | 必填 | 说明 |
|
|
66
|
+
|------|------|------|
|
|
67
|
+
| `--output-dir` | 是 | 输出目录的绝对路径 |
|
|
68
|
+
| `--name` | 是 | 资源中文名称 |
|
|
69
|
+
| `--first-frame-image` | 是 | 首帧图片的公网 URL(JPG/PNG/WebP,<20MB) |
|
|
70
|
+
| `prompt` | 否 | 基于首帧图像的动作/变化描述 |
|
|
71
|
+
| `--duration` | 否 | 视频时长,默认 6 |
|
|
72
|
+
| `--resolution` | 否 | 分辨率,默认 768P |
|
|
73
|
+
|
|
74
|
+
**注意**:视频生成为异步任务,耗时较长(通常 1-5 分钟),脚本会自动轮询等待完成。
|
|
75
|
+
|
|
50
76
|
## 架构
|
|
51
77
|
|
|
52
78
|
```
|
|
53
|
-
image.sh
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
image.sh → 图片生成入口
|
|
80
|
+
music.sh → 音乐生成入口
|
|
81
|
+
video.sh → 文生视频入口 (Text-to-Video)
|
|
82
|
+
i2v.sh → 图生视频入口 (Image-to-Video)
|
|
83
|
+
generate.py → 统一路由调度(优先级数组,依次尝试)
|
|
84
|
+
config.json → provider 配置中心(优先级列表)
|
|
85
|
+
providers/
|
|
86
|
+
vapi_image.py → VAPI 图片
|
|
87
|
+
minimax_image.py → MiniMax 图片
|
|
88
|
+
jimeng_image.py → 即梦 图片 4.0(异步)
|
|
89
|
+
minimax_music.py → MiniMax 音乐
|
|
90
|
+
minimax_video.py → MiniMax 视频(文生+图生)
|
|
91
|
+
jimeng_video.py → 即梦 视频 3.0(异步)
|
|
60
92
|
```
|
|
61
93
|
|
|
62
|
-
## Provider
|
|
94
|
+
## Provider 配置(优先级列表)
|
|
63
95
|
|
|
64
|
-
|
|
96
|
+
`config.json` 中每个资源类型是一个数组,按优先级从高到低排列。
|
|
97
|
+
第一个失败自动尝试下一个,直到成功或全部失败。
|
|
65
98
|
|
|
66
99
|
当前配置:
|
|
67
|
-
-
|
|
68
|
-
-
|
|
100
|
+
- **图片**:① VAPI (nano-banana-2) → ② MiniMax (image-01) → ③ 即梦 (jimeng_t2i_v40)
|
|
101
|
+
- **音乐**:① MiniMax (music-2.6)
|
|
102
|
+
- **文生视频**:① MiniMax (MiniMax-Hailuo-2.3) → ② 即梦 (jimeng_t2v_v30)
|
|
103
|
+
- **图生视频**:① MiniMax (MiniMax-Hailuo-2.3-Fast)
|
|
69
104
|
|
|
70
105
|
## 详细 API 参考
|
|
71
106
|
|
|
@@ -1,26 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"output_dir": "media",
|
|
3
|
-
"image":
|
|
4
|
-
|
|
3
|
+
"image": [
|
|
4
|
+
{
|
|
5
|
+
"provider": "jimeng_image",
|
|
6
|
+
"model": "jimeng_t2i_v40",
|
|
7
|
+
"access_key": "AKLTYjZkY2FiZmZkYWU5NDkxNmEwZjNlYTRjNmRlZmYwNDI",
|
|
8
|
+
"secret_key": "TjJGbU5HVTBZek14TnpFeE5HWTVOVGhsTURRNE9XRXhNR1JoTm1FeVlqaw=="
|
|
9
|
+
},
|
|
10
|
+
{
|
|
5
11
|
"provider": "vapi_image",
|
|
6
|
-
"model": "nano-banana-
|
|
12
|
+
"model": "nano-banana-2",
|
|
7
13
|
"base_url": "https://api.v3.cm/v1",
|
|
8
14
|
"api_key": "sk-PXPUzqllWKJy2oj011Df510242264219Ba21093e3d2b2335"
|
|
9
15
|
},
|
|
10
|
-
|
|
16
|
+
{
|
|
11
17
|
"provider": "minimax_image",
|
|
12
18
|
"model": "image-01",
|
|
13
19
|
"base_url": "https://api.minimaxi.com/v1",
|
|
14
20
|
"api_key": "sk-cp-DC5lWd2Stt9CBFzLIT2awP4K-ZEn5AkYwjl3Cdj-mIBmgjxod518F2LaVF2L9c35Wv5-Eox0F1ctJD5vXtB9p3OmxoWLd9ge9zIUIMrCVuqBYdL_s6kb8Qs"
|
|
15
21
|
}
|
|
16
|
-
|
|
17
|
-
"music":
|
|
18
|
-
|
|
22
|
+
],
|
|
23
|
+
"music": [
|
|
24
|
+
{
|
|
19
25
|
"provider": "minimax_music",
|
|
20
26
|
"model": "music-2.6",
|
|
21
27
|
"base_url": "https://api.minimaxi.com/v1",
|
|
22
28
|
"api_key": "sk-cp-DC5lWd2Stt9CBFzLIT2awP4K-ZEn5AkYwjl3Cdj-mIBmgjxod518F2LaVF2L9c35Wv5-Eox0F1ctJD5vXtB9p3OmxoWLd9ge9zIUIMrCVuqBYdL_s6kb8Qs"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"video": [
|
|
32
|
+
{
|
|
33
|
+
"provider": "minimax_video",
|
|
34
|
+
"model": "MiniMax-Hailuo-2.3",
|
|
35
|
+
"base_url": "https://api.minimaxi.com/v1",
|
|
36
|
+
"api_key": "sk-cp-DC5lWd2Stt9CBFzLIT2awP4K-ZEn5AkYwjl3Cdj-mIBmgjxod518F2LaVF2L9c35Wv5-Eox0F1ctJD5vXtB9p3OmxoWLd9ge9zIUIMrCVuqBYdL_s6kb8Qs"
|
|
23
37
|
},
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
{
|
|
39
|
+
"provider": "jimeng_video",
|
|
40
|
+
"model": "jimeng_t2v_v30",
|
|
41
|
+
"access_key": "AKLTYjZkY2FiZmZkYWU5NDkxNmEwZjNlYTRjNmRlZmYwNDI",
|
|
42
|
+
"secret_key": "TjJGbU5HVTBZek14TnpFeE5HWTVOVGhsTURRNE9XRXhNR1JoTm1FeVlqaw=="
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"i2v": [
|
|
46
|
+
{
|
|
47
|
+
"provider": "minimax_video",
|
|
48
|
+
"model": "MiniMax-Hailuo-2.3-Fast",
|
|
49
|
+
"base_url": "https://api.minimaxi.com/v1",
|
|
50
|
+
"api_key": "sk-cp-DC5lWd2Stt9CBFzLIT2awP4K-ZEn5AkYwjl3Cdj-mIBmgjxod518F2LaVF2L9c35Wv5-Eox0F1ctJD5vXtB9p3OmxoWLd9ge9zIUIMrCVuqBYdL_s6kb8Qs"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
26
53
|
}
|
|
@@ -102,9 +102,9 @@ def append_log(log_path, entry):
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def dispatch(resource_type, prompt, **kwargs):
|
|
105
|
-
"""Route to
|
|
105
|
+
"""Route to providers sequentially as a priority array.
|
|
106
106
|
|
|
107
|
-
Always tries
|
|
107
|
+
Always tries the first provider, then falls back to the next on failure.
|
|
108
108
|
Returns (files, used_provider_cfg).
|
|
109
109
|
"""
|
|
110
110
|
cfg = load_config()
|
|
@@ -112,9 +112,15 @@ def dispatch(resource_type, prompt, **kwargs):
|
|
|
112
112
|
if not resource_cfg:
|
|
113
113
|
raise ValueError(f"unknown resource type: {resource_type}")
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
if resource_cfg
|
|
117
|
-
providers
|
|
115
|
+
# 支持旧版 dict(primary/fallback) 过渡到新版 list
|
|
116
|
+
if isinstance(resource_cfg, dict):
|
|
117
|
+
providers = [resource_cfg["primary"]]
|
|
118
|
+
if resource_cfg.get("fallback"):
|
|
119
|
+
providers.append(resource_cfg["fallback"])
|
|
120
|
+
elif isinstance(resource_cfg, list):
|
|
121
|
+
providers = resource_cfg
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError(f"invalid config structure for {resource_type}")
|
|
118
124
|
|
|
119
125
|
sys.path.insert(0, SCRIPT_DIR)
|
|
120
126
|
from providers import get_adapter
|
|
@@ -136,16 +142,23 @@ def dispatch(resource_type, prompt, **kwargs):
|
|
|
136
142
|
|
|
137
143
|
def main():
|
|
138
144
|
parser = argparse.ArgumentParser(description="Unified media generation dispatcher")
|
|
139
|
-
parser.add_argument("type", choices=["image", "music"], help="Resource type")
|
|
140
|
-
parser.add_argument("prompt", help="Generation prompt")
|
|
141
|
-
parser.add_argument("--aspect-ratio", default="
|
|
145
|
+
parser.add_argument("type", choices=["image", "music", "video", "i2v"], help="Resource type")
|
|
146
|
+
parser.add_argument("prompt", nargs="?", default="", help="Generation prompt")
|
|
147
|
+
parser.add_argument("--aspect-ratio", default="16:9", help="Aspect ratio (image: 1:1; video: 16:9/9:16/4:3/1:1)")
|
|
142
148
|
parser.add_argument("--lyrics", default=None, help="Lyrics text (music only)")
|
|
143
149
|
parser.add_argument("--instrumental", action="store_true", help="Instrumental mode (music only)")
|
|
150
|
+
parser.add_argument("--duration", type=int, default=None, help="Video duration in seconds (video/i2v, default 6)")
|
|
151
|
+
parser.add_argument("--resolution", default=None, help="Video resolution: 720P/768P/1080P (video/i2v)")
|
|
152
|
+
parser.add_argument("--first-frame-image", default=None, help="First frame image URL (i2v only, required)")
|
|
144
153
|
parser.add_argument("--output", default=None, help="Output file path")
|
|
145
154
|
parser.add_argument("--output-dir", required=True, help="Absolute path to output directory (required)")
|
|
146
155
|
parser.add_argument("--name", required=True, help="Resource name in Chinese (required, e.g. 日落风景)")
|
|
147
156
|
args = parser.parse_args()
|
|
148
157
|
|
|
158
|
+
# i2v 必须有 --first-frame-image
|
|
159
|
+
if args.type == "i2v" and not args.first_frame_image:
|
|
160
|
+
parser.error("i2v (图生视频) 模式必须提供 --first-frame-image 参数")
|
|
161
|
+
|
|
149
162
|
out_dir = ensure_output_dir(args.output_dir)
|
|
150
163
|
|
|
151
164
|
# Prepare kwargs
|
|
@@ -155,6 +168,28 @@ def main():
|
|
|
155
168
|
"aspect_ratio": args.aspect_ratio,
|
|
156
169
|
}
|
|
157
170
|
ext = "png"
|
|
171
|
+
elif args.type == "video":
|
|
172
|
+
# 文生视频 — 纯文本,无图片
|
|
173
|
+
kwargs = {
|
|
174
|
+
"out_dir": out_dir,
|
|
175
|
+
"aspect_ratio": args.aspect_ratio,
|
|
176
|
+
}
|
|
177
|
+
if args.duration:
|
|
178
|
+
kwargs["duration"] = args.duration
|
|
179
|
+
if args.resolution:
|
|
180
|
+
kwargs["resolution"] = args.resolution
|
|
181
|
+
ext = "mp4"
|
|
182
|
+
elif args.type == "i2v":
|
|
183
|
+
# 图生视频 — 必须有首帧图片
|
|
184
|
+
kwargs = {
|
|
185
|
+
"out_dir": out_dir,
|
|
186
|
+
"first_frame_image": args.first_frame_image,
|
|
187
|
+
}
|
|
188
|
+
if args.duration:
|
|
189
|
+
kwargs["duration"] = args.duration
|
|
190
|
+
if args.resolution:
|
|
191
|
+
kwargs["resolution"] = args.resolution
|
|
192
|
+
ext = "mp4"
|
|
158
193
|
else:
|
|
159
194
|
kwargs = {
|
|
160
195
|
"out_dir": out_dir,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 图生视频入口 (Image-to-Video)
|
|
3
|
+
# 模型: MiniMax-Hailuo-2.3-Fast
|
|
4
|
+
# 用法: ./i2v.sh --output-dir /abs/path --name 中文名 --first-frame-image URL ["描述"]
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
OUTPUT_DIR=""
|
|
8
|
+
NAME=""
|
|
9
|
+
PROMPT=""
|
|
10
|
+
DURATION=""
|
|
11
|
+
RESOLUTION=""
|
|
12
|
+
FIRST_FRAME=""
|
|
13
|
+
|
|
14
|
+
while [ $# -gt 0 ]; do
|
|
15
|
+
case "$1" in
|
|
16
|
+
--output-dir) OUTPUT_DIR="$2"; shift 2 ;;
|
|
17
|
+
--name) NAME="$2"; shift 2 ;;
|
|
18
|
+
--duration) DURATION="$2"; shift 2 ;;
|
|
19
|
+
--resolution) RESOLUTION="$2"; shift 2 ;;
|
|
20
|
+
--first-frame-image) FIRST_FRAME="$2"; shift 2 ;;
|
|
21
|
+
*)
|
|
22
|
+
if [ -z "$PROMPT" ]; then
|
|
23
|
+
PROMPT="$1"
|
|
24
|
+
fi
|
|
25
|
+
shift
|
|
26
|
+
;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
if [ -z "$OUTPUT_DIR" ] || [ -z "$NAME" ] || [ -z "$FIRST_FRAME" ]; then
|
|
31
|
+
echo "用法: ./i2v.sh --output-dir <绝对路径> --name <中文名> --first-frame-image <图片URL> [\"动作描述\"]"
|
|
32
|
+
echo "示例: ./i2v.sh --output-dir \$(pwd) --name 猫咪跑步 --first-frame-image https://example.com/cat.jpg \"cat running toward camera\""
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
37
|
+
|
|
38
|
+
ARGS=()
|
|
39
|
+
ARGS+=("i2v")
|
|
40
|
+
[ -n "$PROMPT" ] && ARGS+=("$PROMPT")
|
|
41
|
+
ARGS+=(--output-dir "$OUTPUT_DIR")
|
|
42
|
+
ARGS+=(--name "$NAME")
|
|
43
|
+
ARGS+=(--first-frame-image "$FIRST_FRAME")
|
|
44
|
+
[ -n "$DURATION" ] && ARGS+=(--duration "$DURATION")
|
|
45
|
+
[ -n "$RESOLUTION" ] && ARGS+=(--resolution "$RESOLUTION")
|
|
46
|
+
|
|
47
|
+
python3 "$SCRIPT_DIR/generate.py" "${ARGS[@]}"
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from .vapi_image import VAPIImageAdapter
|
|
2
2
|
from .minimax_image import MiniMaxImageAdapter
|
|
3
3
|
from .minimax_music import MiniMaxMusicAdapter
|
|
4
|
+
from .minimax_video import MiniMaxVideoAdapter
|
|
5
|
+
from .jimeng_video import JimengVideoAdapter
|
|
6
|
+
from .jimeng_image import JimengImageAdapter
|
|
4
7
|
|
|
5
8
|
ADAPTERS = {
|
|
6
9
|
"vapi_image": VAPIImageAdapter(),
|
|
7
10
|
"minimax_image": MiniMaxImageAdapter(),
|
|
8
11
|
"minimax_music": MiniMaxMusicAdapter(),
|
|
12
|
+
"minimax_video": MiniMaxVideoAdapter(),
|
|
13
|
+
"jimeng_video": JimengVideoAdapter(),
|
|
14
|
+
"jimeng_image": JimengImageAdapter(),
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
def get_adapter(name: str):
|
|
@@ -13,3 +19,5 @@ def get_adapter(name: str):
|
|
|
13
19
|
if not adapter:
|
|
14
20
|
raise ValueError(f"unknown provider: {name}")
|
|
15
21
|
return adapter
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""即梦(Jimeng) image adapter — Volcengine SDK, async workflow.
|
|
2
|
+
Uses CVSync2AsyncSubmitTask → CVSync2AsyncGetResult polling.
|
|
3
|
+
Supports text-to-image via jimeng_t2i_v40."""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from volcengine.visual.VisualService import VisualService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JimengImageAdapter:
|
|
14
|
+
POLL_INTERVAL = 5 # 图片生成比视频快,5 秒轮询
|
|
15
|
+
MAX_WAIT = 120 # 最长等 2 分钟
|
|
16
|
+
|
|
17
|
+
# 比例 → 推荐的 2K 分辨率(官方文档中的推荐值)
|
|
18
|
+
RATIO_MAP = {
|
|
19
|
+
"1:1": (2048, 2048),
|
|
20
|
+
"4:3": (2304, 1728),
|
|
21
|
+
"3:4": (1728, 2304),
|
|
22
|
+
"3:2": (2496, 1664),
|
|
23
|
+
"2:3": (1664, 2496),
|
|
24
|
+
"16:9": (2560, 1440),
|
|
25
|
+
"9:16": (1440, 2560),
|
|
26
|
+
"21:9": (3024, 1296),
|
|
27
|
+
"9:21": (1296, 3024),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def generate(self, prompt, config, **kwargs):
|
|
31
|
+
access_key = config["access_key"]
|
|
32
|
+
secret_key = config["secret_key"]
|
|
33
|
+
req_key = config.get("model", "jimeng_t2i_v40")
|
|
34
|
+
out_dir = kwargs["out_dir"]
|
|
35
|
+
aspect_ratio = kwargs.get("aspect_ratio", "1:1")
|
|
36
|
+
|
|
37
|
+
vs = VisualService()
|
|
38
|
+
vs.set_ak(access_key)
|
|
39
|
+
vs.set_sk(secret_key)
|
|
40
|
+
|
|
41
|
+
# ═══ Step 1: 提交任务 ═══
|
|
42
|
+
body = {
|
|
43
|
+
"req_key": req_key,
|
|
44
|
+
"prompt": prompt,
|
|
45
|
+
"force_single": True, # 强制输出单图,确保智能体调用行为可预测
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# 宽高映射
|
|
49
|
+
if aspect_ratio in self.RATIO_MAP:
|
|
50
|
+
w, h = self.RATIO_MAP[aspect_ratio]
|
|
51
|
+
body["width"] = w
|
|
52
|
+
body["height"] = h
|
|
53
|
+
else:
|
|
54
|
+
# 不传 width/height,让模型根据 prompt 自动判断
|
|
55
|
+
body["size"] = 2048 * 2048 # 2K 默认面积
|
|
56
|
+
|
|
57
|
+
print(f"[jimeng_image] 提交文生图任务 req_key={req_key} ratio={aspect_ratio}...", file=sys.stderr)
|
|
58
|
+
try:
|
|
59
|
+
data = vs.cv_sync2async_submit_task(body)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
if hasattr(e, 'args') and len(e.args) > 0 and isinstance(e.args[0], bytes):
|
|
62
|
+
try:
|
|
63
|
+
err_json = json.loads(e.args[0].decode('utf-8'))
|
|
64
|
+
code = err_json.get("code", 0)
|
|
65
|
+
msg = err_json.get("message", "")
|
|
66
|
+
raise RuntimeError(f"即梦 API 错误: {msg} (code={code})")
|
|
67
|
+
except RuntimeError:
|
|
68
|
+
raise
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
raise RuntimeError(f"即梦提交失败: {str(e)}")
|
|
72
|
+
|
|
73
|
+
if data.get("code") != 10000:
|
|
74
|
+
raise RuntimeError(f"即梦 API 错误: {data.get('message')} (code={data.get('code')})")
|
|
75
|
+
|
|
76
|
+
task_id = data["data"]["task_id"]
|
|
77
|
+
print(f"[jimeng_image] task_id={task_id}, 开始轮询...", file=sys.stderr)
|
|
78
|
+
|
|
79
|
+
# ═══ Step 2: 轮询状态 ═══
|
|
80
|
+
image_urls = None
|
|
81
|
+
binary_data = None
|
|
82
|
+
elapsed = 0
|
|
83
|
+
while elapsed < self.MAX_WAIT:
|
|
84
|
+
time.sleep(self.POLL_INTERVAL)
|
|
85
|
+
elapsed += self.POLL_INTERVAL
|
|
86
|
+
|
|
87
|
+
query_body = {
|
|
88
|
+
"req_key": req_key,
|
|
89
|
+
"task_id": task_id,
|
|
90
|
+
"req_json": json.dumps({"return_url": True}),
|
|
91
|
+
}
|
|
92
|
+
try:
|
|
93
|
+
qdata = vs.cv_sync2async_get_result(query_body)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
if hasattr(e, 'args') and len(e.args) > 0 and isinstance(e.args[0], bytes):
|
|
96
|
+
try:
|
|
97
|
+
err_json = json.loads(e.args[0].decode('utf-8'))
|
|
98
|
+
code = err_json.get("code", 0)
|
|
99
|
+
if code in (50429, 50430, 50500, 50501):
|
|
100
|
+
print(f"[jimeng_image] 轮询 {elapsed}s: 可重试错误 code={code}", file=sys.stderr)
|
|
101
|
+
continue
|
|
102
|
+
raise RuntimeError(f"即梦查询失败: {err_json.get('message')} (code={code})")
|
|
103
|
+
except RuntimeError:
|
|
104
|
+
raise
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
raise RuntimeError(f"即梦查询异常: {str(e)}")
|
|
108
|
+
|
|
109
|
+
if qdata.get("code") != 10000:
|
|
110
|
+
print(f"[jimeng_image] 轮询 {elapsed}s: code={qdata.get('code')} msg={qdata.get('message')}", file=sys.stderr)
|
|
111
|
+
if qdata.get("code") in (50429, 50430, 50500, 50501):
|
|
112
|
+
continue
|
|
113
|
+
raise RuntimeError(f"即梦状态获取失败: {qdata.get('message')} (code={qdata.get('code')})")
|
|
114
|
+
|
|
115
|
+
status = qdata.get("data", {}).get("status", "")
|
|
116
|
+
print(f"[jimeng_image] 轮询 {elapsed}s: status={status}", file=sys.stderr)
|
|
117
|
+
|
|
118
|
+
if status == "done":
|
|
119
|
+
image_urls = qdata["data"].get("image_urls", [])
|
|
120
|
+
binary_data = qdata["data"].get("binary_data_base64", [])
|
|
121
|
+
break
|
|
122
|
+
elif status in ("in_queue", "generating"):
|
|
123
|
+
continue
|
|
124
|
+
elif status in ("not_found", "expired"):
|
|
125
|
+
raise RuntimeError(f"即梦任务异常: status={status}")
|
|
126
|
+
|
|
127
|
+
if not image_urls and not binary_data:
|
|
128
|
+
raise RuntimeError(f"即梦图片生成超时 ({self.MAX_WAIT}s), task_id={task_id}")
|
|
129
|
+
|
|
130
|
+
print(f"[jimeng_image] 生成完成, 下载中...", file=sys.stderr)
|
|
131
|
+
|
|
132
|
+
# ═══ Step 3: 下载图片 ═══
|
|
133
|
+
import base64
|
|
134
|
+
saved = []
|
|
135
|
+
|
|
136
|
+
if image_urls:
|
|
137
|
+
for i, url in enumerate(image_urls):
|
|
138
|
+
fname = kwargs.get("output_path") or os.path.join(out_dir, f"image_{i}.png")
|
|
139
|
+
r = requests.get(url, timeout=60)
|
|
140
|
+
r.raise_for_status()
|
|
141
|
+
with open(fname, "wb") as f:
|
|
142
|
+
f.write(r.content)
|
|
143
|
+
saved.append(fname)
|
|
144
|
+
print(f"[jimeng_image] 已保存: {fname}", file=sys.stderr)
|
|
145
|
+
elif binary_data:
|
|
146
|
+
for i, b64 in enumerate(binary_data):
|
|
147
|
+
if not b64:
|
|
148
|
+
continue
|
|
149
|
+
fname = kwargs.get("output_path") or os.path.join(out_dir, f"image_{i}.png")
|
|
150
|
+
with open(fname, "wb") as f:
|
|
151
|
+
f.write(base64.b64decode(b64))
|
|
152
|
+
saved.append(fname)
|
|
153
|
+
print(f"[jimeng_image] 已保存 (base64): {fname}", file=sys.stderr)
|
|
154
|
+
|
|
155
|
+
if not saved:
|
|
156
|
+
raise RuntimeError("即梦未返回任何图片数据")
|
|
157
|
+
|
|
158
|
+
return saved
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""即梦(Jimeng) video adapter — Volcengine SDK Implementation.
|
|
2
|
+
Supports text-to-video via CVSync2AsyncSubmitTask."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from volcengine.visual.VisualService import VisualService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JimengVideoAdapter:
|
|
13
|
+
POLL_INTERVAL = 10
|
|
14
|
+
MAX_WAIT = 600
|
|
15
|
+
|
|
16
|
+
def generate(self, prompt, config, **kwargs):
|
|
17
|
+
access_key = config["access_key"]
|
|
18
|
+
secret_key = config["secret_key"]
|
|
19
|
+
req_key = config.get("model", "jimeng_t2v_v30")
|
|
20
|
+
out_dir = kwargs["out_dir"]
|
|
21
|
+
|
|
22
|
+
# duration → frames: 5s=121, 10s=241 (assuming default 5s max logic for now, standard expects 121 for 5s)
|
|
23
|
+
duration = int(kwargs.get("duration", 5))
|
|
24
|
+
frames = 24 * duration + 1
|
|
25
|
+
|
|
26
|
+
aspect_ratio = kwargs.get("aspect_ratio", "16:9")
|
|
27
|
+
|
|
28
|
+
# ═══ Step 1: 提交任务 ═══
|
|
29
|
+
vs = VisualService()
|
|
30
|
+
vs.set_ak(access_key)
|
|
31
|
+
vs.set_sk(secret_key)
|
|
32
|
+
|
|
33
|
+
body = {
|
|
34
|
+
"req_key": req_key,
|
|
35
|
+
"prompt": prompt,
|
|
36
|
+
"seed": -1,
|
|
37
|
+
"frames": frames,
|
|
38
|
+
"aspect_ratio": aspect_ratio,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
print(f"[jimeng_video] 提交文生视频任务 req_key={req_key}...", file=sys.stderr)
|
|
42
|
+
try:
|
|
43
|
+
data = vs.cv_sync2async_submit_task(body)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
# Handle SDK exceptions which wrap the raw response
|
|
46
|
+
if hasattr(e, 'args') and len(e.args) > 0 and isinstance(e.args[0], bytes):
|
|
47
|
+
try:
|
|
48
|
+
err_json = json.loads(e.args[0].decode('utf-8'))
|
|
49
|
+
raise RuntimeError(f"即梦 API 错误: {err_json}")
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
raise RuntimeError(f"即梦提交失败: {str(e)}")
|
|
53
|
+
|
|
54
|
+
if data.get("code") != 10000:
|
|
55
|
+
raise RuntimeError(f"即梦 API 错误: {data.get('message')} (code={data.get('code')})")
|
|
56
|
+
|
|
57
|
+
task_id = data["data"]["task_id"]
|
|
58
|
+
print(f"[jimeng_video] task_id={task_id}, 开始轮询...", file=sys.stderr)
|
|
59
|
+
|
|
60
|
+
# ═══ Step 2: 轮询状态 ═══
|
|
61
|
+
video_url = None
|
|
62
|
+
elapsed = 0
|
|
63
|
+
while elapsed < self.MAX_WAIT:
|
|
64
|
+
time.sleep(self.POLL_INTERVAL)
|
|
65
|
+
elapsed += self.POLL_INTERVAL
|
|
66
|
+
|
|
67
|
+
query_body = {
|
|
68
|
+
"req_key": req_key,
|
|
69
|
+
"task_id": task_id,
|
|
70
|
+
}
|
|
71
|
+
try:
|
|
72
|
+
qdata = vs.cv_sync2async_get_result(query_body)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
# Same exception handling for polling
|
|
75
|
+
if hasattr(e, 'args') and len(e.args) > 0 and isinstance(e.args[0], bytes):
|
|
76
|
+
try:
|
|
77
|
+
err_json = json.loads(e.args[0].decode('utf-8'))
|
|
78
|
+
if err_json.get("code") in (50500, 50501):
|
|
79
|
+
continue # Server error, can retry
|
|
80
|
+
raise RuntimeError(f"即梦查询失败: {err_json}")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
raise RuntimeError(f"即梦查询异常: {str(e)}")
|
|
84
|
+
|
|
85
|
+
if qdata.get("code") != 10000:
|
|
86
|
+
print(f"[jimeng_video] 轮询 {elapsed}s: code={qdata.get('code')} msg={qdata.get('message')}", file=sys.stderr)
|
|
87
|
+
if qdata.get("code") in (50500, 50501):
|
|
88
|
+
continue # 可重试的内部错误
|
|
89
|
+
raise RuntimeError(f"即梦状态获取失败: {qdata.get('message')} (code={qdata.get('code')})")
|
|
90
|
+
|
|
91
|
+
status = qdata.get("data", {}).get("status", "")
|
|
92
|
+
print(f"[jimeng_video] 轮询 {elapsed}s: status={status}", file=sys.stderr)
|
|
93
|
+
|
|
94
|
+
if status == "done":
|
|
95
|
+
video_url = qdata["data"].get("video_url")
|
|
96
|
+
break
|
|
97
|
+
elif status in ("in_queue", "generating"):
|
|
98
|
+
continue
|
|
99
|
+
elif status in ("not_found", "expired"):
|
|
100
|
+
raise RuntimeError(f"即梦任务异常停机: status={status}")
|
|
101
|
+
|
|
102
|
+
if not video_url:
|
|
103
|
+
raise RuntimeError(f"即梦视频生成超时 ({self.MAX_WAIT}s), task_id={task_id}")
|
|
104
|
+
|
|
105
|
+
print(f"[jimeng_video] 生成完成, 下载中...", file=sys.stderr)
|
|
106
|
+
|
|
107
|
+
# ═══ Step 3: 下载视频 ═══
|
|
108
|
+
fname = kwargs.get("output_path") or os.path.join(out_dir, "video.mp4")
|
|
109
|
+
r = requests.get(video_url, timeout=300)
|
|
110
|
+
r.raise_for_status()
|
|
111
|
+
with open(fname, "wb") as f:
|
|
112
|
+
f.write(r.content)
|
|
113
|
+
|
|
114
|
+
print(f"[jimeng_video] 已保存: {fname}", file=sys.stderr)
|
|
115
|
+
return [fname]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""MiniMax video adapter — POST /video_generation, poll status, download via file_id.
|
|
2
|
+
Supports both Text-to-Video and Image-to-Video modes."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MiniMaxVideoAdapter:
|
|
12
|
+
# 轮询间隔和超时
|
|
13
|
+
POLL_INTERVAL = 10 # 每 10 秒查一次
|
|
14
|
+
MAX_WAIT = 600 # 最长等 10 分钟
|
|
15
|
+
|
|
16
|
+
def generate(self, prompt, config, **kwargs):
|
|
17
|
+
base_url = config["base_url"].rstrip("/")
|
|
18
|
+
api_key = config["api_key"]
|
|
19
|
+
model = config["model"]
|
|
20
|
+
out_dir = kwargs["out_dir"]
|
|
21
|
+
|
|
22
|
+
headers = {
|
|
23
|
+
"Authorization": f"Bearer {api_key}",
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# ═══ Step 1: 提交生成任务 ═══
|
|
28
|
+
payload = {
|
|
29
|
+
"model": model,
|
|
30
|
+
}
|
|
31
|
+
# prompt(图生视频时可选,文生视频时必填)
|
|
32
|
+
if prompt:
|
|
33
|
+
payload["prompt"] = prompt
|
|
34
|
+
# 图生视频模式:传入首帧图片
|
|
35
|
+
if kwargs.get("first_frame_image"):
|
|
36
|
+
payload["first_frame_image"] = kwargs["first_frame_image"]
|
|
37
|
+
# 可选参数
|
|
38
|
+
if kwargs.get("duration"):
|
|
39
|
+
payload["duration"] = int(kwargs["duration"])
|
|
40
|
+
if kwargs.get("resolution"):
|
|
41
|
+
payload["resolution"] = kwargs["resolution"]
|
|
42
|
+
if kwargs.get("prompt_optimizer") is not None:
|
|
43
|
+
payload["prompt_optimizer"] = kwargs["prompt_optimizer"]
|
|
44
|
+
|
|
45
|
+
mode = "图生视频" if "first_frame_image" in payload else "文生视频"
|
|
46
|
+
print(f"[minimax_video] 提交{mode}任务 model={model}...", file=sys.stderr)
|
|
47
|
+
resp = requests.post(
|
|
48
|
+
f"{base_url}/video_generation",
|
|
49
|
+
headers=headers,
|
|
50
|
+
json=payload,
|
|
51
|
+
timeout=60,
|
|
52
|
+
)
|
|
53
|
+
resp.raise_for_status()
|
|
54
|
+
data = resp.json()
|
|
55
|
+
|
|
56
|
+
# 检查业务错误
|
|
57
|
+
base_resp = data.get("base_resp", {})
|
|
58
|
+
if base_resp.get("status_code", 0) != 0:
|
|
59
|
+
raise RuntimeError(f"MiniMax error: {base_resp.get('status_msg')}")
|
|
60
|
+
|
|
61
|
+
task_id = data.get("task_id")
|
|
62
|
+
if not task_id:
|
|
63
|
+
raise RuntimeError(f"未返回 task_id: {data}")
|
|
64
|
+
|
|
65
|
+
print(f"[minimax_video] task_id={task_id}, 开始轮询...", file=sys.stderr)
|
|
66
|
+
|
|
67
|
+
# ═══ Step 2: 轮询任务状态 ═══
|
|
68
|
+
file_id = None
|
|
69
|
+
elapsed = 0
|
|
70
|
+
while elapsed < self.MAX_WAIT:
|
|
71
|
+
time.sleep(self.POLL_INTERVAL)
|
|
72
|
+
elapsed += self.POLL_INTERVAL
|
|
73
|
+
|
|
74
|
+
query_resp = requests.get(
|
|
75
|
+
f"{base_url}/query/video_generation",
|
|
76
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
77
|
+
params={"task_id": task_id},
|
|
78
|
+
timeout=30,
|
|
79
|
+
)
|
|
80
|
+
query_resp.raise_for_status()
|
|
81
|
+
qdata = query_resp.json()
|
|
82
|
+
status = qdata.get("status", "")
|
|
83
|
+
print(f"[minimax_video] 轮询 {elapsed}s: status={status}", file=sys.stderr)
|
|
84
|
+
|
|
85
|
+
if status == "Success":
|
|
86
|
+
file_id = qdata.get("file_id")
|
|
87
|
+
break
|
|
88
|
+
elif status == "Fail":
|
|
89
|
+
err_msg = qdata.get("error_message", "未知错误")
|
|
90
|
+
raise RuntimeError(f"视频生成失败: {err_msg}")
|
|
91
|
+
# Processing / Queueing → 继续等
|
|
92
|
+
|
|
93
|
+
if not file_id:
|
|
94
|
+
raise RuntimeError(f"视频生成超时 ({self.MAX_WAIT}s), task_id={task_id}")
|
|
95
|
+
|
|
96
|
+
print(f"[minimax_video] 生成完成 file_id={file_id}, 下载中...", file=sys.stderr)
|
|
97
|
+
|
|
98
|
+
# ═══ Step 3: 获取下载 URL 并下载 ═══
|
|
99
|
+
file_resp = requests.get(
|
|
100
|
+
f"{base_url}/files/retrieve",
|
|
101
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
102
|
+
params={"file_id": file_id},
|
|
103
|
+
timeout=30,
|
|
104
|
+
)
|
|
105
|
+
file_resp.raise_for_status()
|
|
106
|
+
download_url = file_resp.json()["file"]["download_url"]
|
|
107
|
+
|
|
108
|
+
fname = kwargs.get("output_path") or os.path.join(out_dir, "video.mp4")
|
|
109
|
+
r = requests.get(download_url, timeout=300)
|
|
110
|
+
r.raise_for_status()
|
|
111
|
+
with open(fname, "wb") as f:
|
|
112
|
+
f.write(r.content)
|
|
113
|
+
|
|
114
|
+
print(f"[minimax_video] 已保存: {fname}", file=sys.stderr)
|
|
115
|
+
return [fname]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 文生视频入口 (Text-to-Video)
|
|
3
|
+
# 主力模型: MiniMax-Hailuo-2.3 | 降级: 即梦 jimeng_t2v_v30
|
|
4
|
+
# 用法: ./video.sh --output-dir /abs/path --name 中文名 "描述" [--duration 6] [--resolution 768P] [--aspect-ratio 16:9]
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
OUTPUT_DIR=""
|
|
8
|
+
NAME=""
|
|
9
|
+
PROMPT=""
|
|
10
|
+
DURATION=""
|
|
11
|
+
RESOLUTION=""
|
|
12
|
+
ASPECT_RATIO=""
|
|
13
|
+
|
|
14
|
+
while [ $# -gt 0 ]; do
|
|
15
|
+
case "$1" in
|
|
16
|
+
--output-dir) OUTPUT_DIR="$2"; shift 2 ;;
|
|
17
|
+
--name) NAME="$2"; shift 2 ;;
|
|
18
|
+
--duration) DURATION="$2"; shift 2 ;;
|
|
19
|
+
--resolution) RESOLUTION="$2"; shift 2 ;;
|
|
20
|
+
--aspect-ratio) ASPECT_RATIO="$2"; shift 2 ;;
|
|
21
|
+
*)
|
|
22
|
+
if [ -z "$PROMPT" ]; then
|
|
23
|
+
PROMPT="$1"
|
|
24
|
+
fi
|
|
25
|
+
shift
|
|
26
|
+
;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
if [ -z "$OUTPUT_DIR" ] || [ -z "$NAME" ] || [ -z "$PROMPT" ]; then
|
|
31
|
+
echo "用法: ./video.sh --output-dir <绝对路径> --name <中文名> \"视频描述\""
|
|
32
|
+
echo "示例: ./video.sh --output-dir /root/.openclaw/workspace --name 日落延时 \"a sunset timelapse\""
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
37
|
+
|
|
38
|
+
ARGS=()
|
|
39
|
+
ARGS+=("video")
|
|
40
|
+
ARGS+=("$PROMPT")
|
|
41
|
+
ARGS+=(--output-dir "$OUTPUT_DIR")
|
|
42
|
+
ARGS+=(--name "$NAME")
|
|
43
|
+
[ -n "$DURATION" ] && ARGS+=(--duration "$DURATION")
|
|
44
|
+
[ -n "$RESOLUTION" ] && ARGS+=(--resolution "$RESOLUTION")
|
|
45
|
+
[ -n "$ASPECT_RATIO" ] && ARGS+=(--aspect-ratio "$ASPECT_RATIO")
|
|
46
|
+
|
|
47
|
+
python3 "$SCRIPT_DIR/generate.py" "${ARGS[@]}"
|