返回 AiToEarn
prompt-parser.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / ai / libs / volcengine / prompt-parser.ts
1 import { Logger } from '@nestjs/common'
2 import {
3 AITranslationSkillParams,
4 LanguageCode,
5 SkillType,
6 SubtitleRecognitionType,
7 TranslationType,
8 } from './volcengine.interface'
9
10 const logger = new Logger('PromptParser')
11
12 /**
13 * 语言映射表(中文关键词 -> LanguageCode)
14 */
15 const LANGUAGE_MAP: Record<string, LanguageCode> = {
16 // 中文
17 中文: LanguageCode.Zh,
18 汉语: LanguageCode.Zh,
19 中: LanguageCode.Zh,
20 chinese: LanguageCode.Zh,
21 zh: LanguageCode.Zh,
22
23 // 英文
24 英文: LanguageCode.En,
25 英语: LanguageCode.En,
26 英: LanguageCode.En,
27 english: LanguageCode.En,
28 en: LanguageCode.En,
29
30 // 日语
31 日文: LanguageCode.Ja,
32 日语: LanguageCode.Ja,
33 日: LanguageCode.Ja,
34 japanese: LanguageCode.Ja,
35 ja: LanguageCode.Ja,
36
37 // 韩语
38 韩文: LanguageCode.Ko,
39 韩语: LanguageCode.Ko,
40 韩: LanguageCode.Ko,
41 朝鲜语: LanguageCode.Ko,
42 korean: LanguageCode.Ko,
43 ko: LanguageCode.Ko,
44
45 // 德语
46 德文: LanguageCode.De,
47 德语: LanguageCode.De,
48 德: LanguageCode.De,
49 german: LanguageCode.De,
50 de: LanguageCode.De,
51
52 // 法语
53 法文: LanguageCode.Fr,
54 法语: LanguageCode.Fr,
55 法: LanguageCode.Fr,
56 french: LanguageCode.Fr,
57 fr: LanguageCode.Fr,
58
59 // 俄语
60 俄文: LanguageCode.Ru,
61 俄语: LanguageCode.Ru,
62 俄: LanguageCode.Ru,
63 russian: LanguageCode.Ru,
64 ru: LanguageCode.Ru,
65
66 // 西班牙语
67 西班牙文: LanguageCode.Es,
68 西班牙语: LanguageCode.Es,
69 西语: LanguageCode.Es,
70 spanish: LanguageCode.Es,
71 es: LanguageCode.Es,
72
73 // 葡萄牙语
74 葡萄牙文: LanguageCode.Pt,
75 葡萄牙语: LanguageCode.Pt,
76 葡语: LanguageCode.Pt,
77 portuguese: LanguageCode.Pt,
78 pt: LanguageCode.Pt,
79
80 // 意大利语
81 意大利文: LanguageCode.It,
82 意大利语: LanguageCode.It,
83 意语: LanguageCode.It,
84 italian: LanguageCode.It,
85 it: LanguageCode.It,
86
87 // 印尼语
88 印尼文: LanguageCode.Id,
89 印尼语: LanguageCode.Id,
90 印度尼西亚语: LanguageCode.Id,
91 indonesian: LanguageCode.Id,
92 id: LanguageCode.Id,
93
94 // 越南语
95 越南文: LanguageCode.Vi,
96 越南语: LanguageCode.Vi,
97 越语: LanguageCode.Vi,
98 vietnamese: LanguageCode.Vi,
99 vi: LanguageCode.Vi,
100
101 // 泰语
102 泰文: LanguageCode.Th,
103 泰语: LanguageCode.Th,
104 泰: LanguageCode.Th,
105 thai: LanguageCode.Th,
106 th: LanguageCode.Th,
107
108 // 阿拉伯语
109 阿拉伯文: LanguageCode.Ar,
110 阿拉伯语: LanguageCode.Ar,
111 阿语: LanguageCode.Ar,
112 arabic: LanguageCode.Ar,
113 ar: LanguageCode.Ar,
114
115 // 土耳其语
116 土耳其文: LanguageCode.Tr,
117 土耳其语: LanguageCode.Tr,
118 土语: LanguageCode.Tr,
119 turkish: LanguageCode.Tr,
120 tr: LanguageCode.Tr,
121 }
122
123 const DEFAULT_SOURCE: LanguageCode = LanguageCode.Zh
124 const DEFAULT_TARGET: LanguageCode = LanguageCode.En
125 // Note: Only FacialTranslation is supported, no keyword mapping needed
126
127 /**
128 * from prompt extract source and target languages
129 */
130 /**
131 * 清洗并标准化语言关键词
132 */
133 function normalizeLang(raw?: string): LanguageCode | undefined {
134 if (!raw)
135 return
136 const key = raw
137 .toLowerCase()
138 .replace(/[^\w\u4E00-\u9FA5]/g, '') // 去掉标点,只保留字母/数字/中文
139 return LANGUAGE_MAP[key]
140 }
141
142 /**
143 * 从 prompt 中提取源语言和目标语言
144 */
145 function extractLanguage(prompt: string): { source: LanguageCode, target: LanguageCode } {
146 let source: LanguageCode | undefined
147 let target: LanguageCode | undefined
148
149 // 常见中英文表达
150 const patterns: RegExp[] = [
151 // 从中文翻译到英文 / 由A翻译成B
152 /(?:从|由)\s*([^\s到成为至::,,。]+)\s*(?:翻译)?[到成为至]\s*([^\s::,,。]+)/,
153
154 // 中文翻译成英文 / 中文译成英文
155 /(\S+)\s*(?:翻译|译)[成为到]\s*([^\s::,,。]+)/,
156
157 // translate from chinese to english
158 /translate\s+(?:from\s+)?(\w+)\s+(?:to|into)\s+(\w+)/i,
159 ]
160
161 for (const p of patterns) {
162 const m = prompt.match(p)
163 if (m) {
164 source = normalizeLang(m[1])
165 target = normalizeLang(m[2])
166 if (source || target)
167 break
168 }
169 }
170
171 // 只指定目标语言:翻译为英文 / translate to english
172 if (!target) {
173 const targetPatterns: RegExp[] = [
174 /(?:翻译|译)[为成到]\s*([^\s::,,。]+)/,
175 /translate\s+(?:to|into)\s+(\w+)/i,
176 ]
177
178 for (const p of targetPatterns) {
179 const m = prompt.match(p)
180 if (m) {
181 target = normalizeLang(m[1])
182 if (target)
183 break
184 }
185 }
186 }
187
188 // 🔹 MCP 提示词兜底:to target language
189 if (!target && /target language/i.test(prompt)) {
190 target = DEFAULT_TARGET
191 }
192
193 // ✅ 默认:中文 → 英文
194 source = source ?? DEFAULT_SOURCE
195 target = target ?? DEFAULT_TARGET
196
197 logger.debug(`Extracted language => source: ${source}, target: ${target}`)
198 return { source, target }
199 }
200
201 /**
202 * Extract translation type from prompt
203 */
204 function extractTranslationType(_prompt: string): TranslationType[] {
205 logger.debug('Using FacialTranslation (only supported type)')
206 return [TranslationType.FacialTranslation, TranslationType.SubtitleTranslation, TranslationType.VoiceTranslation]
207 }
208
209 /**
210 * Extract subtitle recognition type from prompt
211 */
212 function extractSubtitleRecognitionType(prompt: string): SubtitleRecognitionType {
213 const lowerPrompt = prompt.toLowerCase()
214
215 if (lowerPrompt.includes('ocr') || lowerPrompt.includes('image')) {
216 return SubtitleRecognitionType.OCR
217 }
218
219 if (lowerPrompt.includes('asr') || lowerPrompt.includes('audio') || lowerPrompt.includes('speech')) {
220 return SubtitleRecognitionType.ASR
221 }
222
223 if (lowerPrompt.includes('vision') || lowerPrompt.includes('ai')) {
224 return SubtitleRecognitionType.Vision
225 }
226
227 // 如果提到硬字幕相关关键词,使用 OCR
228 if (lowerPrompt.includes('硬字幕') || lowerPrompt.includes('hard subtitle') || lowerPrompt.includes('hardcoded')) {
229 return SubtitleRecognitionType.OCR
230 }
231
232 // Default to ASR - 大多数视频翻译场景是翻译语音内容
233 return SubtitleRecognitionType.ASR
234 }
235
236 /**
237 * Check if hard subtitle and source erasure are needed
238 * (Reserved for future prompt-based override)
239 */
240 function _extractSubtitleConfig(prompt: string) {
241 const lowerPrompt = prompt.toLowerCase()
242
243 const needHardSubtitle = lowerPrompt.includes('hard subtitle')
244 || lowerPrompt.includes('burn')
245 || lowerPrompt.includes('embed')
246
247 const needEraseSource = lowerPrompt.includes('erase')
248 || lowerPrompt.includes('remove')
249 || needHardSubtitle
250
251 return {
252 needHardSubtitle,
253 needEraseSource,
254 }
255 }
256
257 function isTranslationPrompt(prompt: string): boolean {
258 const text = prompt.toLowerCase()
259
260 const patterns: RegExp[] = [
261 // 中文翻译意图
262 /翻译/,
263 /译为|译成/,
264 /将.*翻译/,
265 /字幕.*翻译/,
266 /翻译.*字幕/,
267
268 // 英文翻译意图
269 /translate/,
270 /translation/,
271 /subtitles?/,
272 /voice/,
273 /lip/,
274 /facial/,
275 /audio/,
276 ]
277
278 return patterns.some(p => p.test(text))
279 }
280
281 /**
282 * Parse translation prompt
283 */
284 export function parseTranslationPrompt(prompt: string): AITranslationSkillParams | null {
285 logger.debug(`Parsing translation prompt: ${prompt}`)
286
287 if (!isTranslationPrompt(prompt)) {
288 logger.debug('Prompt does not contain translation intent')
289 return null
290 }
291
292 const { source, target } = extractLanguage(prompt)
293
294 if (!target) {
295 logger.warn('Unable to extract target language from prompt')
296 return null
297 }
298
299 const translationTypes = extractTranslationType(prompt)
300 const recognitionType = extractSubtitleRecognitionType(prompt)
301
302 // Build params - Facial translation config
303 const params: AITranslationSkillParams = {
304 TranslationConfig: {
305 SourceLanguage: source || LanguageCode.Zh, // Default source: Chinese
306 TargetLanguage: target,
307 TranslationTypeList: translationTypes, // Always [FacialTranslation]
308 },
309 OperatorConfig: {
310 SubtitleRecognitionConfig: {
311 RecognitionType: recognitionType,
312 },
313 },
314 }
315
316 // Facial translation defaults: hard subtitle + erase source
317 params.SubtitleConfig = {
318 IsHardSubtitle: true,
319 IsEraseSource: true,
320 FontSize: 24,
321 MarginL: 0.1,
322 MarginR: 0.1,
323 MarginV: 0.05,
324 ShowLines: 2,
325 }
326 logger.debug('Applied facial translation defaults: hard subtitle + erase source')
327
328 return params
329 }
330
331 /**
332 * Parse VCreative (AI editing) prompt
333 * Currently disabled, reserved for future use
334 */
335 function _parseVCreativePrompt(prompt: string): { Text: string } | null {
336 const lowerPrompt = prompt.toLowerCase()
337
338 const mergeKeywords = [
339 'merge',
340 'combine',
341 'mix',
342 'audio',
343 'video',
344 'sound',
345 ]
346
347 const isMerge = mergeKeywords.some(keyword => lowerPrompt.includes(keyword))
348
349 if (isMerge) {
350 logger.debug('Detected merge-related prompt')
351
352 let optimizedText = ''
353
354 if (lowerPrompt.includes('audio') || lowerPrompt.includes('sound')) {
355 optimizedText = 'Merge video visuals with audio from second input'
356 }
357 else if (lowerPrompt.includes('concat') || lowerPrompt.includes('join')) {
358 optimizedText = prompt
359 }
360 else {
361 optimizedText = prompt
362 }
363
364 logger.debug(`VCreative prompt optimized: ${prompt} -> ${optimizedText}`)
365
366 return {
367 Text: optimizedText,
368 }
369 }
370
371 const vCreativeKeywords = [
372 'concat',
373 'trim',
374 'cut',
375 'edit',
376 'image',
377 'generate video',
378 ]
379
380 const isVCreative = vCreativeKeywords.some(keyword => lowerPrompt.includes(keyword))
381
382 if (isVCreative) {
383 logger.debug('Detected VCreative prompt')
384 return {
385 Text: prompt,
386 }
387 }
388
389 return null
390 }
391
392 /**
393 * Check if prompt is a subtitle erase task (no translation intent)
394 */
395 function isEraseOnlyTask(prompt: string): boolean {
396 const lowerPrompt = prompt.toLowerCase()
397
398 const eraseKeywords = ['remove', 'erase', 'delete', 'clear']
399 const hasEraseKeyword = eraseKeywords.some(keyword => lowerPrompt.includes(keyword))
400
401 if (!hasEraseKeyword) {
402 return false
403 }
404
405 // Subtitle-related keywords
406 const subtitleKeywords = ['subtitle', 'caption', 'watermark']
407 const hasSubtitleKeyword = subtitleKeywords.some(keyword => lowerPrompt.includes(keyword))
408
409 // Exclude translation intent
410 const translationIntentKeywords = ['translate', 'translation']
411 const hasTranslationIntent = translationIntentKeywords.some(keyword => lowerPrompt.includes(keyword))
412
413 // Erase task: has erase + subtitle keywords, but no translation intent
414 return hasSubtitleKeyword && !hasTranslationIntent
415 }
416
417 /**
418 * Check if prompt is a highlight extraction task
419 */
420 function isHighlightTask(prompt: string): boolean {
421 const lowerPrompt = prompt.toLowerCase()
422
423 const highlightKeywords = [
424 'highlight',
425 'exciting',
426 'best moments',
427 'compilation',
428 'montage',
429 ]
430
431 return highlightKeywords.some(keyword => lowerPrompt.includes(keyword))
432 }
433
434 /**
435 * Check if prompt is a video understanding task
436 */
437 function isVisionTask(prompt: string): boolean {
438 const lowerPrompt = prompt.toLowerCase()
439
440 const visionKeywords = [
441 'understand',
442 'summarize',
443 'summary',
444 'analyze',
445 'analysis',
446 'describe',
447 'what is',
448 'about',
449 'content',
450 'keywords',
451 'tags',
452 ]
453
454 return visionKeywords.some(keyword => lowerPrompt.includes(keyword))
455 }
456
457 /**
458 * Check if prompt is a VCreative (AI editing) task
459 */
460 function isVCreativeTask(prompt: string): boolean {
461 const lowerPrompt = prompt.toLowerCase()
462
463 const vCreativeKeywords = [
464 'concat',
465 'merge',
466 'combine',
467 'edit',
468 'trim',
469 'cut',
470 'splice',
471 'join',
472 'add music',
473 'background music',
474 'picture-in-picture',
475 'pip',
476 ]
477
478 return vCreativeKeywords.some(keyword => lowerPrompt.includes(keyword))
479 }
480
481 /**
482 * Parse Aideo task prompt to extract skill type and parameters
483 *
484 * Priority order (high to low):
485 * 1. AITranslation - Translation tasks
486 * 2. Erase - Subtitle/watermark removal
487 * 3. Highlight - Highlight extraction
488 * 4. Vision - Video understanding
489 * 5. VCreative - AI editing (fallback)
490 */
491 export function parseAideoPrompt(prompt: string): {
492 skillType: SkillType
493 skillParams: Record<string, unknown>
494 } | null {
495 logger.debug(`Parsing Aideo prompt: ${prompt}`)
496
497 // 1. Check translation task first
498 const translationParams = parseTranslationPrompt(prompt)
499 if (translationParams) {
500 logger.debug('Detected AITranslation task')
501 return {
502 skillType: SkillType.AITranslation,
503 skillParams: translationParams as unknown as Record<string, unknown>,
504 }
505 }
506
507 // 2. Check subtitle erase task
508 if (isEraseOnlyTask(prompt)) {
509 logger.debug('Detected Erase task')
510 return {
511 skillType: SkillType.Erase,
512 skillParams: {
513 Mode: 'Auto',
514 Auto: { Type: 'Subtitle' },
515 NewVid: true,
516 },
517 }
518 }
519
520 // 3. Check highlight task
521 if (isHighlightTask(prompt)) {
522 logger.debug('Detected Highlight task')
523 return {
524 skillType: SkillType.Highlight,
525 skillParams: {
526 Text: prompt,
527 },
528 }
529 }
530
531 // 4. Check vision task
532 if (isVisionTask(prompt)) {
533 logger.debug('Detected Vision task')
534 return {
535 skillType: SkillType.Vision,
536 skillParams: {
537 Text: prompt,
538 },
539 }
540 }
541
542 // 5. Check VCreative task (fallback)
543 if (isVCreativeTask(prompt)) {
544 logger.debug('Detected VCreative task')
545 return {
546 skillType: SkillType.VCreative,
547 skillParams: {
548 Text: prompt,
549 },
550 }
551 }
552
553 // No match, let server decide
554 logger.debug('Unable to detect skill type, server will decide')
555 return null
556 }
557
557 lines TYPESCRIPT