大家好,跟着老黄学AI!
最近有小伙伴问高级版的火柴人案例,今天,它来了!
按照惯例,我们先上效果图:
视频主体拆解
1.顶上有左标题,右标题,左标题还有个图标。这些元素我们可以选择作为用户输入,用户没输入,就默认
2.火柴人图片布局为左中右,按照字幕的时间线,轮流出现,而且顶部还有图片的描述。
3.第一期我们用的都是黑白纯色,这次的高级版有彩色的元素在里面。
4.口播的字幕有中英对照
流程设计
1.根据输入的文案或者大模型产生的文案,用大模型进行分镜描述,批量产出图片。
2.处理音频,动画效果、中英翻译字幕、生成背景图
3.小助手插件生成草稿
核心节点拆解
(1)开始节点,为什么开始节点要特意讲解,因为如果我们想这个工作流越灵活,那么给用户自定义使用的输入,就要越多。
(2)大模型_文案生成
参考提示词:
# 角色
心理学知识分享博主,作为心理学知识分享博主,善于通过理论解读和案例分析,帮助读者理解心理现象背后的原理,用通俗易懂的方式呈现心理学的智慧。
你是一个专门制作以火柴人形式呈现心理知识相关视频内容的专家。
## 技能 创作心理学科普文案
-目标:依据用户提供的心理主题方向,创作具有启发性和实用性的的心理科普文章
-文章结构:采用总分结构,开篇引出主题,分段阐述心理学理论与实际应用,结合案例进行说明,结尾总结要点并提供行动建议。
-案例生动呈现:选取贴近生活的案例,从情境描述、心理分析、应对策略等维度展开多角度剖析。
-语言风格:保持专业性和通俗性平衡,关键理论突出解释,专业术语配合括号解释,段落间逻辑清晰。
-论证方式:采用经典心理学理论框架,注重理论与实践结合,避免过度 简化或夸大。
同样的话不要 反复说,不要重复
输出格式:采用合理长短句结构,不需要有数字分段,直接输出文章内容,标题和1个核心关键词。
- 字数300字以内
(3)大模型_分镜描述:
参考提示词:
# 角色
你是一位专业且富有创意的视频分镜描述专家,专注于火柴人风格的心理学视频分镜创作,能够将心理学科普文章转化为生动、形象且符合极简风格要求的视频分镜描述。
## 技能
### 技能 1: 创作视频分镜描述
1. 仔细研读用户提供的心理学科普文章内容,全面理解其中的心理学知识、情节以及人物情绪等关键要素。
2. 按照要求创作心理学视频分镜描述,确保:
- 字幕文案分段:每个段落均由一句话构成,语句简洁明了,表达清晰流畅,同时具备节奏感。
- 分镜描述:画面呈现极简风格,清晰直观。画面描述要精准、细致地体现情节细节以及人物情绪等方面。每个人物的描述中都要加上火柴人形象。
- 字幕文案必须严格按照用户给的文案拆分,不能禁止严禁修改提供的内容
- 至少8个分镜, 不超过50个分镜
### 技能 2: 生成分镜图像提示词
依据分镜描述,生成对应的[分镜图像提示词],需严格遵循以下要求:
{{llm_prompt}}
(4)背景图_设置:
(5)大模型_中英翻译
参考提示词:
# 角色
你是一位专业的翻译专家,专长于将中文文本精准且流畅地翻译成英文。
## 技能
### 技能 1: 翻译中文内容
1. 当接收到中文内容后,将其准确翻译成英文,要确保译文完整保留原文的意义、语气以及风格, 中文内容在json数组中,需要将数组中的每句中文单独翻译。
2. 在翻译过程中,充分留意中文的语境和文化内涵,让英文表达既忠实于原文,又契合英语习惯。
## 输出要求
1. 翻译后的英文文本务必符合英语语法规范,表达清晰、语句流畅,具备良好的可读性。
2. 准确传达原文的所有信息,杜绝随意添加或删减内容。
3. 若遇到中文习语或具有文化特色的内容,采用符合英语表达习惯的译法,保证信息完整且传达效果良好。
## 注意事项
1. 对于多义词或模糊表达,保证译文明确、连贯且前后一致。
2. 若直译可能产生歧义,则采用意译方法,同时保证原意不变。
(6)动画效果与视频参数组成
| |
(7)调用小助手数据生成器与小助手插件,这个不展开说了,大家可以看看我上一篇文章,有详细介绍。
(8)代码节点
参考代码:
// 在这里,您可以通过 ‘params’ 获取节点中的输入变量,并通过 'ret' 输出结果
// 'params' 和 'ret' 已经被正确地注入到环境中
// 下面是一个示例,获取节点输入中参数名为‘input’的值:
// const input = params.input;
// 下面是一个示例,输出一个包含多种数据类型的 'ret' 对象:
// const ret = { "name": ‘小明’, "hobbies": [“看书”, “旅游”] };
async function main({ params }: Args): Promise<Output> {
const { image_list, list, audio_list, duration_list, bg_image } = params;
// 处理音频数据
const audioData = [];
let audioStartTime = 0;
const videoTimelines = [];
let maxDuration = 0;
for (let i = 0; i < audio_list.length && i < duration_list.length; i++) {
const duration = duration_list[i];
audioData.push({
audio_url: audio_list[i],
duration,
start: audioStartTime,
end: audioStartTime + duration,
audio_effect: "教学"
});
videoTimelines.push({
start: audioStartTime,
end: audioStartTime + duration
});
audioStartTime += duration;
maxDuration = audioStartTime;
}
// 处理图片数据
const imageData = [];
// const imgData = processImageSequence(image_list, duration_list);
// 使用示例
const scheduler = new AnimationScheduler();
// 配置为顺序模式
// configure({
// mode: ANIMATION_MODES.SEQUENTIAL,
// animationPreset: 'SEQUENCE'
// });
// // 示例1:创建对称动画布局
// configure({
// mode: ANIMATION_MODES.SYMMETRIC,
// animationPreset: 'SYMMETRIC',
// symmetricPairing: 'LR', // 左右对称配对
// trackWeights: { left: 2, middle: 1, right: 2 } // 增加左右轨道权重
// });
// 示例3:完全随机模式配置
// configure({
// mode: ANIMATION_MODES.RANDOM,
// animationPreset: 'RANDOM',
// overlapTolerance: 0.6, // 允许更多重叠
// groupSyncThreshold: 3 // 每5个元素形成一组
// });
// // 示例4:自定义顺序模式
// configure({
// mode: ANIMATION_MODES.SEQUENTIAL,
// sequentialOrder: ['middle', 'left', 'right'], // 自定义播放顺序
// trackWeights: { left: 1, middle: 3, right: 1 } // 中间轨道权重最高
// });
const type = params.type;
if(type == "中心聚焦模式"){
// 示例2:配置中心聚焦模式
// configure({
// mode: ANIMATION_MODES.CENTER_FOCUS,
// animationPreset: 'FOCUS',
// durationSettings: {
// baseExtension: 800000, // 扩展800ms
// minDuration: 1500000 // 最小1500ms
// }
// });
}else if(type=="随机模式"){
// 示例3:完全随机模式配置
// configure({
// mode: ANIMATION_MODES.RANDOM,
// animationPreset: 'RANDOM',
// overlapTolerance: 0.6, // 允许更多重叠
// groupSyncThreshold: 3 // 每5个元素形成一组
// });
} else if(type=="左中右布局模式"){
// 示例1:创建对称动画布局
//configure({
// mode: ANIMATION_MODES.SYMMETRIC,
// animationPreset: 'SYMMETRIC',
// symmetricPairing: 'LMR', // 左中右配对
// trackWeights: { left: 2, middle: 1, right: 2 } // 增加左右轨道权重
//});
} else{
// 默认顺序模式
}
// 处理图片序列
const imgData = scheduler.process(image_list,duration_list);
// 处理背景图片
const bgImageData = [
{
image_url: bg_image,
width: 1920,
height: 1080,
start: 0,
end: maxDuration + 600000
}
];
// 处理字幕数据
const captions = list.map(item => item.cap);
const subtitleDurations = duration_list;
const processedSubtitles = [];
const processedSubtitleDurations = [];
for (let i = 0; i < captions.length; i++) {
const text = captions[i];
const totalDuration = subtitleDurations[i];
const subtitles = text.split(/[,。!,!?]/).filter(part => part.trim() !== '');
const subtitleCount = subtitles.length;
if (subtitleCount > 0) {
const perSubtitleDuration = totalDuration / subtitleCount;
for (let j = 0; j < subtitleCount; j++) {
processedSubtitles.push(subtitles[j]);
processedSubtitleDurations.push(perSubtitleDuration);
}
} else {
processedSubtitles.push(text);
processedSubtitleDurations.push(totalDuration);
}
}
// 处理字幕时间线
const textTimelines = [];
let textStartTime = 0;
for (const duration of processedSubtitleDurations) {
const endTime = textStartTime + duration;
textTimelines.push({
start: textStartTime,
end: endTime
});
textStartTime = endTime;
}
// 构建输出对象
const result = {
audio_list: JSON.stringify(audioData),
image_list: JSON.stringify(imageData),
timelines: videoTimelines,
text_timelines: textTimelines,
text_cap: processedSubtitles,
max_time: maxDuration,
bg_image: JSON.stringify(bgImageData),
imageDataLeft:JSON.stringify(imgData.left),
imageDataRight:JSON.stringify(imgData.right),
imageDataMiddle:JSON.stringify(imgData.middle),
imgData: imgData
};
return result;
}
/*
* 入场动画预设池
* 说明:定义不同布局模式下的可用动画类型组合
* 结构:模式名 -> 轨道位置 -> 可选的动画类型数组
*/
const ANIMATION_PRESETS = {
// 顺序模式预设:左中右轨道使用不同动画组合
SEQUENCE: {
left: ["向右滑动", "放大"], // 左轨道可选动画
middle: ["向上滑动", "放大"], // 中间轨道可选动画
right: ["向左滑动", "向左转入"] // 右轨道可选动画
},
// 中心聚焦模式预设:中间轨道优先配置
FOCUS: {
middle: ["放大", "向上滑动"], // 中间轨道使用更突出的动画
left: ["向右滑动", "放大"],
right: ["向左滑动", "向左转入"]
},
// 对称模式预设:左右轨道使用对称动画组合
SYMMETRIC: {
left: ["向右滑动", "放大"],
right: ["向左滑动", "向左转入"],
middle: ["向上滑动", "放大"],
},
// 随机模式预设:所有位置共享动画池
RANDOM: {
all: ["向右滑动", "放大", "向下滑动","向左转入"] // 全轨道共用动画池
}
};
/*
* 动画模式枚举
* 说明:定义不同的动画调度策略模式
*/
const ANIMATION_MODES = {
SEQUENTIAL: 'sequential', // 顺序模式:按指定顺序播放
CENTER_FOCUS: 'center', // 中心聚焦模式:中间元素优先
SYMMETRIC: 'symmetric', // 对称模式:左右对称播放
RANDOM: 'random' // 随机模式:完全随机选择
};
/*
* 全局配置参数
* 示例用法:
* configure({
* mode: ANIMATION_MODES.SYMMETRIC,
* animationPreset: 'SYMMETRIC',
* trackWeights: { left: 2, middle: 1, right: 2 }
* })
*/
const CONFIG = {
mode: ANIMATION_MODES.SEQUENTIAL, // 动画调度模式(默认顺序模式)
animationPreset: 'SEQUENCE', // 使用的动画预设名称
trackWeights: { left: 1, middle: 1, right: 1 }, // 轨道选择权重(中间轨道权重更高)
overlapTolerance: 1, // 重叠容忍度(0-1,值越大允许更多重叠)
groupSyncThreshold: 3, // 组同步阈值(元素数量达到阈值后同步)
sequentialOrder: ['left', 'middle', 'right'], // 顺序模式的播放顺序
symmetricPairing: 'LR', // 对称模式配对方式(LR: 左右对称 / LMR: 包含中间)
durationSettings: { // 时长相关配置
unit: 1, // 时间单位系数(示例:1表示1ms)
baseExtension: 500000, // 基础时长扩展(500ms)
minDuration: 1000000, // 最小持续时间(1000ms)
groupRatio: 1.8 // 组时长比例系数
}
};
// 核心处理器
class AnimationScheduler {
constructor() {
this.tracks = { left: [], middle: [], right: [] };
this.timeWindows = [];
this.groups = [];
this.currentGroup = null;
}
process(imageList, durationList) {
imageList.forEach((img, idx) => {
this._processImage(img, idx, durationList);
});
this._postProcess();
return this._formatOutput();
}
_processImage(image, index, durations) {
const start = this._getStartTime(index, durations);
const originalDuration = durations[index] * CONFIG.durationSettings.unit;
this._createGroupIfNeeded(index);
const track = this._selectTrack(start, originalDuration);
const { endTime, groupEnd } = this._calculateTiming(start, originalDuration);
const animation = this._selectAnimation(track);
const item = this._createItem(image, track, start, endTime, animation);
this._updateState(item, groupEnd);
}
_createGroupIfNeeded(index) {
let shouldCreate = false;
// 顺序模式专用分组逻辑
if (CONFIG.mode === ANIMATION_MODES.SEQUENTIAL) {
const groupSize = CONFIG.sequentialOrder.length;
shouldCreate = (index % groupSize === 0) || (this.groups.length === 0);
}
// 其他模式原有逻辑
else {
shouldCreate =
index % 3 === 0 ||
(CONFIG.mode === ANIMATION_MODES.SYMMETRIC && index % 2 === 0) ||
this.groups.length === 0;
}
if (shouldCreate) {
this.currentGroup = {
items: [],
animationType: this._getGroupAnimationType(),
endTime: 0
};
this.groups.push(this.currentGroup);
}
}
_getGroupAnimationType() {
const preset = ANIMATION_PRESETS[CONFIG.animationPreset];
switch(CONFIG.mode) {
case ANIMATION_MODES.SEQUENTIAL:
return {
left: this._randomPick(preset.left),
middle: this._randomPick(preset.middle),
right: this._randomPick(preset.right)
};
case ANIMATION_MODES.CENTER_FOCUS:
return {
middle: this._randomPick(preset.middle),
left: this._randomPick(preset.left),
right: this._randomPick(preset.right)
};
case ANIMATION_MODES.SYMMETRIC:
return CONFIG.symmetricPairing === 'LR' ? {
left: this._randomPick(preset.left),
right: this._randomPick(preset.right)
} : {
left: this._randomPick(preset.left),
middle: this._randomPick(preset.middle),
right: this._randomPick(preset.right)
};
case ANIMATION_MODES.RANDOM:
return { all: this._randomPick(preset.all) };
default:
throw new Error('未知动画模式');
}
}
_selectTrack(start, duration) {
// 新增顺序模式专用处理逻辑
if (CONFIG.mode === ANIMATION_MODES.SEQUENTIAL) {
const groupSize = CONFIG.sequentialOrder.length;
const positionInGroup = this.currentGroup.items.length % groupSize;
const targetTrack = CONFIG.sequentialOrder[positionInGroup];
// 强制等待机制:如果当前轨道冲突,延迟到下一个可用时间点
const lastInTrack = [...this.tracks[targetTrack]].pop();
if (lastInTrack && lastInTrack.end > start) {
start = lastInTrack.end + 100000; // 增加100ms间隔
}
return targetTrack;
}
// 保留其他模式的原有逻辑
const candidates = Object.keys(this.tracks).map(track => ({
track,
score: this._calculateTrackScore(track, start, start + duration)
}));
return candidates.sort((a, b) => b.score - a.score)[0].track;
}
_fallbackToOriginalLogic(start, duration) {
const candidates = Object.keys(this.tracks).map(track => ({
track,
score: this._calculateTrackScore(track, start, start + duration)
}));
return candidates.sort((a, b) => b.score - a.score)[0].track;
}
_calculateTrackScore(track, start, end) {
const overlapCount = this.tracks[track]
.filter(item => item.end > start && item.start < end)
.length;
const recentUsage = this.tracks[track]
.filter(item => item.end > start - 500000)
.length;
const weights = {
availability: overlapCount === 0 ? 400 : 0,
overlap: -overlapCount * 150,
recent: -recentUsage * 50,
configWeight: CONFIG.trackWeights[track] * 200,
random: Math.random() * 100
};
return Object.values(weights).reduce((sum, val) => sum + val, 0);
}
_calculateTiming(start, originalDuration) {
const baseEnd = start + originalDuration;
let endTime = baseEnd + CONFIG.durationSettings.baseExtension;
// 顺序模式强制组同步
if (CONFIG.mode === ANIMATION_MODES.SEQUENTIAL) {
// 始终保证最小持续时间
const minEnd = start + CONFIG.durationSettings.minDuration;
endTime = Math.max(endTime, minEnd);
// 动态扩展组持续时间
if (this.currentGroup.items.length > 0) {
// 取当前元素需要的结束时间和组现有结束时间的较大者
endTime = Math.max(endTime, this.currentGroup.endTime);
}
// 更新组结束时间为当前最大值
this.currentGroup.endTime = Math.max(this.currentGroup.endTime || 0, endTime);
endTime = this.currentGroup.endTime;
} else {
// 保留原有逻辑
if (this.currentGroup.items.length > 0) {
const lastItem = this.currentGroup.items[this.currentGroup.items.length - 1];
endTime = Math.max(endTime, lastItem.end);
}
endTime = Math.max(
endTime,
start + CONFIG.durationSettings.minDuration
);
}
return {
endTime,
groupEnd: CONFIG.mode === ANIMATION_MODES.SYMMETRIC ? endTime : null
};
}
_createItem(image, track, start, end, animation) {
return {
image_url: image,
in_animation: animation,
start: this._adjustTiming(start, track),
end: this._adjustTiming(end, track),
width: 1920,
height: 1080,
track
};
}
_adjustTiming(time, track) {
if (CONFIG.mode === ANIMATION_MODES.SYMMETRIC) {
return track === 'right' ? time + 100000 : time;
}
return time;
}
_selectAnimation(track) {
if (CONFIG.mode === ANIMATION_MODES.RANDOM) {
return this.currentGroup.animationType.all;
}
return this.currentGroup.animationType[track];
}
_updateState(item, groupEnd) {
this.tracks[item.track].push(item);
this.currentGroup.items.push(item);
// 强制同步组内所有元素的结束时间
this.currentGroup.items.forEach(i => i.end = this.currentGroup.endTime);
this.currentGroup.endTime = Math.max(
this.currentGroup.endTime,
groupEnd || item.end
);
}
_postProcess() {
this._adjustTrackOverlaps();
this._balanceDurations();
}
_adjustTrackOverlaps() {
Object.values(this.tracks).forEach(track => {
track.sort((a, b) => a.start - b.start);
for (let i = 1; i < track.length; i++) {
const prev = track[i - 1];
const curr = track[i];
if (prev.end > curr.start) {
const adjust = (prev.end - curr.start) * CONFIG.overlapTolerance;
prev.end -= adjust;
curr.start = prev.end;
}
}
});
}
_balanceDurations() {
if (CONFIG.mode === ANIMATION_MODES.CENTER_FOCUS) {
this.tracks.middle.forEach(item => {
item.end += 300000; // 延长中间元素
});
}
}
_getStartTime(index, durations) {
return index === 0 ? 0 : durations
.slice(0, index)
.reduce((sum, dur) => sum + dur * CONFIG.durationSettings.unit, 0);
}
_formatOutput() {
return Object.entries(this.tracks).reduce((acc, [track, items]) => {
acc[track] = items.map(item => ({
image_url: item.image_url,
in_animation: item.in_animation,
start: item.start,
end: item.end,
width: item.width,
height: item.height
}));
return acc;
}, { left: [], middle: [], right: [] });
}
_randomPick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
}
/*
* 配置接口函数
* 用法示例:动态修改配置参数
* configure({
* mode: ANIMATION_MODES.RANDOM,
* overlapTolerance: 0.5,
* durationSettings: {
* minDuration: 2000000 // 设置最小持续时间为2000ms
* }
* })
*/
function configure(options) {
Object.assign(CONFIG, options);
}
// --------------------------
// 主要配置参数使用示例说明:
// --------------------------
// // 示例1:创建对称动画布局
// configure({
// mode: ANIMATION_MODES.SYMMETRIC,
// animationPreset: 'SYMMETRIC',
// symmetricPairing: 'LR', // 左右对称配对
// trackWeights: { left: 2, middle: 1, right: 2 } // 增加左右轨道权重
// });
// // 示例2:配置中心聚焦模式
// configure({
// mode: ANIMATION_MODES.CENTER_FOCUS,
// animationPreset: 'FOCUS',
// durationSettings: {
// baseExtension: 800000, // 扩展800ms
// minDuration: 1500000 // 最小1500ms
// }
// });
// // 示例3:完全随机模式配置
// configure({
// mode: ANIMATION_MODES.RANDOM,
// animationPreset: 'RANDOM',
// overlapTolerance: 0.6, // 允许更多重叠
// groupSyncThreshold: 5 // 每5个元素形成一组
// });
// // 示例4:自定义顺序模式
// configure({
// mode: ANIMATION_MODES.SEQUENTIAL,
// sequentialOrder: ['middle', 'left', 'right'], // 自定义播放顺序
// trackWeights: { left: 1, middle: 3, right: 1 } // 中间轨道权重最高
// });
// // 使用示例
// const scheduler = new AnimationScheduler();
// // 配置为中心聚焦模式
// configure({
// mode: ANIMATION_MODES.CENTER_FOCUS,
// animationPreset: 'FOCUS'
// });
// // 处理图片序列
// const result = scheduler.process(
// ['img1.jpg', 'img2.jpg', 'img3.jpg'],
// [1000, 1500, 1200]
// );
以上就是本期的所有内容!
想要完整工作流、完整代码、提示词的,请关注我的同名小绿书领取
我在Coze商店里面上架了该类视频的两种工作流。目前是免费使用的,大家可以去体验。这里是Coze应用体验链接:https://www.coze.cn/s/4-UQdusJPU0/