|
|
@@ -24,7 +24,7 @@ import { TemplateService } from '../../../services/template.service';
|
|
|
import { GenerationTemplate } from '../../../models/template.model';
|
|
|
|
|
|
interface SceneSegment {
|
|
|
- /** 画面描述(英文 prompt);快速模式由 LLM 生成,手动模式由用户填入 */
|
|
|
+ /** 画面描述(中文提示词);快速模式由 LLM 生成,手动模式由用户填入 */
|
|
|
scenePrompt: string;
|
|
|
/** 镜头类型:决定默认继承哪些连续性锚点 */
|
|
|
shotType?: T2vShotType;
|
|
|
@@ -194,11 +194,11 @@ export class TopicToVideoComponent implements OnInit, OnDestroy {
|
|
|
private genSub?: Subscription;
|
|
|
|
|
|
readonly stylePresets: { value: T2vStyle; label: string; promptHint: string }[] = [
|
|
|
- { value: 'cinematic', label: '电影感', promptHint: 'cinematic lighting, film grain, anamorphic' },
|
|
|
- { value: 'realistic', label: '写实', promptHint: 'photorealistic, ultra detailed, 4k' },
|
|
|
- { value: 'anime', label: '动漫', promptHint: 'anime style, vibrant colors, studio ghibli' },
|
|
|
- { value: 'illustration', label: '插画', promptHint: 'flat illustration, modern design' },
|
|
|
- { value: 'documentary', label: '纪录片', promptHint: 'documentary photography, natural light' },
|
|
|
+ { value: 'cinematic', label: '电影感', promptHint: '电影感光影、胶片颗粒、宽银幕镜头质感' },
|
|
|
+ { value: 'realistic', label: '写实', promptHint: '写实照片质感,细节丰富,高清画面' },
|
|
|
+ { value: 'anime', label: '动漫', promptHint: '动画风格,色彩鲜明,柔和手绘质感' },
|
|
|
+ { value: 'illustration', label: '插画', promptHint: '扁平插画风格,现代设计感' },
|
|
|
+ { value: 'documentary', label: '纪录片', promptHint: '纪实摄影风格,自然光,真实现场感' },
|
|
|
];
|
|
|
|
|
|
readonly aspectPresets: { value: T2vAspect; label: string; w: number; h: number }[] = [
|
|
|
@@ -571,6 +571,10 @@ export class TopicToVideoComponent implements OnInit, OnDestroy {
|
|
|
return this.stylePresets.find((item) => item.value === value)?.label || value;
|
|
|
}
|
|
|
|
|
|
+ private shotTypeLabel(value: T2vShotType): string {
|
|
|
+ return this.shotTypePresets.find((item) => item.value === value)?.label || value;
|
|
|
+ }
|
|
|
+
|
|
|
private aspectLabel(value: T2vAspect): string {
|
|
|
return this.aspectPresets.find((item) => item.value === value)?.label || value;
|
|
|
}
|
|
|
@@ -673,14 +677,14 @@ export class TopicToVideoComponent implements OnInit, OnDestroy {
|
|
|
if (!this.continuityEnabled) return '';
|
|
|
const inherit = this.resolveInheritance(scene);
|
|
|
const parts = [
|
|
|
- inherit.subject && this.visualBible.subject ? `subject: ${this.visualBible.subject}` : '',
|
|
|
- inherit.subject && this.visualBible.product ? `product: ${this.visualBible.product}` : '',
|
|
|
- inherit.location && this.visualBible.location ? `location: ${this.visualBible.location}` : '',
|
|
|
- inherit.style && this.visualBible.palette ? `palette: ${this.visualBible.palette}` : '',
|
|
|
- inherit.style && this.visualBible.lighting ? `lighting: ${this.visualBible.lighting}` : '',
|
|
|
- inherit.style && this.visualBible.camera ? `camera: ${this.visualBible.camera}` : '',
|
|
|
- inherit.style && this.visualBible.styleNotes ? `style: ${this.visualBible.styleNotes}` : '',
|
|
|
- this.visualBible.constraints ? `must keep: ${this.visualBible.constraints}` : '',
|
|
|
+ inherit.subject && this.visualBible.subject ? `主体:${this.visualBible.subject}` : '',
|
|
|
+ inherit.subject && this.visualBible.product ? `产品:${this.visualBible.product}` : '',
|
|
|
+ inherit.location && this.visualBible.location ? `场景:${this.visualBible.location}` : '',
|
|
|
+ inherit.style && this.visualBible.palette ? `配色:${this.visualBible.palette}` : '',
|
|
|
+ inherit.style && this.visualBible.lighting ? `光线:${this.visualBible.lighting}` : '',
|
|
|
+ inherit.style && this.visualBible.camera ? `镜头:${this.visualBible.camera}` : '',
|
|
|
+ inherit.style && this.visualBible.styleNotes ? `风格:${this.visualBible.styleNotes}` : '',
|
|
|
+ this.visualBible.constraints ? `必须保持:${this.visualBible.constraints}` : '',
|
|
|
].filter(Boolean);
|
|
|
return parts.join(', ');
|
|
|
}
|
|
|
@@ -715,30 +719,30 @@ export class TopicToVideoComponent implements OnInit, OnDestroy {
|
|
|
const isEmptyScene = !inherit.subject;
|
|
|
const frameRule = frame === 'tail'
|
|
|
? (isEmptyScene
|
|
|
- ? 'ending frame, environment-only composition, clearly later moment with different camera angle and lighting beat'
|
|
|
- : 'ending frame, clearly later moment after the opening frame, subject/product position and gesture changed, different camera angle, no identical composition')
|
|
|
+ ? '尾帧,纯环境构图,呈现明显更晚的时刻,镜头角度和光线节奏与首帧不同'
|
|
|
+ : '尾帧,呈现首帧之后的明确后续时刻,主体或产品的位置与姿态发生变化,镜头角度不同,不要复制首帧构图')
|
|
|
: frame === 'video'
|
|
|
? (isEmptyScene
|
|
|
- ? 'smooth natural environmental motion only (wind, light shift, camera drift); do not introduce people or products'
|
|
|
- : 'smooth natural motion, preserve same identity and environment')
|
|
|
+ ? '只保留自然环境运动,例如风、光影变化、轻微镜头漂移;不要引入人物或产品'
|
|
|
+ : '自然流畅的运动,保持同一主体身份和同一环境')
|
|
|
: (isEmptyScene
|
|
|
- ? 'opening frame, environment-only / pure scenery composition, no people or products in frame'
|
|
|
- : 'opening frame, subject/product visible in foreground, unique composition for this shot');
|
|
|
- const shotLabel = index >= 0 ? `shot ${index + 1}` : '';
|
|
|
- const narration = scene.narration ? `story beat: ${scene.narration}` : '';
|
|
|
+ ? '首帧,纯环境或纯风景构图,画面中不要出现人物或产品'
|
|
|
+ : '首帧,主体或产品在前景清晰可见,本镜头构图需要独立且有辨识度');
|
|
|
+ const shotLabel = index >= 0 ? `第 ${index + 1} 个镜头` : '';
|
|
|
+ const narration = scene.narration ? `故事节拍:${scene.narration}` : '';
|
|
|
const emptySceneRule = isEmptyScene
|
|
|
- ? 'STRICT: this is an empty / establishing shot; do NOT include any person, character, face, hand, body, crowd, product or branded item; render landscape, environment, objects, abstract elements only'
|
|
|
+ ? '严格要求:这是空镜或建立镜头;不要包含任何人物、角色、面部、手、身体、人群、产品或品牌物品;只呈现风景、环境、物体或抽象元素'
|
|
|
: '';
|
|
|
return [
|
|
|
shotLabel,
|
|
|
- `shot-specific visual action: ${scene.scenePrompt}`,
|
|
|
- `shot type: ${shotType}`,
|
|
|
+ `本镜头画面动作:${scene.scenePrompt}`,
|
|
|
+ `镜头类型:${this.shotTypeLabel(shotType)}`,
|
|
|
narration,
|
|
|
- state ? `continuity state: ${state}` : '',
|
|
|
+ state ? `连续性状态:${state}` : '',
|
|
|
frameRule,
|
|
|
- bible ? `global consistency reference: ${bible}` : '',
|
|
|
+ bible ? `全局一致性参考:${bible}` : '',
|
|
|
emptySceneRule,
|
|
|
- 'make this shot visually distinct from other shots, do not reuse the same composition, avoid generic establishing shot',
|
|
|
+ '让本镜头与其他镜头在视觉上明显区分,不要复用同一构图,避免泛泛的空镜',
|
|
|
].filter(Boolean).join(', ');
|
|
|
}
|
|
|
|
|
|
@@ -883,8 +887,8 @@ ${this.topic.trim()}
|
|
|
|
|
|
== 全局视觉锚点(每段都必须复用)==
|
|
|
- 视觉风格:${styleHint}
|
|
|
-- 你需要先在脑内确定「主角/主体(subject)」+「主色调(palette,如 warm amber / cool teal)」+「场景地点(location)」+「光线时段(lighting,如 golden hour / overcast)」
|
|
|
-- 上述 4 项一旦确定,**全部 ${this.nScenes} 段的 scene_prompt 中都要明确包含同样的 subject / palette / lighting / location 关键词**,仅变化镜头与动作
|
|
|
+- 你需要先在脑内确定「主角/主体」+「主色调(如暖琥珀色 / 冷青色)」+「场景地点」+「光线时段(如金色晨光 / 阴天漫射光)」
|
|
|
+- 上述 4 项一旦确定,**全部 ${this.nScenes} 段的 scene_prompt 中都要明确包含同样的主体、配色、光线、场景关键词**,仅变化镜头与动作
|
|
|
|
|
|
== 叙事骨架(按顺序对应每段位置)==
|
|
|
${arc}
|
|
|
@@ -896,8 +900,8 @@ ${arc}
|
|
|
- **严禁段落独立**:禁止每段自成一个小故事;解说必须前后呼应,把整支视频读成一段完整文章
|
|
|
|
|
|
== 字段格式 ==
|
|
|
-- scene_prompt: 英文 ≤30 词,必含 subject / palette / lighting / location 锚点 + 镜头语言(wide / close-up / tracking / low angle 等)${
|
|
|
- this.mediaMode === 'video' ? ',并加入轻微动作(slow zoom in / drift / wind / rising steam 等)以驱动 i2v' : ''
|
|
|
+- scene_prompt: 中文 ≤38 字,必含主体 / 配色 / 光线 / 场景锚点 + 镜头语言(如广角、特写、跟拍、低角度等)${
|
|
|
+ this.mediaMode === 'video' ? ',并加入轻微动作(缓慢推进、轻微漂移、风吹、蒸汽上升等)以驱动图生视频' : ''
|
|
|
}
|
|
|
- ${narrationRule}
|
|
|
|
|
|
@@ -956,7 +960,7 @@ ${arc}
|
|
|
${this.topic.trim()}
|
|
|
|
|
|
已有全局设定(为空则你补全):
|
|
|
-${existingBible || 'none'}
|
|
|
+${existingBible || '无'}
|
|
|
|
|
|
视觉风格:
|
|
|
${styleHint}
|
|
|
@@ -983,14 +987,14 @@ ${arc}
|
|
|
只输出严格 JSON 对象,不要 markdown:
|
|
|
{
|
|
|
"visual_bible": {
|
|
|
- "subject": "English fixed person/subject appearance",
|
|
|
- "product": "English fixed product look/selling point, empty if none",
|
|
|
- "location": "English fixed location and spatial layout",
|
|
|
- "palette": "English fixed color palette",
|
|
|
- "lighting": "English fixed lighting",
|
|
|
- "camera": "English overall camera language",
|
|
|
- "style_notes": "English visual style",
|
|
|
- "constraints": "English things that must not change"
|
|
|
+ "subject": "中文填写固定人物/主体外观",
|
|
|
+ "product": "中文填写固定产品外观/卖点,无则留空",
|
|
|
+ "location": "中文填写固定场景和空间布局",
|
|
|
+ "palette": "中文填写固定色彩方案",
|
|
|
+ "lighting": "中文填写固定光线",
|
|
|
+ "camera": "中文填写整体镜头语言",
|
|
|
+ "style_notes": "中文填写视觉风格",
|
|
|
+ "constraints": "中文填写必须保持不变的要点"
|
|
|
},
|
|
|
"scenes": [
|
|
|
{
|
|
|
@@ -999,10 +1003,10 @@ ${arc}
|
|
|
"inherit_location": true,
|
|
|
"inherit_style": true,
|
|
|
"reference_mode": "auto | none | anchor | previous | current-head",
|
|
|
- "scene_prompt": "English <= 38 words, concrete subject/product action + composition + camera change",
|
|
|
- "start_state": "English state continuing from previous shot",
|
|
|
- "end_state": "English ending state for next shot",
|
|
|
- "continuity": "English connection to previous shot",
|
|
|
+ "scene_prompt": "中文,38 字以内,写清具体主体/产品动作、构图和镜头变化",
|
|
|
+ "start_state": "中文填写承接上一镜的起始状态",
|
|
|
+ "end_state": "中文填写留给下一镜承接的结束状态",
|
|
|
+ "continuity": "中文填写与上一镜的衔接关系",
|
|
|
"narration": "中文"
|
|
|
}
|
|
|
]
|
|
|
@@ -1044,7 +1048,7 @@ ${arc}
|
|
|
referenceMode: this.normalizeReferenceMode(item.reference_mode || item.referenceMode),
|
|
|
startState: String(item.start_state || item.startState || '').trim(),
|
|
|
endState: String(item.end_state || item.endState || '').trim(),
|
|
|
- continuity: String(item.continuity || (idx === 0 ? 'opening shot' : '')).trim(),
|
|
|
+ continuity: String(item.continuity || (idx === 0 ? '开场镜头' : '')).trim(),
|
|
|
narration: String(item.narration || '').trim(),
|
|
|
state: 'idle' as const,
|
|
|
videoMode: 'first' as const,
|
|
|
@@ -1371,7 +1375,7 @@ ${arc}
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
|
this.compositeHandle = this.browserFfmpeg.composeVideo({
|
|
|
segments,
|
|
|
- title: this.topic.trim() || 'topic-to-video',
|
|
|
+ title: this.topic.trim() || '主题生视频',
|
|
|
onStage: (e) => {
|
|
|
this.zone.run(() => {
|
|
|
if (e.message) this.stageStatus = e.message;
|
|
|
@@ -1596,7 +1600,7 @@ ${arc}
|
|
|
|
|
|
/**
|
|
|
* 手动模式:AI 生成尾帧(即梦文生图)。
|
|
|
- * 使用 scene.scenePrompt 作为输入,加 "ending frame, final shot of scene" 提示,
|
|
|
+ * 使用 scene.scenePrompt 作为输入,并补充尾帧提示,
|
|
|
* 让画面与首帧保持一致风格但作为「场景尾」。
|
|
|
*/
|
|
|
regenerateTailImage(index: number): void {
|