-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
621 lines (425 loc) · 20.7 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
import { copyFileSync, existsSync, readdirSync, writeFileSync } from 'fs';
import { resolve as resolvePath } from 'path';
import { pathToFileURL } from 'url';
import { readJSONSync } from 'fs-extra/esm';
import { parseFile } from 'music-metadata';
import { intersected } from './lib/array-intersect.lib.js';
import { parseRuncom, readRuncomConfig } from './lib/read-runcom-config.js';
import readDictationLines from './lib/read-raw-lines.js';
import formatEvent from './lib/format-event.js';
import { dirDistExtend, dirProject } from './lib/global.js';
import { linesPublic$event } from './lib/public.js';
const formatLine = line => line
.replace(/\\[n.,,。…、\\]/g, '\n')
.replace(/\\([!?:“”[\]()!?:【】])/g, '$1\n');
/**
* @param {string} string
* @returns {string}
*/
const parsePresetPath = (string, willResolvePath = true) => {
const result = string?.replace(/\$\{.+?\}/g, text => {
try {
// eslint-disable-next-line no-unused-vars
const VA = dirVoicesAll;
// eslint-disable-next-line no-unused-vars
const D = dirDictations;
// eslint-disable-next-line no-unused-vars
const R = resolvePath(dirResources, 'project');
// eslint-disable-next-line no-unused-vars
const RP = dirResourcesProject;
return eval(text.replace(/(^\$\{)|(\}$)/g, ''));
}
catch { return text; }
});
return willResolvePath && result ? resolvePath(result) : result;
};
/** 命令条 */
const runcom = readRuncomConfig();
/** 默认配置 @type {ProjectConfig} */
const configDefault = readJSONSync(resolvePath(dirProject, 'config.project-default.json'), { throws: false });
/** 用户配置 @type {ProjectConfig} */
const configUser = readJSONSync(resolvePath(dirProject, 'config.project-user.json'), { throws: false });
/** 语音总目录 */
const dirVoicesAll = resolvePath(dirProject, configUser.dirVoicesAll || configDefault.dirVoicesAll);
/** 台词总目录 */
const dirDictations = resolvePath(dirProject, configUser.dirDictations || configDefault.dirDictations);
/** 资源总目录 */
const dirResources = resolvePath(dirProject, configUser.dirResources || configDefault.dirResources);
/** 英雄基础信息 */
const fileChampions = resolvePath(dirProject, configUser.fileChampions || configDefault.fileChampions);
/** 英雄皮肤基础数据 */
const champions = readJSONSync(fileChampions);
/** 是否皮肤模式 */
const isSkinMode = runcom.mode == 'cs';
/** 英雄ID,仅皮肤模式 */
const idChampion = isSkinMode ? Number(runcom.slotMain.slice(0, 3)) : null;
/** 皮肤ID,仅皮肤模式 */
const idSkin = isSkinMode ? Number(runcom.slotMain.slice(3, 6)) : null;
/** 英雄ID填充 */
const idChampionPad = isSkinMode ? String(idChampion).padStart(3, '0') : null;
/** 皮肤ID填充 */
const idSkinPad = isSkinMode ? String(idSkin).padStart(3, '0') : null;
/** 英雄基础数据,仅皮肤模式 */
const champion = isSkinMode ? champions[idChampion] : null;
/** 皮肤基础数据,仅皮肤模式 */
const skin = isSkinMode ? champion.skins[idSkin] : null;
/** 主标题 */
let title1 = isSkinMode ? (idSkin == 0 ? champion.title : skin.name.replace(champion.name, '').trim()) : null;
/** 副标题 */
let title2 = isSkinMode ? champion.name : null;
/** 工程资源目录名 */
const nameDirProject = isSkinMode ? `${idChampionPad}-${champion.slot.toLowerCase()}${runcom.slotSub ? `.${runcom.slotSub}` : ''}` : `${runcom.slotSpecial}-${runcom.slot}`;
/** 工程资源目录 */
const dirResourcesProject = resolvePath(dirResources, 'project', nameDirProject);
/** 工程配置 @type {ProjectConfig} */
const configProject = (await import(pathToFileURL(resolvePath(dirResourcesProject, `${isSkinMode ? `${idSkinPad}-` : ''}config.js`)))).default;
/** 默认头像文件 */
const fileHead = configProject.fileHead ? parsePresetPath(configProject.fileHead)
: resolvePath(dirResourcesProject, `${isSkinMode ? `${idSkinPad}-` : ''}header.png`);
/** 默认背景文件 */
const fileBackground = configProject.fileBackground ? parsePresetPath(configProject.fileBackground)
: resolvePath(dirResourcesProject, `${isSkinMode ? `${idSkinPad}-` : ''}splash.jpg`);
/** 默认主背景文件 */
const fileBackgroundMain = configProject.fileBackgroundMain ? parsePresetPath(configProject.fileBackgroundMain)
: resolvePath(dirResourcesProject, `${isSkinMode ? `${idSkinPad}-` : ''}splash-left.png`);
/** 默认主Logo文件 */
const fileLogo = configProject.fileLogo ? resolvePath(dirResourcesProject, configProject.fileLogo)
: configUser.fileLogo ? resolvePath(dirProject, configUser.fileLogo)
: configDefault.fileLogo ? resolvePath(dirProject, configDefault.fileLogo) : null;
/** 默认主Logo文件 */
const fileLogoSide = configProject.fileLogoSide ? resolvePath(dirResourcesProject, configProject.fileLogoSide)
: configUser.fileLogoSide ? resolvePath(dirProject, configUser.fileLogoSide)
: configDefault.fileLogoSide ? resolvePath(dirProject, configDefault.fileLogoSide) : null;
/** 背景音乐文件 */
const fileBGM = configProject.fileBGM ? resolvePath(dirResourcesProject, configProject.fileBGM)
: configUser.fileBGM ? resolvePath(dirProject, configUser.fileBGM)
: configDefault.fileBGM ? resolvePath(dirProject, configDefault.fileBGM) : null;
/** 水印文件 */
const fileWaterMark = configProject.fileWaterMark ? resolvePath(dirProject, configProject.fileWaterMark)
: configUser.fileWaterMark ? resolvePath(dirProject, configUser.fileWaterMark)
: configDefault.fileWaterMark ? resolvePath(dirProject, configDefault.fileWaterMark) : null;
title1 = configProject.title1 ?? title1;
title2 = configProject.title2 ?? title2;
/** 副标题后缀 */
const title2Suffix = configProject.title2Suffix || '';
/** 片尾文本 */
const textEnding = configProject.textEnding || configUser.textEnding || configDefault.textEnding || '';
// 台词文件
const fileDictation = parsePresetPath(configProject.fileDictation) || resolvePath(dirDictations,
readdirSync(dirDictations).find(file => file.includes(runcom.slot) && file.includes('@zh-cn') && !file.includes('.bak')));
// 语音目录
const dirVoices = configProject.dirVoices || resolvePath(dirVoicesAll,
readdirSync(dirVoicesAll).find(dir => dir.includes(runcom.slot) && dir.includes('@zh')));
/** 视频参数配置集 @type {Object<string, VideoConfig>} */
const profilesVideo = readJSONSync(resolvePath(dirProject, 'config.video-default.json'), { throws: false });
/** 视频参数配置 @type {VideoConfig} */
const configVideo = Object.assign({},
profilesVideo.$,
profilesVideo[configProject.profileVideo || configUser.profileVideo || configDefault.profileVideo],
configUser,
configProject,
);
const lineWhoDefault = Object.assign({ head: fileHead }, configProject.lines$who?.$);
/** 台词角色集 */
const lines$who = Object.assign({}, configProject.lines$who, { $: lineWhoDefault });
// 指定与排除台词
const idsLineInclude = configProject.idsLineInclude?.length ? configProject.idsLineInclude : null;
const idsLineExclude = configProject.idsLineExclude?.length ? configProject.idsLineExclude : null;
const linesDictation = (fileDictation ? readDictationLines(fileDictation) : [])
.filter(lineDictation => {
if(idsLineExclude
&& (idsLineExclude.includes(lineDictation.idAudio)
|| intersected(idsLineExclude, lineDictation.idsSound ?? []))
) { return false; }
return idsLineInclude
? (idsLineInclude.includes(lineDictation.idAudio) || intersected(idsLineInclude, lineDictation.idsSound ?? []))
: true;
});
/**
* @param {DictationLineConfig} lineDictation
* @param {string} [from]
* @returns {LineConfig}
*/
const parseDictaionLineConfig = (lineDictation, from) => {
const extras = lineDictation.extras;
/** @type {LineConfig} */
const line = {
order: null,
orderRanged: undefined,
ids: [lineDictation.idAudio, ...lineDictation.idsSound].filter(id => id).join('|'),
event: lineDictation.eventsRaw.map(eventRaw => formatEvent(eventRaw)).join('、'),
caption: lineDictation.caption,
mark: extras.mark?.[0] ?? null,
cond: extras.cond?.[0] ?? null,
duration: 0,
audio: null,
color: null,
colorTile: null,
head: null,
target: null,
skill: null,
side: extras.side?.[0] ?? 'right',
dialog: undefined,
from,
lineDictation,
};
if(extras.head) { line.head = '${R}' + `/${extras.head.join('/')}.png`; }
if(extras.target) { line.target = '${R}' + `/${extras.target.join('/')}.png`; }
if(extras.skill) {
if(isSkinMode) {
line.skill = '${RP}' + `/skill-${extras.skill[0]}.png`;
}
else {
line.skill = '${R}' + `/${extras.skill.join('/')}.png`;
}
}
return line;
};
/** 额外台词集 @type {LineConfig[]} */
const linesExtra = configProject.linesExtra?.map(lineExtra => parseDictaionLineConfig(lineExtra, 'project')) ?? [];
if(runcom.slotsExtra?.length) {
for(const slotExtra of runcom.slotsExtra) {
const configExtra = configProject.configsExtra[slotExtra];
if(!configExtra) { continue; }
linesExtra.push(...readDictationLines(parsePresetPath(configExtra.fileDictation))
.map(lineDictation => parseDictaionLineConfig(lineDictation, slotExtra))
);
}
}
/** @type {LineConfig[]} */
let linesFinal = [];
for(const lineDictation of linesDictation) {
const extras = lineDictation.extras;
// 优先级0:台词文件自身及其额外指令
const line = parseDictaionLineConfig(lineDictation);
linesFinal.push(line);
line.order = linesFinal.length;
if(extras.before) {
for(const idBefore of extras.before) {
const lineBefore = linesExtra.find(lineDictationExtra => lineDictationExtra.idAudio == idBefore || lineDictationExtra.idsSound.includes(idBefore));
if(!lineBefore) { continue; }
linesFinal.splice(linesFinal.length - 1, 0, lineBefore);
lineBefore.order = linesFinal.length;
}
}
if(extras.after) {
for(const idAfter of extras.after) {
const lineAfter = linesExtra.find(lineDictationExtra => lineDictationExtra.idAudio == idAfter || lineDictationExtra.idsSound.includes(idAfter));
if(!lineAfter) { continue; }
linesFinal.push(lineAfter);
lineAfter.order = linesFinal.length;
}
}
}
// 范围限制
const range = configProject.range || configUser.range || configDefault.range || null;
if(range && range[1] >= range[0]) {
linesFinal.splice(0, range[0] - 1);
linesFinal.splice(range[1] - range[0] + 1, linesFinal.length);
for(let order = 0; order < linesFinal.length; order++) {
linesFinal[order].orderRanged = order + 1;
}
}
// 对话预处理
const linesDialog$id = {};
/** 对话成员 */
const whosDialogist$slot = Object.assign({ A: '$' }, configProject.dialogists);
const whosDialogist = [whosDialogist$slot.A, whosDialogist$slot.B, whosDialogist$slot.C, whosDialogist$slot.D].filter(s => s);
for(let indexDialog = 1; indexDialog <= configProject.dialogs?.length ?? 0; indexDialog++) {
const dialog = configProject.dialogs[indexDialog - 1];
const textIndexDialog = String(indexDialog).padStart(2, '0');
let [rawRevs, rawPlayers, rawTargets] = dialog.trim().split('|');
// 台词顺序
const orderIDLine = rawRevs.split('>');
// 演员顺序
const orderWhoPlayer = (rawPlayers = rawPlayers || 'A').split('').map(slot => whosDialogist$slot[slot] ?? slot);
// 目标顺序
const orderWhoTarget = (rawTargets || (rawPlayers == 'A' ? 'B' : '')).split('').map(slot => whosDialogist$slot[slot] ?? slot);
// 补全顺序
const indexPlayerRawLast = whosDialogist.indexOf(orderWhoPlayer[orderWhoPlayer.length - 1]);
const lengthPlayerRaw = orderWhoPlayer.length;
while(orderWhoPlayer.length < orderIDLine.length) {
orderWhoPlayer.push(whosDialogist[
(orderWhoPlayer.length + 1 - lengthPlayerRaw + indexPlayerRawLast) % whosDialogist.length
]);
}
const indexTargetRawLast = whosDialogist.indexOf(orderWhoTarget[orderWhoTarget.length - 1]);
const lengthTargetRaw = orderWhoTarget.length;
while(whosDialogist.length == 2 && orderWhoTarget.length < orderIDLine.length) {
orderWhoTarget.push(whosDialogist[
(orderWhoTarget.length + 1 - lengthTargetRaw + indexTargetRawLast) % whosDialogist.length
]);
}
const alignOrder = Math.max(...whosDialogist.map(who => who.length), 8);
globalThis.console.log(`对话${textIndexDialog} [${dialog.trim()}]\n`,
`\t台词: ${orderIDLine.map(order => order.padStart(alignOrder, ' ')).join(' => ')}\n`,
`\t角色: ${orderWhoPlayer.map(order => order.padStart(alignOrder, ' ')).join(' => ')}\n`,
`\t目标: ${orderWhoTarget.map(order => order.padStart(alignOrder, ' ')).join(' => ')}\n`,
`\t台词: ${orderWhoTarget.map(order => order.padStart(alignOrder, ' ')).join(' => ')}\n`,
`${orderIDLine.map((order, index) => `\t ${index}: ${linesFinal.find(line => line.ids.includes(order))?.caption ?? '(空台词?)'}`).join('\n')}\n`,
);
for(let indexRev = 1; indexRev <= orderIDLine.length; indexRev++) {
const idLine = orderIDLine[indexRev - 1];
const whoPlayer = orderWhoPlayer[indexRev - 1];
const whoTarget = orderWhoTarget[indexRev - 1];
const linePlayer = lines$who[whoPlayer];
const lineTarget = lines$who[whoTarget];
/** @type {LineConfig} */
const lineDialog = {};
if(indexRev == 1) { lineDialog.mark = `对话${textIndexDialog} 开始`; }
else if(indexRev == orderIDLine.length) { lineDialog.mark = `对话${textIndexDialog} 结束`; }
if(indexRev > 1) { lineDialog.event = formatEvent(`[标题:对话${textIndexDialog}-${indexRev}]`); }
if(linePlayer) { Object.assign(lineDialog, linePlayer); }
if(lineTarget) { lineDialog.target = lineTarget.head; }
linesDialog$id[idLine] = Object.assign(lineDialog, { dialog: `${textIndexDialog}-${indexRev}` });
}
}
// 读取目录文件列表换成
/** @type {Object<string,string[]>} */
const filesAudio$dirVoices = {};
const slotsEventTrans$event = { '选用': 'pick', '禁用': 'ban' };
const colorsLine$color = new Set();
for(const line of linesFinal) {
/** @type {DictationLineConfig} */
const lineDictation = line.lineDictation;
// 优先级1:公共事件匹配
for(const eventRaw of lineDictation.eventsRaw) {
const lineEventMatched = linesPublic$event[eventRaw];
if(!lineEventMatched) { continue; }
Object.assign(line, lineEventMatched);
};
// 优先级2:工程公共资源
Object.assign(line, {
color: configProject.color ?? null,
colorTile: configProject.colorTile ?? null,
head: fileHead ?? null,
});
// 优先级3:工程事件匹配
for(const eventRaw of lineDictation.eventsRaw) {
const lineEventMatched = configProject.lines$event?.[eventRaw];
if(!lineEventMatched) { continue; }
Object.assign(line, lineEventMatched);
};
// 优先级4:工程角色匹配
const lineWho = lines$who[lineDictation.extras.who?.[0] ?? '$'];
Object.assign(line, lineWho);
// 优先级5:对话匹配
const linesIDDialog = [lineDictation.idAudio, ...lineDictation.idsSound].map(idSound => linesDialog$id[idSound]).filter(l => l);
for(const lineID of linesIDDialog) { Object.assign(line, lineID); }
// 优先级6:工程台词ID匹配
const linesID = [lineDictation.idAudio, ...lineDictation.idsSound].map(idSound => configProject.lines$id?.[idSound]).filter(l => l);
for(const lineID of linesID) { Object.assign(line, lineID); }
// 格式化台词
line.caption = formatLine(line.caption);
if(line.cond) { line.cond = `子条件:${line.cond}`; line.cond = formatLine(line.cond); }
if(line.mark) { line.mark = formatLine(line.mark); }
if(line.head) { line.head = parsePresetPath(line.head); }
if(line.target) { line.target = parsePresetPath(line.target); }
if(line.skill) { line.skill = parsePresetPath(line.skill); }
// 处理音频和读取时长
const dirVoicesLine = parsePresetPath(line.dirVoices ?? (line.from && line.from != 'project' ? configProject.configsExtra[line.from]?.dirVoices : dirVoices));
if(!line.audio && dirVoicesLine) {
const nameFileMatch = (filesAudio$dirVoices[dirVoicesLine] || (filesAudio$dirVoices[dirVoicesLine] = readdirSync(dirVoicesLine)))
.find(name => name.includes(line.nameAudio) || line.ids?.split('|').reduce((acc, id) => acc + name.includes(id), 0) > 0);
if(nameFileMatch) { line.audio = resolvePath(dirVoicesLine, nameFileMatch); }
}
if(!line.audio && (line.event == '选用' || line.event == '禁用' || line.event == '选用、禁用')) {
line.audio = resolvePath(dirResourcesProject, `voice-${slotsEventTrans$event[line.event] ?? 'pick'}.wav`);
}
line.audio = parsePresetPath(line.audio ?? '');
if(existsSync(line.audio)) { line.duration = (await parseFile(line.audio)).format.duration; }
colorsLine$color.add(line.color);
colorsLine$color.add(line.colorTile);
// 移除听写台词
delete line.lineDictation;
}
const onlyMark = configProject.onlyMark || configUser.onlyMark || configDefault.onlyMark || false;
if(onlyMark) { linesFinal = linesFinal.filter(line => line.mark); }
const onlyDialog = configProject.onlyDialog || configUser.onlyDialog || configDefault.onlyDialog || false;
if(onlyDialog) { linesFinal = linesFinal.filter(line => line.dialog); }
// 片头背景预处理
const infosSplashOpener = [];
const offsetsSplashOpener$slot = readJSONSync(resolvePath(dirResources, 'opener-splash-offset.json'), { throws: false }) ?? {};
for(const rawRuncom of runcom.runcoms) {
const runcomOld = parseRuncom(rawRuncom);
if(runcomOld.mode != 'cs') { continue; }
const idChampionOld = Number(runcomOld.slotMain.slice(0, 3));
const idSkinOld = Number(runcomOld.slotMain.slice(3, 6));
const championOld = champions[idChampionOld];
const nameDirProjectOld = `${String(idChampionOld).padStart(3, '0')}-${championOld.slot.toLowerCase()}${runcom.slotSub ? `.${runcom.slotSub}` : ''}`;
infosSplashOpener.push({
file: resolvePath(dirResources, 'project', nameDirProjectOld, `${String(idSkinOld).padStart(3, '0')}-splash.jpg`),
offset: offsetsSplashOpener$slot[runcomOld.slot] ?? 0,
});
if(infosSplashOpener.length >= 8) { break; }
}
// 全局注释
const marksGlobal = configProject.marksGlobal ?? [];
for(const markGlobal of marksGlobal) {
markGlobal.text = formatLine(markGlobal.text);
}
/** @type {FinalProjectInfo} */
const infoProjectFinal = {
fileBackground,
fileBackgroundMain,
fileLogo,
fileLogoSide,
fileBGM,
fileWaterMark,
widthVideo: configVideo.widthVideo,
heightVideo: configVideo.heightVideo,
landscape: configVideo.widthVideo > configVideo.heightVideo,
pixelAspect: configVideo.pixelAspect,
frameRate: configVideo.frameRate,
durationInterval: configVideo.durationInterval,
durationOpener: configVideo.durationOpener,
durationEnding: configVideo.durationEnding,
durationTitle: configVideo.durationTitle,
durationExtendLine: configVideo.durationExtendLine,
sizeFontLine: configVideo.sizeFontLine,
sizeFontCond: configVideo.sizeFontCond,
sizeFontMark: configVideo.sizeFontMark,
paddingLine: configVideo.paddingLine,
paddingCond: configVideo.paddingCond,
paddingMark: configVideo.paddingMark,
paddingTopExtra$event: configVideo.paddingTopExtra$event,
sizeBoxHeader: configVideo.sizeBoxHeader,
heightLeading: configVideo.heightLeading,
gapLive: configVideo.gapLive,
gapBoxLive: configVideo.gapBoxLive,
paddingSideVideo: configVideo.paddingSideVideo,
paddingBottomVideo: configVideo.paddingBottomVideo,
simple: configProject.simple || configUser.simple || configDefault.simple || false,
mute: configProject.mute || configUser.mute || configDefault.mute || false,
title1,
title2,
title2Suffix,
textEnding,
titleComp: `${runcom.slotSpecial ? `${runcom.slotSpecial}-` : ''}${runcom.slot} ${!isSkinMode ? '特别篇' : (idSkin > 0 ? '新皮肤' : '新英雄')}:${title1} ${title2}${title2Suffix || ''}${configVideo.widthVideo > configVideo.heightVideo ? '' : ' (竖屏)'}`,
colorsLine: [...colorsLine$color].filter(c => c),
lines: linesFinal,
infosSplashOpener: infosSplashOpener,
marksGlobal,
};
const stringInfoProjectFinal = JSON.stringify(infoProjectFinal, null, '\t');
// 检查缺失文件
const filesNeed = new Set(stringInfoProjectFinal.match(/(?<=(: |\t)")([A-Z]:(\\\\|\/).+?)(?=")/ig));
const filesLack = [...filesNeed].filter(file => !existsSync(file)).sort();
if(filesLack.length) {
globalThis.console.log(`以下工程所需文件不存在:\n${filesLack.map(file => `\t${file}`).join('\n')}`);
process.exit(1);
}
// 计算时长
let durationAll = 0;
for(const line of infoProjectFinal.lines) {
durationAll += (line.duration ?? 0) + infoProjectFinal.durationInterval;
}
durationAll += (infoProjectFinal.durationOpener) + (infoProjectFinal.durationEnding + 4.5);
globalThis.console.log(`总台词:${infoProjectFinal.lines.length}个`);
globalThis.console.log(`总时长:${durationAll.toFixed(1)}秒 (${~~(durationAll / 60)}分${~~(durationAll - ~~(durationAll / 60) * 60)}秒)`);
const fileInfo = resolvePath(dirResources, 'info', `${runcom.slot}.json`);
const fileInfoDist = resolvePath(dirDistExtend, 'info.json');
writeFileSync(fileInfo, stringInfoProjectFinal);
copyFileSync(fileInfo, fileInfoDist);
writeFileSync(resolvePath(dirDistExtend, 'paths.js'), `this.PATH_INFO = '${fileInfoDist.replace(/\\/g, '\\\\')}';`);
globalThis.console.log(`已更新 ${infoProjectFinal.titleComp}`);