@biunow/agent-biuvibe 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/server.js +22 -3
- package/lib/host.mjs +94 -12
- package/manifest.json +3 -3
- package/package.json +1 -1
- package/studio/app.js +15 -9
- package/studio/composer.css +7 -0
- package/studio/composer.js +42 -0
- package/studio/recipes.mjs +86 -0
- package/studio/select-menu.js +24 -7
- package//351/243/216/346/240/274/346/217/220/347/244/272/350/257/215.md +105 -0
package/bin/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import {
|
|
5
5
|
createAgentServer,
|
|
6
6
|
ensureDir,
|
|
@@ -14,9 +14,28 @@ import {
|
|
|
14
14
|
isIntermediateMp4,
|
|
15
15
|
readJson,
|
|
16
16
|
} from '../lib/host.mjs';
|
|
17
|
-
|
|
18
17
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const recipeFile = [
|
|
19
|
+
path.join(root, 'studio', 'recipes.mjs'),
|
|
20
|
+
path.resolve(root, '..', '_shared', 'studio', 'recipes.mjs'),
|
|
21
|
+
].find((file) => fs.existsSync(file));
|
|
22
|
+
if (!recipeFile) throw new Error('找不到风格提示词模块 studio/recipes.mjs');
|
|
23
|
+
const { findRecipe, loadRecipes, recipeView } = await import(pathToFileURL(recipeFile).href);
|
|
19
24
|
const COLUMNS = readJson(path.join(root, 'data', 'columns.json'), []);
|
|
25
|
+
const RECIPES = loadRecipes(path.join(root, '风格提示词.md'));
|
|
26
|
+
|
|
27
|
+
function presentColumn(column) {
|
|
28
|
+
const found = findRecipe(RECIPES, [column.cn, column.en, column.slug]);
|
|
29
|
+
return {
|
|
30
|
+
...column,
|
|
31
|
+
recipe: recipeView(found, {
|
|
32
|
+
prompt: found?.prompt || `用${column.cn}讲「把引号里换成你要讲的事」。${column.line || ''}`,
|
|
33
|
+
narration: column.voice ? 'required' : 'off',
|
|
34
|
+
attachment: column.voice || column.music ? 'required' : 'optional',
|
|
35
|
+
hint: column.hint || '',
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
20
39
|
|
|
21
40
|
function normalizeDuration(value) {
|
|
22
41
|
if (value == null || value === '' || value === 'auto') return null;
|
|
@@ -70,7 +89,7 @@ createAgentServer({
|
|
|
70
89
|
agent: 'biuvibe',
|
|
71
90
|
name: manifest.name || 'BiuVibe',
|
|
72
91
|
description: manifest.description || '',
|
|
73
|
-
styles: COLUMNS,
|
|
92
|
+
styles: COLUMNS.map(presentColumn),
|
|
74
93
|
canExport: cfg.canExport,
|
|
75
94
|
enginePresent: !!(engineRoot && fs.existsSync(path.join(engineRoot, 'AGENTS.md'))),
|
|
76
95
|
engineRoot: engineRoot || null,
|
package/lib/host.mjs
CHANGED
|
@@ -300,11 +300,39 @@ export function seedSkillIntoProject(projectDir, packageRoot, skillId) {
|
|
|
300
300
|
return dest;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
/**
|
|
303
|
+
/** 分段和无声中间片不能当成片。成片改名后(ep55.mp4 → ep55-master.mp4)要能从同目录找回。 */
|
|
304
304
|
export function isIntermediateMp4(filePath) {
|
|
305
|
-
const base = path.basename(filePath).toLowerCase();
|
|
306
|
-
if (base === 'merged.mp4' || /^
|
|
307
|
-
|
|
305
|
+
const base = path.basename(String(filePath || '')).toLowerCase();
|
|
306
|
+
if (base === 'merged.mp4' || /^seg[-_]\d+\.mp4$/.test(base)) return true;
|
|
307
|
+
if (/^draft([-.]|$)/.test(base)) return true;
|
|
308
|
+
const parts = String(filePath || '').split(/[/\\]/).map((part) => part.toLowerCase());
|
|
309
|
+
if (parts.includes('build') || parts.includes('segments')) return true;
|
|
310
|
+
return base === 'video.mp4' && parts.at(-2) === 'out';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** 记录里的文件被删掉或改名时,在同一目录找「原名开头」的现片。 */
|
|
314
|
+
export function rescueMissingFilm(filePath) {
|
|
315
|
+
const file = String(filePath || '');
|
|
316
|
+
if (!file) return '';
|
|
317
|
+
if (fs.existsSync(file) && !isIntermediateMp4(file)) return file;
|
|
318
|
+
const dir = path.dirname(file);
|
|
319
|
+
if (!fs.existsSync(dir)) return '';
|
|
320
|
+
const stem = path.basename(file, path.extname(file)).toLowerCase();
|
|
321
|
+
if (!stem) return '';
|
|
322
|
+
let best = null;
|
|
323
|
+
let names = [];
|
|
324
|
+
try { names = fs.readdirSync(dir); } catch { return ''; }
|
|
325
|
+
for (const name of names) {
|
|
326
|
+
if (!name.toLowerCase().endsWith('.mp4')) continue;
|
|
327
|
+
if (!name.toLowerCase().startsWith(stem)) continue;
|
|
328
|
+
const full = path.join(dir, name);
|
|
329
|
+
if (isIntermediateMp4(full)) continue;
|
|
330
|
+
let st;
|
|
331
|
+
try { st = fs.statSync(full); } catch { continue; }
|
|
332
|
+
if (!st.isFile()) continue;
|
|
333
|
+
if (!best || st.mtimeMs > best.mtimeMs) best = { path: full, mtimeMs: st.mtimeMs, size: st.size };
|
|
334
|
+
}
|
|
335
|
+
return best ? best.path : '';
|
|
308
336
|
}
|
|
309
337
|
|
|
310
338
|
function collectMp4(rootDir, maxDepth) {
|
|
@@ -344,7 +372,9 @@ function pickFilm(found) {
|
|
|
344
372
|
if (ai !== bi) return ai - bi;
|
|
345
373
|
return b.mtime - a.mtime;
|
|
346
374
|
});
|
|
347
|
-
|
|
375
|
+
const best = found[0];
|
|
376
|
+
if (!best || isIntermediateMp4(best.path)) return null;
|
|
377
|
+
return best;
|
|
348
378
|
}
|
|
349
379
|
|
|
350
380
|
/** Newest deliverable MP4. Root-level `<name>.mp4` wins over `out/video.mp4`. */
|
|
@@ -396,7 +426,34 @@ export function watchForFilm(cfg, jobs, jobId, {
|
|
|
396
426
|
const tick = async () => {
|
|
397
427
|
const job = jobs.get(jobId);
|
|
398
428
|
if (!job || job.status === 'failed' || job.status === 'cancelled') return;
|
|
399
|
-
|
|
429
|
+
const current = String(job.outputMp4 || '');
|
|
430
|
+
if (current && fs.existsSync(current) && !isIntermediateMp4(current)) return;
|
|
431
|
+
if (current && !fs.existsSync(current)) {
|
|
432
|
+
const rescued = rescueMissingFilm(current);
|
|
433
|
+
if (rescued) {
|
|
434
|
+
const message = `成片已就绪 · ${path.basename(rescued)}`;
|
|
435
|
+
const size = fs.statSync(rescued).size;
|
|
436
|
+
jobs.update(jobId, {
|
|
437
|
+
status: 'done',
|
|
438
|
+
progress: 100,
|
|
439
|
+
phase: 'film',
|
|
440
|
+
message,
|
|
441
|
+
outputMp4: rescued,
|
|
442
|
+
artifact: rescued,
|
|
443
|
+
filmBytes: size,
|
|
444
|
+
filmAt: new Date().toISOString(),
|
|
445
|
+
});
|
|
446
|
+
await reportProgress(cfg, {
|
|
447
|
+
taskId: jobs.get(jobId)?.panelTaskId,
|
|
448
|
+
title: title || jobs.get(jobId)?.title,
|
|
449
|
+
progress: 100,
|
|
450
|
+
message,
|
|
451
|
+
done: true,
|
|
452
|
+
artifact: rescued,
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
400
457
|
const hit = locateJobFilm({ ...job, projectPath: projectPath || job.projectPath, artifact: job.artifact }, cfg);
|
|
401
458
|
if (hit && !isIntermediateMp4(hit.path)) {
|
|
402
459
|
const message = `成片已就绪 · ${path.basename(hit.path)}`;
|
|
@@ -520,9 +577,14 @@ export function createAgentServer({ root, studioDir, api }) {
|
|
|
520
577
|
|
|
521
578
|
// 进程重启后:已有成片则收口;否则恢复 watch,避免面板/Studio 任务永远「处理中」
|
|
522
579
|
for (const j of jobs.list()) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
580
|
+
const filmMissing = j.outputMp4 && !fs.existsSync(j.outputMp4);
|
|
581
|
+
const filmBad = j.outputMp4 && isIntermediateMp4(j.outputMp4);
|
|
582
|
+
if (filmMissing || filmBad || (j.status === 'done' && !j.outputMp4)) {
|
|
583
|
+
const rescued = filmMissing ? rescueMissingFilm(j.outputMp4) : '';
|
|
584
|
+
const late = rescued
|
|
585
|
+
? { path: rescued, size: fs.statSync(rescued).size }
|
|
586
|
+
: locateJobFilm(j, cfg);
|
|
587
|
+
if (late && late.path && fs.existsSync(late.path) && !isIntermediateMp4(late.path)) {
|
|
526
588
|
jobs.update(j.id, {
|
|
527
589
|
status: 'done',
|
|
528
590
|
progress: 100,
|
|
@@ -533,8 +595,9 @@ export function createAgentServer({ root, studioDir, api }) {
|
|
|
533
595
|
filmBytes: late.size,
|
|
534
596
|
filmAt: new Date().toISOString(),
|
|
535
597
|
});
|
|
598
|
+
continue;
|
|
536
599
|
}
|
|
537
|
-
continue;
|
|
600
|
+
if (j.status === 'done') continue;
|
|
538
601
|
}
|
|
539
602
|
const waiting = j.status === 'running' || (j.outputMp4 && isIntermediateMp4(j.outputMp4));
|
|
540
603
|
if (!waiting) continue;
|
|
@@ -615,8 +678,27 @@ export function createAgentServer({ root, studioDir, api }) {
|
|
|
615
678
|
if (req.method === 'GET' && /^\/api\/jobs\/[^/]+\/film$/.test(pathname)) {
|
|
616
679
|
const id = decodeURIComponent(pathname.slice('/api/jobs/'.length, -'/film'.length));
|
|
617
680
|
const job = jobs.get(id);
|
|
618
|
-
|
|
619
|
-
if (!file || !
|
|
681
|
+
let file = job ? String(job.outputMp4 || '') : '';
|
|
682
|
+
if (job && (!file || !fs.existsSync(file) || isIntermediateMp4(file))) {
|
|
683
|
+
const rescued = file && !fs.existsSync(file) ? rescueMissingFilm(file) : '';
|
|
684
|
+
const late = rescued
|
|
685
|
+
? { path: rescued, size: fs.statSync(rescued).size }
|
|
686
|
+
: locateJobFilm(job, cfg);
|
|
687
|
+
if (late && late.path && fs.existsSync(late.path) && !isIntermediateMp4(late.path)) {
|
|
688
|
+
jobs.update(job.id, {
|
|
689
|
+
status: 'done',
|
|
690
|
+
progress: 100,
|
|
691
|
+
phase: 'film',
|
|
692
|
+
message: `成片已就绪 · ${path.basename(late.path)}`,
|
|
693
|
+
outputMp4: late.path,
|
|
694
|
+
artifact: late.path,
|
|
695
|
+
filmBytes: late.size,
|
|
696
|
+
filmAt: new Date().toISOString(),
|
|
697
|
+
});
|
|
698
|
+
file = late.path;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (!file || !file.toLowerCase().endsWith('.mp4') || !fs.existsSync(file)) return sendJson(res, 404, { error: '成片还没有' });
|
|
620
702
|
return serveLocalFile(req, res, file);
|
|
621
703
|
}
|
|
622
704
|
if (pathname.startsWith('/api/jobs/') && req.method === 'GET') {
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"id": "biuvibe",
|
|
4
4
|
"name": "BiuVibe",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.2",
|
|
6
6
|
"publisher": "Biunow",
|
|
7
7
|
"description": "BiuVibe 知识短片:选一种画风,写下要讲的事,配上你有权使用的音乐或口播。",
|
|
8
8
|
"entry": {
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"requiresEntitlement": "export"
|
|
45
45
|
},
|
|
46
46
|
"integrity": {
|
|
47
|
-
"sha256": "
|
|
48
|
-
"signature": "
|
|
47
|
+
"sha256": "7595dd13770062c115893b723e9cffa2c871a58543d5bd1877983ec7e5e9b7d3",
|
|
48
|
+
"signature": "O0sfbytJhfMSBWJSPaeqON8nQ4VcGP8ZCMeS6d/CTSGxM42hmyR12ZEesBdP4y5S9qSG4JpZ+koWR+ab8uqqAg==",
|
|
49
49
|
"signedBy": "publisher-key"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/package.json
CHANGED
package/studio/app.js
CHANGED
|
@@ -44,17 +44,18 @@
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function applyStyle(slug) {
|
|
47
|
+
function applyStyle(slug, opts) {
|
|
48
48
|
const style = state.styles.find((item) => item.slug === slug) || null;
|
|
49
49
|
state.style = style;
|
|
50
50
|
const needs = style ? columnNeeds(style) : null;
|
|
51
51
|
if (needs) {
|
|
52
|
-
$('needBar').hidden = false;
|
|
53
|
-
$('needHint').textContent = needs.hint;
|
|
54
52
|
$('uploadHint').textContent = needs.label;
|
|
55
53
|
$('files').setAttribute('accept', needs.accept);
|
|
56
|
-
}
|
|
57
|
-
|
|
54
|
+
}
|
|
55
|
+
if (style && style.recipe) {
|
|
56
|
+
Composer.paintRecipe(state, style.recipe, needs?.hint || style.recipe.hint, opts && opts.keep);
|
|
57
|
+
} else if ($('needBar')) {
|
|
58
|
+
$('needBar').hidden = !style;
|
|
58
59
|
}
|
|
59
60
|
if (style) state.storyboard = style.confirm !== false;
|
|
60
61
|
syncSb();
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
}
|
|
94
95
|
if (job.style && styleMenu) {
|
|
95
96
|
styleMenu.setValue(job.style, true);
|
|
96
|
-
applyStyle(job.style);
|
|
97
|
+
applyStyle(job.style, { keep: true });
|
|
97
98
|
}
|
|
98
99
|
if (typeof job.storyboard === 'boolean') {
|
|
99
100
|
state.storyboard = job.storyboard;
|
|
@@ -173,6 +174,9 @@
|
|
|
173
174
|
if (!state.style) throw new Error('请选择栏目');
|
|
174
175
|
const prompt = $('prompt').value.trim();
|
|
175
176
|
if (!prompt) throw new Error('请填写选题');
|
|
177
|
+
if (state.style.recipe && state.style.recipe.attachment === 'required' && !state.files.length) {
|
|
178
|
+
throw new Error(state.style.hint || '这个栏目必须上传附件');
|
|
179
|
+
}
|
|
176
180
|
if (state.style.music && !state.files.some((file) => String(file.kind || '').startsWith('audio') || /\.(mp3|wav|m4a|flac|ogg|aac)$/i.test(file.name || ''))) {
|
|
177
181
|
throw new Error('请上传一首有授权的配乐');
|
|
178
182
|
}
|
|
@@ -233,9 +237,11 @@
|
|
|
233
237
|
});
|
|
234
238
|
|
|
235
239
|
const first = state.styles[0];
|
|
236
|
-
|
|
240
|
+
const want = new URLSearchParams(location.search).get('style');
|
|
241
|
+
const picked = state.styles.find((item) => item.slug === want) || first;
|
|
242
|
+
if (picked) state.style = picked;
|
|
237
243
|
styleMenu = SelectMenu.mount($('stylePick'), {
|
|
238
|
-
value:
|
|
244
|
+
value: picked?.slug || '',
|
|
239
245
|
options: state.styles.map((item) => ({
|
|
240
246
|
value: item.slug,
|
|
241
247
|
label: item.cn,
|
|
@@ -247,7 +253,7 @@
|
|
|
247
253
|
size: 'sm',
|
|
248
254
|
onChange: (value) => applyStyle(value),
|
|
249
255
|
});
|
|
250
|
-
if (
|
|
256
|
+
if (picked) applyStyle(picked.slug);
|
|
251
257
|
await refreshJobs();
|
|
252
258
|
setInterval(refreshJobs, 2000);
|
|
253
259
|
void health;
|
package/studio/composer.css
CHANGED
|
@@ -91,6 +91,13 @@ button:disabled { cursor: not-allowed; opacity: .5; }
|
|
|
91
91
|
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
|
92
92
|
padding: 12px 16px 8px;
|
|
93
93
|
}
|
|
94
|
+
.need-flags { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
|
|
95
|
+
.need-flag {
|
|
96
|
+
font-size: 11px; font-weight: 650; border-radius: 99px; padding: 2px 8px;
|
|
97
|
+
background: #f4f4ef; color: var(--ink);
|
|
98
|
+
}
|
|
99
|
+
.need-flag.required { background: var(--ink); color: var(--accent); }
|
|
100
|
+
.need-flag.off { color: var(--muted); }
|
|
94
101
|
.link-input {
|
|
95
102
|
flex: 1; min-width: 200px; border: 1px solid var(--line); border-radius: 10px;
|
|
96
103
|
padding: 8px 12px; font-size: 12px; background: #fafaf7; color: var(--ink); outline: 0;
|
package/studio/composer.js
CHANGED
|
@@ -282,6 +282,47 @@
|
|
|
282
282
|
container.appendChild(c);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
const RECIPE_LEVEL = { off: '无', optional: '可选', required: '必须' };
|
|
286
|
+
|
|
287
|
+
function paintRecipe(state, recipe, hint, keepText) {
|
|
288
|
+
const prompt = document.getElementById('prompt');
|
|
289
|
+
let bar = document.getElementById('needBar');
|
|
290
|
+
if (!bar && prompt) {
|
|
291
|
+
bar = document.createElement('div');
|
|
292
|
+
bar.id = 'needBar';
|
|
293
|
+
bar.className = 'link-bar';
|
|
294
|
+
prompt.parentNode.insertBefore(bar, prompt);
|
|
295
|
+
}
|
|
296
|
+
if (!bar) return;
|
|
297
|
+
bar.hidden = false;
|
|
298
|
+
let hintEl = document.getElementById('needHint');
|
|
299
|
+
if (!hintEl) {
|
|
300
|
+
hintEl = document.createElement('span');
|
|
301
|
+
hintEl.id = 'needHint';
|
|
302
|
+
hintEl.className = 'muted';
|
|
303
|
+
bar.appendChild(hintEl);
|
|
304
|
+
}
|
|
305
|
+
hintEl.textContent = hint || (recipe && recipe.hint) || '';
|
|
306
|
+
let flags = document.getElementById('needFlags');
|
|
307
|
+
if (!flags) {
|
|
308
|
+
flags = document.createElement('div');
|
|
309
|
+
flags.id = 'needFlags';
|
|
310
|
+
flags.className = 'need-flags';
|
|
311
|
+
bar.appendChild(flags);
|
|
312
|
+
}
|
|
313
|
+
const narr = (recipe && recipe.narration) || 'optional';
|
|
314
|
+
const att = (recipe && recipe.attachment) || 'optional';
|
|
315
|
+
flags.innerHTML =
|
|
316
|
+
`<span class="need-flag ${narr}">旁白 · ${RECIPE_LEVEL[narr] || narr}</span>` +
|
|
317
|
+
`<span class="need-flag ${att}">附件 · ${RECIPE_LEVEL[att] || att}</span>`;
|
|
318
|
+
const text = String((recipe && recipe.prompt) || '').trim();
|
|
319
|
+
if (prompt && text && !keepText) {
|
|
320
|
+
prompt.value = text;
|
|
321
|
+
prompt.dispatchEvent(new Event('input'));
|
|
322
|
+
state.seed = text;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
285
326
|
function titleFromPrompt(prompt, fallback) {
|
|
286
327
|
const line = String(prompt || '')
|
|
287
328
|
.trim()
|
|
@@ -303,5 +344,6 @@
|
|
|
303
344
|
renderJobs,
|
|
304
345
|
addChip,
|
|
305
346
|
titleFromPrompt,
|
|
347
|
+
paintRecipe,
|
|
306
348
|
};
|
|
307
349
|
})(window);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/** 从《风格提示词.md》读出每条模板的起步提示词。标题用 ## 或 ###。 */
|
|
4
|
+
export function loadRecipes(file) {
|
|
5
|
+
if (!file || !fs.existsSync(file)) return [];
|
|
6
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
7
|
+
const recipes = [];
|
|
8
|
+
let section = '';
|
|
9
|
+
let title = '';
|
|
10
|
+
let blurb = [];
|
|
11
|
+
let buf = [];
|
|
12
|
+
let inCode = false;
|
|
13
|
+
|
|
14
|
+
const flush = () => {
|
|
15
|
+
const prompt = buf.join('\n').trim();
|
|
16
|
+
if (title && prompt) {
|
|
17
|
+
recipes.push({
|
|
18
|
+
section,
|
|
19
|
+
title: title.trim(),
|
|
20
|
+
blurb: blurb.join(' ').replace(/\s+/g, ' ').trim(),
|
|
21
|
+
prompt,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
buf = [];
|
|
25
|
+
blurb = [];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (line.startsWith('```')) {
|
|
30
|
+
if (inCode) {
|
|
31
|
+
inCode = false;
|
|
32
|
+
flush();
|
|
33
|
+
title = '';
|
|
34
|
+
} else {
|
|
35
|
+
inCode = true;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (inCode) {
|
|
40
|
+
buf.push(line);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (line.startsWith('### ')) {
|
|
44
|
+
title = line.slice(4).trim();
|
|
45
|
+
blurb = [];
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (line.startsWith('## ')) {
|
|
49
|
+
section = line.slice(3).trim();
|
|
50
|
+
title = section;
|
|
51
|
+
blurb = [];
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (line.startsWith('# ')) continue;
|
|
55
|
+
if (title && line.trim()) blurb.push(line.trim());
|
|
56
|
+
}
|
|
57
|
+
return recipes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function norm(value) {
|
|
61
|
+
return String(value || '').trim().toLowerCase().replace(/\s+/g, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function findRecipe(recipes, keys) {
|
|
65
|
+
const wants = [...new Set((keys || []).map(norm).filter(Boolean))];
|
|
66
|
+
for (const want of wants) {
|
|
67
|
+
const hit = (recipes || []).find((recipe) => {
|
|
68
|
+
const title = norm(recipe.title);
|
|
69
|
+
const head = norm(String(recipe.title).split('·')[0]);
|
|
70
|
+
return title === want || head === want;
|
|
71
|
+
});
|
|
72
|
+
if (hit) return hit;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function recipeView(recipe, extras = {}) {
|
|
78
|
+
const prompt = String(recipe?.prompt || extras.prompt || '').trim();
|
|
79
|
+
return {
|
|
80
|
+
prompt,
|
|
81
|
+
blurb: recipe?.blurb || '',
|
|
82
|
+
narration: extras.narration || 'optional',
|
|
83
|
+
attachment: extras.attachment || 'optional',
|
|
84
|
+
hint: extras.hint || recipe?.blurb || '',
|
|
85
|
+
};
|
|
86
|
+
}
|
package/studio/select-menu.js
CHANGED
|
@@ -138,16 +138,33 @@
|
|
|
138
138
|
const minWidth = grid ? 404 : 210;
|
|
139
139
|
const width = Math.min(Math.max(r.width, minWidth), vw - 24);
|
|
140
140
|
const left = Math.min(Math.max(12, r.left), Math.max(12, vw - width - 12));
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.dropUp = vh - r.bottom < estHeight + 14 && r.top > estHeight + 14;
|
|
147
|
-
this.pos = { left, width, top: r.bottom + 6, bottom: vh - r.top + 6 };
|
|
141
|
+
const gap = 6;
|
|
142
|
+
const margin = 12;
|
|
143
|
+
const below = Math.max(0, vh - r.bottom - gap - margin);
|
|
144
|
+
const above = Math.max(0, r.top - gap - margin);
|
|
145
|
+
let chrome = 0;
|
|
148
146
|
if (this.panel) {
|
|
149
147
|
this.panel.style.left = `${left}px`;
|
|
150
148
|
this.panel.style.width = `${width}px`;
|
|
149
|
+
const cats = this.panel.querySelector('.sm-cats');
|
|
150
|
+
const entry = this.panel.querySelector('.sm-entry');
|
|
151
|
+
chrome = (cats ? cats.offsetHeight : 0) + (entry ? entry.offsetHeight : 0);
|
|
152
|
+
}
|
|
153
|
+
const scrollCap = grid ? 430 : 292;
|
|
154
|
+
const naturalScroll = grid
|
|
155
|
+
? Math.min(scrollCap, Math.ceil(Math.max(visible.length, 1) / 3) * 96 + 14)
|
|
156
|
+
: Math.min(scrollCap, visible.length * 38 + 12);
|
|
157
|
+
const want = Math.min(chrome + naturalScroll, 240);
|
|
158
|
+
this.dropUp = below < want && above > below;
|
|
159
|
+
const available = Math.max(0, this.dropUp ? above : below);
|
|
160
|
+
const room = Math.min(available, chrome + naturalScroll);
|
|
161
|
+
const scrollMax = Math.max(0, Math.min(scrollCap, room - chrome));
|
|
162
|
+
this.pos = { left, width, top: r.bottom + gap, bottom: vh - r.top + gap };
|
|
163
|
+
if (this.panel) {
|
|
164
|
+
this.panel.style.maxHeight = `${room}px`;
|
|
165
|
+
this.panel.style.overflow = scrollMax > 0 ? 'hidden' : 'auto';
|
|
166
|
+
const scroll = this.panel.querySelector('.sm-scroll');
|
|
167
|
+
if (scroll) scroll.style.maxHeight = `${scrollMax}px`;
|
|
151
168
|
if (this.dropUp) {
|
|
152
169
|
this.panel.style.bottom = `${this.pos.bottom}px`;
|
|
153
170
|
this.panel.style.top = 'auto';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# BiuVibe 风格提示词
|
|
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
|
+
游戏 HUD 和发光图表。没有口播,不画真实游戏的角色和枪械。配乐必须上传。
|
|
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
|
+
```
|