koishi-plugin-ai-video 0.0.2 → 0.0.3
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/lib/index.js +117 -63
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -39,12 +39,12 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
39
39
|
}).description('基本设置'),
|
|
40
40
|
koishi_1.Schema.object({
|
|
41
41
|
proxyEnabled: koishi_1.Schema.boolean().default(false).description('启用代理'),
|
|
42
|
-
proxyProtocol: koishi_1.Schema.union([koishi_1.Schema.const('http'), koishi_1.Schema.const('https')]).default('http'),
|
|
43
|
-
proxyHost: koishi_1.Schema.string().default(''),
|
|
44
|
-
proxyPort: koishi_1.Schema.number().default(8080),
|
|
45
|
-
proxyAuth: koishi_1.Schema.boolean().default(false),
|
|
46
|
-
proxyUsername: koishi_1.Schema.string().default(''),
|
|
47
|
-
proxyPassword: koishi_1.Schema.string().role('secret').default(''),
|
|
42
|
+
proxyProtocol: koishi_1.Schema.union([koishi_1.Schema.const('http'), koishi_1.Schema.const('https')]).default('http').description('代理协议'),
|
|
43
|
+
proxyHost: koishi_1.Schema.string().default('').description('代理主机地址'),
|
|
44
|
+
proxyPort: koishi_1.Schema.number().default(8080).description('代理端口'),
|
|
45
|
+
proxyAuth: koishi_1.Schema.boolean().default(false).description('启用代理认证'),
|
|
46
|
+
proxyUsername: koishi_1.Schema.string().default('').description('代理用户名'),
|
|
47
|
+
proxyPassword: koishi_1.Schema.string().role('secret').default('').description('代理密码'),
|
|
48
48
|
}).description('代理设置'),
|
|
49
49
|
koishi_1.Schema.object({
|
|
50
50
|
useCustomApi: koishi_1.Schema.boolean().default(false).description('使用自定义API'),
|
|
@@ -52,26 +52,30 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
52
52
|
apiKey: koishi_1.Schema.string().role('secret').default('').description('API密钥'),
|
|
53
53
|
model: koishi_1.Schema.string().default('agnes-video-v2.0').description('模型'),
|
|
54
54
|
img2videoModel: koishi_1.Schema.string().default('').description('图生视频模型'),
|
|
55
|
-
videoDuration: koishi_1.Schema.number().default(0).description('
|
|
56
|
-
videoResolution: koishi_1.Schema.string().default('').description('
|
|
55
|
+
videoDuration: koishi_1.Schema.number().default(0).description('默认视频时长(秒)'),
|
|
56
|
+
videoResolution: koishi_1.Schema.string().default('').description('默认视频分辨率(宽x高)'),
|
|
57
57
|
txt2videoPrompt: koishi_1.Schema.string().default('').description('文生视频提示模板'),
|
|
58
58
|
img2videoPrompt: koishi_1.Schema.string().default('').description('图生视频提示模板'),
|
|
59
|
-
customHeaders: koishi_1.Schema.string().role('textarea').default('{}').description('自定义请求头'),
|
|
59
|
+
customHeaders: koishi_1.Schema.string().role('textarea').default('{}').description('自定义请求头(JSON)'),
|
|
60
60
|
}).description('内置API'),
|
|
61
61
|
koishi_1.Schema.object({
|
|
62
|
-
apiStrategy: koishi_1.Schema.union([
|
|
62
|
+
apiStrategy: koishi_1.Schema.union([
|
|
63
|
+
koishi_1.Schema.const('sequence').description('顺序使用'),
|
|
64
|
+
koishi_1.Schema.const('roundrobin').description('轮询使用')
|
|
65
|
+
]).default('roundrobin').description('API调用策略'),
|
|
63
66
|
customApiList: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
64
|
-
enable: koishi_1.Schema.boolean().default(true),
|
|
65
|
-
endpoint: koishi_1.Schema.string().default('https://apihub.agnes-ai.com/v1/videos'),
|
|
66
|
-
apiKey: koishi_1.Schema.string().role('secret').default(''),
|
|
67
|
-
model: koishi_1.Schema.string().default('agnes-video-v2.0'),
|
|
68
|
-
img2videoModel: koishi_1.Schema.string().default(''),
|
|
69
|
-
videoDuration: koishi_1.Schema.number().default(0),
|
|
70
|
-
videoResolution: koishi_1.Schema.string().default(''),
|
|
71
|
-
txt2videoPrompt: koishi_1.Schema.string().default(''),
|
|
72
|
-
img2videoPrompt: koishi_1.Schema.string().default(''),
|
|
67
|
+
enable: koishi_1.Schema.boolean().default(true).description('启用'),
|
|
68
|
+
endpoint: koishi_1.Schema.string().default('https://apihub.agnes-ai.com/v1/videos').description('API端点'),
|
|
69
|
+
apiKey: koishi_1.Schema.string().role('secret').default('').description('API密钥'),
|
|
70
|
+
model: koishi_1.Schema.string().default('agnes-video-v2.0').description('模型'),
|
|
71
|
+
img2videoModel: koishi_1.Schema.string().default('').description('图生视频模型'),
|
|
72
|
+
videoDuration: koishi_1.Schema.number().default(0).description('默认视频时长(秒)'),
|
|
73
|
+
videoResolution: koishi_1.Schema.string().default('').description('默认视频分辨率(宽x高)'),
|
|
74
|
+
txt2videoPrompt: koishi_1.Schema.string().default('').description('文生视频提示模板'),
|
|
75
|
+
img2videoPrompt: koishi_1.Schema.string().default('').description('图生视频提示模板'),
|
|
73
76
|
customHeaders: koishi_1.Schema.string().role('textarea')
|
|
74
|
-
.default('{"Authorization":"Bearer {apiKey}","Content-Type":"application/json"}')
|
|
77
|
+
.default('{"Authorization":"Bearer {apiKey}","Content-Type":"application/json"}')
|
|
78
|
+
.description('自定义请求头(JSON)'),
|
|
75
79
|
bodyTemplate: koishi_1.Schema.string().role('textarea')
|
|
76
80
|
.default(JSON.stringify({
|
|
77
81
|
txt2videoBody: { model: '{model}', prompt: '{prompt}', height: 768, width: 1152, num_frames: 121, frame_rate: 24 },
|
|
@@ -80,11 +84,11 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
80
84
|
pollUrlTemplate: 'https://apihub.agnes-ai.com/agnesapi?video_id={task_id}',
|
|
81
85
|
taskIdPath: 'video_id',
|
|
82
86
|
}, null, 2))
|
|
83
|
-
.description('自定义请求体'),
|
|
87
|
+
.description('自定义请求体(JSON模板)'),
|
|
84
88
|
})).default([]).description('自定义API列表'),
|
|
85
89
|
}).description('自定义API配置'),
|
|
86
90
|
koishi_1.Schema.object({
|
|
87
|
-
blacklistAdmins: koishi_1.Schema.array(String).default([]).description('管理员QQ'),
|
|
91
|
+
blacklistAdmins: koishi_1.Schema.array(String).default([]).description('管理员QQ号'),
|
|
88
92
|
}).description('权限管理'),
|
|
89
93
|
koishi_1.Schema.object({
|
|
90
94
|
messages: koishi_1.Schema.object({
|
|
@@ -147,8 +151,6 @@ async function apply(ctx, cfg) {
|
|
|
147
151
|
return apiCallTimestamps.length + 1 <= cfg.rateLimit;
|
|
148
152
|
}
|
|
149
153
|
function recordApiCall() { apiCallTimestamps.push(Date.now()); }
|
|
150
|
-
const BUILTIN_TXT2VIDEO = { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' };
|
|
151
|
-
const BUILTIN_IMG2VIDEO = { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' };
|
|
152
154
|
function parseApiEntry(entry) {
|
|
153
155
|
if (!entry.endpoint)
|
|
154
156
|
return null;
|
|
@@ -167,8 +169,8 @@ async function apply(ctx, cfg) {
|
|
|
167
169
|
if (entry.bodyTemplate) {
|
|
168
170
|
try {
|
|
169
171
|
const tmpl = JSON.parse(entry.bodyTemplate);
|
|
170
|
-
txt2videoBody = tmpl.txt2videoBody ||
|
|
171
|
-
img2videoBody = tmpl.img2videoBody ||
|
|
172
|
+
txt2videoBody = tmpl.txt2videoBody || { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' };
|
|
173
|
+
img2videoBody = tmpl.img2videoBody || { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' };
|
|
172
174
|
responseVideoPath = tmpl.responseVideoPath || 'video_url';
|
|
173
175
|
pollUrlTemplate = tmpl.pollUrlTemplate || '';
|
|
174
176
|
taskIdPath = tmpl.taskIdPath || '';
|
|
@@ -178,8 +180,8 @@ async function apply(ctx, cfg) {
|
|
|
178
180
|
}
|
|
179
181
|
}
|
|
180
182
|
else {
|
|
181
|
-
txt2videoBody =
|
|
182
|
-
img2videoBody =
|
|
183
|
+
txt2videoBody = { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' };
|
|
184
|
+
img2videoBody = { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' };
|
|
183
185
|
responseVideoPath = 'video_url';
|
|
184
186
|
pollUrlTemplate = '';
|
|
185
187
|
taskIdPath = '';
|
|
@@ -215,8 +217,8 @@ async function apply(ctx, cfg) {
|
|
|
215
217
|
return {
|
|
216
218
|
endpoint: cfg.apiEndpoint || 'https://apihub.agnes-ai.com/v1/videos',
|
|
217
219
|
headers,
|
|
218
|
-
txt2videoBody:
|
|
219
|
-
img2videoBody:
|
|
220
|
+
txt2videoBody: { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' },
|
|
221
|
+
img2videoBody: { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' },
|
|
220
222
|
responseVideoPath: 'video_url',
|
|
221
223
|
pollUrlTemplate: '',
|
|
222
224
|
taskIdPath: '',
|
|
@@ -249,15 +251,27 @@ async function apply(ctx, cfg) {
|
|
|
249
251
|
return buildBuiltinApi();
|
|
250
252
|
}
|
|
251
253
|
}
|
|
252
|
-
function resolveTemplate(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
254
|
+
function resolveTemplate(templateObj, vars) {
|
|
255
|
+
if (templateObj === null || templateObj === undefined)
|
|
256
|
+
return templateObj;
|
|
257
|
+
if (typeof templateObj === 'string') {
|
|
258
|
+
const match = templateObj.match(/^\{(\w+)\}$/);
|
|
259
|
+
if (match && vars.hasOwnProperty(match[1])) {
|
|
260
|
+
return vars[match[1]];
|
|
261
|
+
}
|
|
262
|
+
return templateObj;
|
|
263
|
+
}
|
|
264
|
+
if (Array.isArray(templateObj)) {
|
|
265
|
+
return templateObj.map(item => resolveTemplate(item, vars));
|
|
266
|
+
}
|
|
267
|
+
if (typeof templateObj === 'object') {
|
|
268
|
+
const result = {};
|
|
269
|
+
for (const key of Object.keys(templateObj)) {
|
|
270
|
+
result[key] = resolveTemplate(templateObj[key], vars);
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
return templateObj;
|
|
261
275
|
}
|
|
262
276
|
function getValueByPath(obj, path) {
|
|
263
277
|
if (!obj || !path)
|
|
@@ -333,7 +347,7 @@ async function apply(ctx, cfg) {
|
|
|
333
347
|
logger.error('发送失败', e);
|
|
334
348
|
}
|
|
335
349
|
}
|
|
336
|
-
async function
|
|
350
|
+
async function sendSingleVideo(session, url) {
|
|
337
351
|
const mode = cfg.videoSendMode;
|
|
338
352
|
if (mode === 'url')
|
|
339
353
|
await safeSend(session, url);
|
|
@@ -366,7 +380,12 @@ async function apply(ctx, cfg) {
|
|
|
366
380
|
if (promptTemplate)
|
|
367
381
|
finalPrompt = promptTemplate.replace('{prompt}', prompt).replace('{url}', imageUrl);
|
|
368
382
|
const bodyTemplate = isImg2Video ? api.img2videoBody : api.txt2videoBody;
|
|
369
|
-
const vars = {
|
|
383
|
+
const vars = {
|
|
384
|
+
model,
|
|
385
|
+
prompt: finalPrompt,
|
|
386
|
+
duration,
|
|
387
|
+
size: resolution,
|
|
388
|
+
};
|
|
370
389
|
if (isImg2Video)
|
|
371
390
|
vars.url = imageUrl;
|
|
372
391
|
let body;
|
|
@@ -374,10 +393,13 @@ async function apply(ctx, cfg) {
|
|
|
374
393
|
body = resolveTemplate(bodyTemplate, vars);
|
|
375
394
|
}
|
|
376
395
|
catch {
|
|
377
|
-
|
|
396
|
+
await safeSend(session, cfg.messages.fail + cfg.messages.templateError);
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
if (!validateEndpoint(api.endpoint)) {
|
|
400
|
+
await safeSend(session, cfg.messages.fail + '(端点无效)');
|
|
401
|
+
return null;
|
|
378
402
|
}
|
|
379
|
-
if (!validateEndpoint(api.endpoint))
|
|
380
|
-
return safeSend(session, cfg.messages.fail + '(端点无效)');
|
|
381
403
|
const sensitive = api.headers?.Authorization?.split(' ')[1] || '';
|
|
382
404
|
if (debug)
|
|
383
405
|
logger.info('API请求', JSON.stringify(sanitizeForLog(body, sensitive)));
|
|
@@ -393,32 +415,62 @@ async function apply(ctx, cfg) {
|
|
|
393
415
|
if (cfg.pollEnabled && api.taskIdPath) {
|
|
394
416
|
const taskId = getValueByPath(res, api.taskIdPath);
|
|
395
417
|
if (taskId) {
|
|
396
|
-
const pollUrl = api.pollUrlTemplate.replace('{
|
|
397
|
-
safeSend(session, cfg.messages.pollWaiting);
|
|
418
|
+
const pollUrl = api.pollUrlTemplate.replace('{task_id}', taskId);
|
|
419
|
+
await safeSend(session, cfg.messages.pollWaiting);
|
|
398
420
|
res = await pollForResult(pollUrl, api.headers, cfg.pollInterval, cfg.pollTimeout);
|
|
399
421
|
}
|
|
400
422
|
}
|
|
401
423
|
const videoUrl = getValueByPath(res, api.responseVideoPath) || findVideoUrl(res);
|
|
402
|
-
|
|
403
|
-
await sendVideo(session, videoUrl);
|
|
404
|
-
else
|
|
405
|
-
await safeSend(session, cfg.messages.fail + cfg.messages.noContent);
|
|
406
|
-
const userId = `${session.guildId || 'private'}-${session.userId}`;
|
|
407
|
-
lastTaskMap.set(userId, { prompt, imageUrl, isImg2Video, model });
|
|
424
|
+
return videoUrl || null;
|
|
408
425
|
}
|
|
409
426
|
catch (err) {
|
|
410
427
|
logger.error('视频生成失败', err);
|
|
411
|
-
safeSend(session, cfg.messages.fail + ' [' + (err.message?.slice(0, 100) || '未知错误') + ']');
|
|
428
|
+
await safeSend(session, cfg.messages.fail + ' [' + (err.message?.slice(0, 100) || '未知错误') + ']');
|
|
429
|
+
return null;
|
|
412
430
|
}
|
|
413
431
|
}
|
|
414
|
-
async function
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
432
|
+
async function generateVideos(session, prompt, imageUrl, count) {
|
|
433
|
+
const videoUrls = [];
|
|
434
|
+
for (let i = 0; i < count; i++) {
|
|
435
|
+
if (!checkRateLimit()) {
|
|
436
|
+
await safeSend(session, cfg.messages.rateLimit);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
const api = getApi();
|
|
440
|
+
if (!api) {
|
|
441
|
+
await safeSend(session, cfg.messages.noApi);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
recordApiCall();
|
|
445
|
+
const url = await customGenerateVideo(session, api, prompt, imageUrl);
|
|
446
|
+
if (url)
|
|
447
|
+
videoUrls.push(url);
|
|
448
|
+
if (i < count - 1)
|
|
449
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
450
|
+
}
|
|
451
|
+
if (videoUrls.length === 0) {
|
|
452
|
+
await safeSend(session, cfg.messages.fail + cfg.messages.noContent);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (videoUrls.length === 1 || !cfg.enableForward) {
|
|
456
|
+
for (const url of videoUrls) {
|
|
457
|
+
await sendSingleVideo(session, url);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
const children = videoUrls.map(url => (0, koishi_1.h)('message', koishi_1.h.video(url)));
|
|
462
|
+
try {
|
|
463
|
+
await safeSend(session, (0, koishi_1.h)('message', { forward: true }, ...children));
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
for (const url of videoUrls)
|
|
467
|
+
await sendSingleVideo(session, url);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const userId = `${session.guildId || 'private'}-${session.userId}`;
|
|
471
|
+
if (videoUrls.length > 0) {
|
|
472
|
+
lastTaskMap.set(userId, { prompt, imageUrl, isImg2Video: !!imageUrl, model: 'multiple' });
|
|
473
|
+
}
|
|
422
474
|
}
|
|
423
475
|
function startTimer(session, key, collect) {
|
|
424
476
|
return setTimeout(() => {
|
|
@@ -459,6 +511,8 @@ async function apply(ctx, cfg) {
|
|
|
459
511
|
await safeSend(session, cfg.messages.enterCollect);
|
|
460
512
|
});
|
|
461
513
|
ctx.on('message', async (session) => {
|
|
514
|
+
if (session.command)
|
|
515
|
+
return;
|
|
462
516
|
if (!session || !session.elements)
|
|
463
517
|
return;
|
|
464
518
|
const key = `${session.guildId || 'private'}-${session.userId}`;
|
|
@@ -478,7 +532,7 @@ async function apply(ctx, cfg) {
|
|
|
478
532
|
if (!collect.prompt && !collect.imageUrl)
|
|
479
533
|
return safeSend(session, cfg.messages.empty);
|
|
480
534
|
await safeSend(session, cfg.messages.generating);
|
|
481
|
-
return
|
|
535
|
+
return generateVideos(session, collect.prompt || '默认', collect.imageUrl, cfg.maxVideos);
|
|
482
536
|
}
|
|
483
537
|
if (imgs.length > 0 && !collect.imageUrl) {
|
|
484
538
|
const assets = ctx.assets;
|
|
@@ -515,7 +569,7 @@ async function apply(ctx, cfg) {
|
|
|
515
569
|
if (last.isImg2Video)
|
|
516
570
|
return safeSend(session, cfg.messages.redrawImg2Video);
|
|
517
571
|
await safeSend(session, cfg.messages.redrawing);
|
|
518
|
-
return
|
|
572
|
+
return generateVideos(session, last.prompt, '', cfg.maxVideos);
|
|
519
573
|
});
|
|
520
574
|
function isValidQQ(id) { return /^\d{5,11}$/.test(id); }
|
|
521
575
|
async function isBlacklisted(userId) {
|