返回 VideoClaw
utils.ts
1 /**
2 * 阶段数据工具函数
3 * - 路径转 URL
4 * - 剧本/分镜结构化文本解析与重建
5 */
6
7 /** 将后端本地文件路径转换为浏览器可访问的 URL */
8 export function assetUrl(path: string): string {
9 if (!path) return '';
10 if (path.startsWith('http') || path.startsWith('/') || path.startsWith('blob:') || path.startsWith('data:')) return path;
11 return '/' + path;
12 }
13
14 export function assetVersionLabel(path: string, index: number): string {
15 const file = (path || '').split('/').pop() || '';
16 const uploadMatch = file.match(/_upload_(v\d+)/i);
17 if (uploadMatch) return `用户上传: ${uploadMatch[1].toLowerCase()}`;
18 const aiMatch = file.match(/_v(\d+)\.[^.]+$/i);
19 const version = aiMatch ? `v${aiMatch[1]}` : `v${index + 1}`;
20 return `AI生成: ${version}`;
21 }
22
23 /* ─── 剧本 / 分镜 结构化文本解析 ─── */
24
25 export interface ParsedCharacter {
26 name: string;
27 description: string;
28 }
29
30 export interface ParsedSetting {
31 name: string;
32 description: string;
33 }
34
35 export interface ParsedScene {
36 id: string;
37 characters: string[];
38 settings: string[];
39 description: string;
40 raw: string;
41 }
42
43 export interface ParsedScript {
44 characters: ParsedCharacter[];
45 settings: ParsedSetting[];
46 scenes: ParsedScene[];
47 isZh: boolean;
48 }
49
50 /** 解析结构化剧本/分镜文本 → 角色 + 场景 + 故事线 */
51 export function parseScriptText(text: string): ParsedScript {
52 if (!text) return { characters: [], settings: [], scenes: [], isZh: false };
53
54 const normalized = text.replace(/\\n/g, '\n');
55 const isZh = /角色[::]|场景设置[::]|视频片段/.test(normalized);
56
57 const charHeader = isZh ? /角色[::]/ : /Characters[::]/;
58 const settingHeader = isZh ? /场景设置[::]/ : /Settings[::]/;
59 const sceneHeader = isZh ? /视频片段[::]/ : /Scenes[::]/;
60
61 function findMatch(src: string, pat: RegExp) {
62 const m = src.match(pat);
63 return m ? { index: src.indexOf(m[0]), length: m[0].length } : null;
64 }
65
66 function extractBlock(src: string, startPat: RegExp, endPat: RegExp | null): string {
67 const sm = findMatch(src, startPat);
68 if (!sm) return '';
69 const sp = sm.index + sm.length;
70 if (endPat) {
71 const em = findMatch(src.slice(sp), endPat);
72 return em ? src.slice(sp, sp + em.index) : src.slice(sp);
73 }
74 return src.slice(sp);
75 }
76
77 const charBlock = extractBlock(normalized, charHeader, settingHeader).trim();
78 const settingBlock = extractBlock(normalized, settingHeader, sceneHeader).trim();
79 const sceneBlock = extractBlock(normalized, sceneHeader, null).trim();
80
81 const characters: ParsedCharacter[] = [];
82 for (const line of charBlock.split('\n')) {
83 const t = line.trim();
84 if (!t) continue;
85 const ci = t.search(/[::]/);
86 if (ci > 0) {
87 characters.push({
88 name: t.slice(0, ci).trim(),
89 description: t.slice(ci + 1).trim().replace(/\.\s*$/, ''),
90 });
91 }
92 }
93
94 const settings: ParsedSetting[] = [];
95 for (const line of settingBlock.split('\n')) {
96 const t = line.trim();
97 if (!t) continue;
98 const ci = t.search(/[::]/);
99 if (ci > 0) {
100 settings.push({
101 name: t.slice(0, ci).trim(),
102 description: t.slice(ci + 1).trim().replace(/\.\s*$/, ''),
103 });
104 }
105 }
106
107 const scenes: ParsedScene[] = [];
108 const scenePat = isZh
109 ? /^视频片段\s*(\d+)\s*[::]\s*(.*)/
110 : /^Scene\s+(\d+)\s*[::]\s*(.*)/i;
111
112 for (const line of sceneBlock.split('\n')) {
113 const m = line.trim().match(scenePat);
114 if (m) {
115 const raw = m[2];
116 const cm = raw.match(/\[(?:Characters|角色)\s*[::]\s*([^\]]+)\]/i);
117 const sm2 = raw.match(/\[(?:Settings|场景(?:设置)?)\s*[::]\s*([^\]]+)\]/i);
118 const chars = cm ? cm[1].split(/[,,]/).map(s => s.trim()).filter(Boolean) : [];
119 const sets = sm2 ? sm2[1].split(/[,,]/).map(s => s.trim()).filter(Boolean) : [];
120 const desc = raw.replace(/\[.*?\]/g, '').trim();
121 scenes.push({ id: m[1], characters: chars, settings: sets, description: desc, raw });
122 }
123 }
124
125 return { characters, settings, scenes, isZh };
126 }
127
128 /** 将解析后的结构重建为标准文本格式 */
129 export function reconstructScriptText(parsed: ParsedScript): string {
130 const { isZh, characters, settings, scenes } = parsed;
131 const charH = isZh ? '角色:' : 'Characters:';
132 const settH = isZh ? '场景设置:' : 'Settings:';
133 const sceneH = isZh ? '视频片段:' : 'Scenes:';
134 const scenePrefix = isZh ? '视频片段' : 'Scene';
135 const charLabel = isZh ? '角色' : 'Characters';
136 const settLabel = isZh ? '场景' : 'Settings';
137
138 const lines: string[] = [];
139 lines.push(charH);
140 for (const c of characters) {
141 lines.push(`${c.name}: ${c.description}`);
142 }
143 lines.push(settH);
144 for (const s of settings) {
145 lines.push(`${s.name}: ${s.description}`);
146 }
147 lines.push(sceneH);
148 for (const sc of scenes) {
149 const cp = sc.characters.length ? `[${charLabel}: ${sc.characters.join(', ')}]` : '';
150 const sp = sc.settings.length ? `[${settLabel}: ${sc.settings.join(', ')}]` : '';
151 const parts = [`${scenePrefix} ${sc.id}:`, cp, sp, sc.description].filter(Boolean);
152 lines.push(parts.join(' '));
153 }
154 return lines.join('\n');
155 }
156
156 lines TYPESCRIPT