效果展示

搭建过程

创建工作流

登录扣子官网,创建一个工作流地址:https://www.coze.cn/home
![请添加图片描述](https://i-blog.csdnimg.cn/direct/638bb795fb0a4a88b597122fb31e3ef5.gi

第 1 步:设置开始节点

参数设置:

  • wenan(字符串):自定义文案,不填就让 AI 自动生成
  • AIPkey(字符串):必填,填你的生成图片、视频、语音用的密钥
  • subject(字符串):选题,比如偷窥

第 2 步:判断用户有没有自己填文案

  • 从「开始」拉出一个新的【选择器】节点。重命名为:"2-选择器"

参数设置:

  • 分支条件:引用「开始」输出的wenan
  • 条件逻辑:当wenan为空,或者wenan等于-时,走生成文案分支
  • 否则:直接走用户自定义文案分支

第 3 步:没填文案时,根据选题自动生成口播稿

  • 从「2-选择器」的满足条件分支拉出一个新的【大模型】节点。重命名为:"3-根据主题生成文案"

模型选择:

  • 模型:豆包·1.8·深度思考

参数设置:

  • subject(字符串):引用「开始」输出的subject

系统提示词:

你是一个不仅没有感情,甚至对人类社会的虚伪感到生理性厌恶的观察者。你不仅能看到事物的表象,更能像手术刀一样划开社会契约的表皮,暴露出内里涌动的欲望、腐烂的真相和令人作呕的潜规则。你的语言风格属于“梦核(Dreamcore)/核类(Corecore)”美学:冷漠、致郁、充满生物性的隐喻和对伪善的极致嘲讽。

# Goal:
接收用户给定的【选题/关键词】,将其作为“输入”,输出一段具有强烈的视听认知失调感、揭露人性阴暗面或社会荒诞性的“定义”。

# Rules & Constraints:
1.  **结构强制:** 必须严格遵守最后仅输出口播稿,此外不要输出任何其他解释。
2.  **拒绝说教:** 绝对不要出现“我们要”、“你应该”、“切记”这种建议性的话术。你是来审判的,不是来当人生导师的。
3.  **生物性隐喻(关键):** 必须使用具有实体感、痛感或令人不适的名词/动词(例如:蛆虫、撕开、福尔马林、缝合、流脓、窥视、咀嚼、寒毛直竖、腥味)。
4.  **反转与解构:** “输出”的内容必须是对“输入”词汇的背叛。如果输入是美好的,输出就是恐怖的;如果输入是中性的,输出就是荒谬的。
5.  **冷漠的句式:** 多用陈述句,或者带有嘲讽意味的反问句。不要用感叹号,情绪要压抑在文字底下,像死水下的暗流。
6.  **篇幅控制:** 每句话小于10个字, 总字数在50-100字左右,短小精悍,像一记耳光。

# Methodology (思维路径):
1.  **剥皮:** 拿到关键词,先思考大众对它的常规理解是什么。
2.  **透视:** 思考这个词在极端现实、利益交换或人性本恶的视角下,真正意味着什么?
3.  **通感:** 给这个残酷的真相配上气味、触觉或视觉画面。
4.  **封装:** 用最冷静的口吻把这个画面描述出来。


# 示例输入/输出:
## 示例输入:
偷窥
## 示例输出:
有的人长得像人,但未必真的是人。他们不出声,在黑暗中交换他人的裸影取乐。畜生是没有偷窥习惯的,撕开人皮,内里不过是一只只的蛆虫,在腐烂的欲望里蠕动着。

## 示例输入:
做自己
## 示例输出:
人们总是劝你做自己,但当你真的做了自己,他们又开始评价你的自己是否符合他们的标准。原来那句话完整的是:请在我允许的范围内做你自己。

## 示例输入:
真招笑
## 示例输出:
你的鼻子已经闻到了欺骗的味道,你的耳朵也听到了谎言的警告,甚至连皮肤都警惕地竖起了寒毛,但你最终却选择相信你的眼睛。真招笑。

## 示例输入:
怀旧
## 示例输出:
你怀念的根本不是那个夏天,而是那个还没被现实阉割的自己。记忆像是加了柔光滤镜的尸体,你抱着它痛哭,却忘了它活着的时候,你也曾想过掐死它。

用户提示词:

故事主题:{{subject}}

输出变量:

  • wenan(字符串)

第 4 步:把“AI生成文案”和“用户自填文案”合并成一份正文

  • 从「3-根据主题生成文案」拉到新的【变量聚合】节点。重命名为:"4-变量聚合"
  • 再从「2-选择器」的否则分支也拉到这个【变量聚合】节点。

参数设置:

  • content(字符串):优先接「3-根据主题生成文案」输出的wenan
  • content(字符串):否则接「开始」输出的wenan

第 5 步:把整段口播稿切成一句一句

  • 从「4-变量聚合」拉出一个新的【插件】节点。插件市场搜剪映小助手辅助工具,选text_splitter。重命名为:"5-播稿分割"

参数设置:

  • text(字符串):引用「4-变量聚合」输出的content

第 6 步:给每一句话生成画面提示词、视频提示词、音效关键词

  • 从「5-播稿分割」拉出一个新的【大模型】节点。重命名为:"6-图像提示词"

模型选择:

  • 模型:豆包·1.8·深度思考

参数设置:

  • content(字符串列表):引用「5-播稿分割」输出的segments

系统提示词:

# 角色
将输入的文案内容按每一句话生成对应的分镜字幕绘画提示词desc_prompt和视频提示词video_prompt。

## 技能
### 技能 1:  生成绘图提示词
1. 把输入的整体文案“{{content}}”按每一句话(每个句号为一句话), 然后生成一个提示词,针对每一句文案,生成对应字幕“{{cap}}”的绘画提示词 desc_promopt。梦核风格,拼接艺术,叠加老电视信号干扰的颗粒噪点,加入角色,融入复古拼贴元素,带剪纸边缘轮廓,营造出一种奇幻又略带暗黑诡异的氛围,契合梦核风格追求的梦幻且神秘的视觉感受。

【音效要求】
- 必须生成真实自然环境音效,与画面内容完全匹配
- 自然声音:鸟鸣、蝉声、青蛙叫、风吹叶响、小溪水流声、大雨声、虫鸣
- 交通工具:火车鸣笛声、火车铁轨声、汽车引擎声、汽车喇叭、火车运行声
- 生活音效:脚步声、开门声、茶具声、翻书声、呼吸声、心跳声
- 城市环境:远处车流、工地声、人声喧哗、钟声
- 严禁生成莫名其妙的噪音或不明音效
- 只能输出一种声音
举例:
1:梦核风格,拼接艺术,背景是蓝色渐变褶皱纸张纹理,叠加老电视信号干扰的颗粒噪点,左侧:高饱和粉紫色带剪纸边缘轮廓的手,从屏幕外手指前伸指向右侧,营造手指从无到有的动画效果,右侧:高饱和无面部齐刘海黑发女性,极简剪影化,融入白色的复古拼贴元素,带剪纸边缘轮廓 ,营造超现实怀旧日 +荒诞感,模拟 复古电子屏的颗粒纹理与
灵感向导 "sound_kv": "信号干扰声音"

### 技能2:生成视频提示词
1. 根据技能1生成的绘图提示词,生成对应字幕“{{cap}}”的视频提示词 video_prompt,按照以下规则详细描述:
    - 对于每个镜头,明确给出镜头的时间区间,如“第 X 个镜头(X - X 秒)”。
    - 说明镜头的景别,如“远景(平面)”“中景(平面)”“特写(平面)”“近景(平面)” 。
    - 详细描述场景元素,包括背景、角色形象(如发型、面部颜色、服装颜色等)、周边装饰等的颜色及样式。
    - 描述角色的动作,例如双手捧出、躺倒、双手交叉等。
    - 强调镜头整体呈现出的风格特点,例如画面色调氛围等方面所传达出的情感风格。


### 技能3:检查空字符
1. 输出的json中cap和desc_promopt都不能为空


输出json格式示例:
{
    "cap": "{{cap}}",
    "desc_promopt": "分镜图像提示词",
    "video_prompt": "视频提示词",
    "sound_kv": "车厢轻摇声"
}


## 限制:
- 只对用户提供的json内容补充desc_prompt字段,不能更改原文
- 严格检查输出的 json 格式正确性并进行修正,特别注意 json 格式不要少括号,逗号等
- 输出的提示词必须是中文
-禁止出现违规词语

输出变量:

  • outputList(对象数组)
    • sound_kv(字符串)
    • video_prompt(字符串)
    • cap(字符串)
    • desc_promopt(字符串)

第 7 步:过滤掉没有字幕的空镜头

  • 从「6-图像提示词」拉出一个新的【代码】节点。重命名为:"7-过滤输出列表非空字幕项"

参数设置:

  • outputList(对象数组):引用「6-图像提示词」输出的outputList

代码直接抄:

async function main({ params }: Args): Promise<Output> {
    const list = params.outputList;

    const ret = {
        "outputList": list.filter(item => item.cap != null && item.cap !== "")
    };

    return ret;
}

输出变量:

  • outputList(对象数组)
    • cap(字符串)
    • desc_promopt(字符串)
    • sound_kv(字符串)
    • video_prompt(字符串)

第 8 步:批量给每个分镜生成图片和音效

  • 从「7-过滤输出列表非空字幕项」拉出一个新的【批处理】节点。重命名为:"8-批处理"

参数设置:

  • scenes(对象数组):引用「7-过滤输出列表非空字幕项」输出的outputList
  • 批处理大小:100
  • 并发数量:1

输出变量:

  • image_list(字符串列表)
  • selected_url_list(字符串列表)

第 8.1 步:在批处理里先按音效关键词搜索音效

  • 在「8-批处理」内部,从「【入口】」拉出一个新的【插件】节点。插件市场搜搜索音效,选search_sound_effect。重命名为:"8.1-搜索音频"

参数设置:

  • keyword(字符串):引用「8-批处理」当前项的scenes.sound_kv

第 8.2 步:从搜索结果里拿第一个可用音效链接

  • 在「8-批处理」内部,从「8.1-搜索音频」拉出一个新的【代码】节点。重命名为:"8.2-选择音效URL"

参数设置:

  • data(对象数组):引用「8.1-搜索音频」输出的data

代码直接抄:

async function main({ params }: Args): Promise<Output> {
    const ret = {
        selected_url: ""
    };

    try {
        if (!params.data || !Array.isArray(params.data) || params.data.length === 0) {
            throw new Error("输入数据不能为空且必须为数组");
        }

        const validSongs = params.data.filter(song =>
            song && typeof song.url === 'string' && song.url.trim() !== ''
        );

        if (validSongs.length === 0) {
            throw new Error("没有找到包含有效URL的歌曲");
        }

        const selectedSong = validSongs[0];
        ret.selected_url = selectedSong.url.trim();

    } catch (error) {
        console.error("随机选择歌曲URL时发生错误:", error);
        ret.selected_url = "";
    }

    return ret;
}

输出变量:

  • selected_url(字符串)

第 8.3 步:准备 5 张固定参考图

  • 在「8-批处理」内部,从「【入口】」拉出一个新的【代码】节点。重命名为:"8.3-图片路径组合"

参数设置:

  • input1(字符串):图片链接1
  • input2(字符串):图片连接2
  • input3(字符串):图片链接3
  • input4(字符串):图片链接4
  • input5(字符串):图片链接5

代码直接抄:

async function main({ params }: Args): Promise<Output> {
    var img_url1 = params.input1
    var img_url2 = params.input2
    var img_url3 = params.input3
    var img_url4 = params.input4
    var img_url5 = params.input5
    let image_url_list = []
    image_url_list.push(img_url1,img_url2,img_url3,img_url4,img_url5)

    const ret = {
        "images_urls": image_url_list,
    };

    return ret;
}

输出变量:

  • images_urls(字符串列表)

第 8.4 步:拼出图片生成提示词

  • 在「8-批处理」内部,从「8.3-图片路径组合」拉出一个新的【文本处理】节点。重命名为:"8.4-图片提示词"

参数设置:

  • String1(字符串):引用「8-批处理」当前项的scenes.desc_promopt

拼接模板这样填:

参照图片风格,色彩,边框{{String1}}

第 8.5 步:第一次尝试生成分镜图片

  • 在「8-批处理」内部,从「8.4-图片提示词」拉出一个新的【插件】节点。插件市场搜即梦图片生成,选jimeng_generate_image。重命名为:"8.5-图片生成"

参数设置:

  • key(字符串):引用「开始」输出的AIPkey
  • prompt(字符串):引用「8.4-图片提示词」输出的output
  • image_urls(字符串列表):引用「8.3-图片路径组合」输出的images_urls
  • model(字符串):图片4.0
  • ratio(字符串):4:3

第 8.6 步:判断第一次图片有没有生成成功

  • 在「8-批处理」内部,从「8.5-图片生成」拉出一个新的【选择器】节点。重命名为:"8.6-选择器"

参数设置:

  • 条件:引用「8.5-图片生成」输出的url
  • 如果url不为空:直接走成功分支
  • 否则:走提示词优化后重试分支

第 8.7 步:第一次图片失败时,优化图片提示词

  • 在「8-批处理」内部,从「8.6-选择器」的否则分支拉出一个新的【插件】节点。插件市场搜提示词优化,选sd_better_prompt。重命名为:"8.7-优化提示词"

参数设置:

  • prompt(字符串):引用「8-批处理」当前项的scenes.desc_promopt

第 8.8 步:用优化后的提示词再生成一次图片

  • 在「8-批处理」内部,从「8.7-优化提示词」拉出一个新的【插件】节点。插件市场搜即梦图片生成,选jimeng_generate_image。重命名为:"8.8-图片生成"

参数设置:

  • key(字符串):引用「开始」输出的AIPkey
  • prompt(字符串):引用「8.7-优化提示词」输出的data
  • image_urls(字符串列表):引用「8.3-图片路径组合」输出的images_urls
  • model(字符串):图片4.0
  • ratio(字符串):4:3

第 8.9 步:统一拿到最终图片链接

  • 在「8-批处理」内部,从「8.6-选择器」成功分支拉出一个新的【代码】节点。重命名为:"8.9-图片路径"
  • 在「8-批处理」内部,再从「8.8-图片生成」拉线到「8.9-图片路径」

参数设置:

  • image1(字符串):引用「8.5-图片生成」输出的url
  • image2(字符串):引用「8.8-图片生成」输出的url

代码直接抄:

async function main({ params }: Args): Promise<Output> {
    var image1 = params.image1;
    var image2 = params.image2;
    if(!image1){
        image1 = image2;
    }

    const ret = {
        "image_url":  image1
    };

    return ret;
}

输出变量:

  • image_url(字符串)

第 9 步:批量把图片变成视频

  • 从「8-批处理」拉出一个新的【批处理】节点。重命名为:"9-批处理视频"

参数设置:

  • images_url(字符串列表):引用「8-批处理」输出的image_list
  • scenes(对象数组):引用「7-过滤输出列表非空字幕项」输出的outputList
  • 批处理大小:100
  • 并发数量:1

输出变量:

  • video_urls(字符串列表)

第 9.1 步:先直接按视频提示词生成视频

  • 在「9-批处理视频」内部,从「【入口】」拉出一个新的【插件】节点。插件市场搜即梦视频生成,选jimeng_generate_video。重命名为:"9.1-视频生成"

参数设置:

  • key(字符串):引用「开始」输出的AIPkey
  • prompt(字符串):引用「9-批处理视频」当前项的scenes.video_prompt
  • duration(整数):5
  • image_url(字符串):引用「9-批处理视频」当前项的images_url
  • model(字符串):视频3.0
  • resolution(字符串):720p

第 9.2 步:判断第一次视频有没有生成成功

  • 在「9-批处理视频」内部,从「9.1-视频生成」拉出一个新的【选择器】节点。重命名为:"9.2-选择器"

参数设置:

  • 条件:引用「9.1-视频生成」输出的url
  • 如果url不为空:直接走成功分支
  • 否则:走提示词优化后重试分支

第 9.3 步:第一次视频失败时,优化视频提示词

  • 在「9-批处理视频」内部,从「9.2-选择器」的否则分支拉出一个新的【插件】节点。插件市场搜提示词优化,选sd_better_prompt。重命名为:"9.3-视频提示词优化"

参数设置:

  • prompt(字符串):引用「9-批处理视频」当前项的scenes.video_prompt

第 9.4 步:用优化后的提示词第二次生成视频

  • 在「9-批处理视频」内部,从「9.3-视频提示词优化」拉出一个新的【插件】节点。插件市场搜即梦视频生成,选jimeng_generate_video。重命名为:"9.4-视频生成"

参数设置:

  • key(字符串):引用「开始」输出的AIPkey
  • prompt(字符串):引用「9.3-视频提示词优化」输出的data
  • duration(整数):5
  • image_url(字符串):引用「9-批处理视频」当前项的images_url
  • model(字符串):视频3.0
  • resolution(字符串):720p

在这里插入图片描述

第 9.5 步:判断第二次视频有没有生成成功

  • 在「9-批处理视频」内部,从「9.4-视频生成」拉出一个新的【选择器】节点。重命名为:"9.5-选择器"

参数设置:

  • 条件:引用「9.4-视频生成」输出的url
  • 如果url不为空:走成功分支
  • 否则:继续优化提示词再重试

在这里插入图片描述

第 9.6 步:第二次失败时,再优化一次视频提示词

  • 在「9-批处理视频」内部,从「9.5-选择器」的否则分支拉出一个新的【插件】节点。插件市场搜提示词优化,选sd_better_prompt。重命名为:"9.6-视频提示词优化"

参数设置:

  • prompt(字符串):引用「9.3-视频提示词优化」输出的data

第 9.7 步:第三次生成视频

  • 在「9-批处理视频」内部,从「9.6-视频提示词优化」拉出一个新的【插件】节点。插件市场搜即梦视频生成,选jimeng_generate_video。重命名为:"9.7-视频生成"

参数设置:

  • key(字符串):引用「开始」输出的AIPkey
  • prompt(字符串):引用「9.6-视频提示词优化」输出的data
  • duration(整数):5
  • image_url(字符串):引用「9-批处理视频」当前项的images_url
  • model(字符串):视频3.0
  • resolution(字符串):720p

第 9.8 步:把三次可能生成出来的视频链接合并成最终视频链接

  • 在「9-批处理视频」内部,从「9.2-选择器」成功分支拉到新的【变量聚合】节点。重命名为:"9.8变量聚合"
  • 在「9-批处理视频」内部,从「9.5-选择器」成功分支也拉到「9.8变量聚合」
  • 在「9-批处理视频」内部,从「9.7-视频生成」也拉到「9.8变量聚合」

参数设置:

  • Group1(字符串):引用「9.1-视频生成」输出的url
  • Group1(字符串):引用「9.4-视频生成」输出的url
  • Group1(字符串):引用「9.7-视频生成」输出的url

在这里插入图片描述

第 10 步:循环给每一句字幕生成配音

  • 从「9-批处理视频」拉出一个新的【循环】节点。重命名为:"10-循环"

参数设置:

  • scenes(字符串列表):引用「5-播稿分割」输出的segments
  • 循环类型:数组循环
  • 循环次数:10

第 10.1 步:每次循环生成一段语音

  • 在「10-循环」内部,从「【入口】」拉出一个新的【插件】节点。插件市场搜fish语音,选generate_voice。重命名为:"10.1-音频"

参数设置:

  • key(字符串):引用「开始」输出的AIPkey
  • text(字符串):引用「10-循环」当前项的scenes
  • reference_id(字符串):561fcedfdf0e4e1399d1bc4930d50c0e

第 10.2 步:获取每段语音的时长

  • 在「10-循环」内部,从「10.1-音频」拉出一个新的【插件】节点。插件市场搜获取音频时长,选get_audio_duration。重命名为:"10.2-音频时长"

参数设置:

  • mp3_url(字符串):引用「10.1-音频」输出的data

第 11 步:根据配音列表生成音频时间线

  • 从「10-循环」拉出一个新的【插件】节点。插件市场搜剪映小助手,选audio_timelines。重命名为:"11-音频时间线"

参数设置:

  • links(字符串列表):引用「10-循环」输出的link_list

第 12 步:让字幕和配音时长对齐

  • 从「11-音频时间线」拉出一个新的【插件】节点。插件市场搜字幕音频对齐,选align_text_to_audio。重命名为:"12-音频字幕时间线对齐"

参数设置:

  • duration_list(整数列表):引用「10-循环」输出的duration_list
  • texts(字符串列表):引用「5-播稿分割」输出的segments

第 13 步:统一构建音频、图片、字幕、视频的时间轴数据

  • 从「12-音频字幕时间线对齐」拉出一个新的【代码】节点。重命名为:"13-构建时间轴"

参数设置:

  • image_list(字符串列表):引用「8-批处理」输出的image_list
  • audio_list(字符串列表):引用「10-循环」输出的link_list
  • duration_list(小数列表):引用「10-循环」输出的duration_list
  • scenes(对象数组):引用「6-图像提示词」输出的outputList
  • video_list(字符串列表):引用「9-批处理视频」输出的video_urls

代码直接抄:

async function main({ params }: Args): Promise<Output> {
    const { image_list, audio_list, duration_list, scenes ,video_list} = params;

    const audioData = [];
    let audioStartTime = 0;
    const aideoTimelines = [];
    let maxDuration = 0;

    const imageData = [];

    const video_timeline =[];
    let video_start = 0;
    for (let i = 0; i < video_list.length; i++) {
        video_timeline.push({
            start:video_start,
            end: video_start+5000000
        });
        video_start = video_start+5000000;
    }

    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,
            volume: 5
        });
        aideoTimelines.push({
            start: audioStartTime,
            end: audioStartTime + duration
        });

        if((i-1)%2==0){
            imageData.push({
                image_url: image_list[i],
                start: audioStartTime,
                end: audioStartTime + duration,
                width: 1024,
                height: 576,
                in_animation: "轻微放大",
                in_animation_duration: 100000
            });
        } else{
            imageData.push({
                image_url: image_list[i],
                start: audioStartTime,
                end: audioStartTime + duration,
                width: 1024,
                height: 576
            });
        }

        audioStartTime += duration;
        maxDuration = audioStartTime;
    }

    const captions = scenes.map(item => item.cap);
    const subtitleDurations = duration_list;

    const { textTimelines, processedSubtitles } = processSubtitles(
        captions,
        subtitleDurations
    );

    var kc_audio_url = "https://p9-bot-workflow-sign.byteimg.com/tos-cn-i-mdko3gqilj/c04e7b48586a48f1863e421be4b10cf1.MP3~tplv-mdko3gqilj-image.image?rk3s=81d4c505&x-expires=1777550323&x-signature=T%2BNjvPHPyHnGICvWRFDeFaj17UM%3D&x-wf-file_name=%E6%95%85%E4%BA%8B%E5%BC%80%E5%9C%BA%E9%9F%B3%E6%95%88.MP3";
    var bg_audio_url ="https://p3-bot-workflow-sign.byteimg.com/tos-cn-i-mdko3gqilj/5603dc783a6c4b75a4bf4e1b44086ad5.MP3~tplv-mdko3gqilj-image.image?rk3s=81d4c505&x-expires=1777550332&x-signature=E1123RzPTMD%2BipseRN4itYxhZyc%3D&x-wf-file_name=%E6%95%85%E4%BA%8B%E8%83%8C%E6%99%AF%E9%9F%B3%E4%B9%90.MP3";

    const bg_audio_data = [];
    bg_audio_data.push({
        audio_url: bg_audio_url,
        duraion: maxDuration,
        start: 0,
        end: maxDuration,
        volume: 2
    });

    const kc_audio_data = [];
    kc_audio_data.push({
        audio_url: kc_audio_url,
        duration: 4884897,
        start: 0,
        end: 4884897
    });

    const common_timelines = [{
        start: 0,
        end: maxDuration
    }];

    const ret = {
        "audioData": JSON.stringify(audioData),
        "bgAudioData": JSON.stringify(bg_audio_data),
        "kcAudioData": JSON.stringify(kc_audio_data),
        "imageData": JSON.stringify(imageData),
        "text_timielines":textTimelines,
        "text_captions":processedSubtitles,
        "common_timelines": common_timelines,
        "max_time": maxDuration,
        "aideoTimelines":aideoTimelines,
        "video_timeline": video_timeline
    };

    return ret;
}

const SUB_CONFIG = {
    MAX_LINE_LENGTH: 25,
    SPLIT_PRIORITY: ['。','!','?',',',',',':',':','、',';',';',' '],
    TIME_PRECISION: 3
};

function splitLongPhrase(text, maxLen) {
    if (text.length <= maxLen) return [text];

    for (const delimiter of SUB_CONFIG.SPLIT_PRIORITY) {
        const pos = text.lastIndexOf(delimiter, maxLen - 1);
        if (pos > 0) {
            const splitPos = pos + 1;
            return [
                text.substring(0, splitPos).trim(),
                ...splitLongPhrase(text.substring(splitPos).trim(), maxLen)
            ];
        }
    }

    const startPos = Math.min(maxLen, text.length) - 1;
    for (let i = startPos; i > 0; i--) {
        if (/[\p{Unified_Ideograph}]/u.test(text[i])) {
            return [
                text.substring(0, i + 1).trim(),
                ...splitLongPhrase(text.substring(i + 1).trim(), maxLen)
            ];
        }
    }

    const splitPos = Math.min(maxLen, text.length);
    return [
        text.substring(0, splitPos).trim(),
        ...splitLongPhrase(text.substring(splitPos).trim(), maxLen)
    ];
}

const processSubtitles = (
    captions,
    subtitleDurations,
    startTimeμs = 0
  ) => {
    const cleanRegex = /[\u3000\u3002-\u303F\uff00-\uffef\u2000-\u206F!"#$%&'()*+\-./=?@\\^_`{|}~]/g;

    let processedSubtitles = [];
    let processedSubtitleDurations = [];

    captions.forEach((text, index) => {
      const totalDuration = subtitleDurations[index];
      let phrases = splitLongPhrase(text, SUB_CONFIG.MAX_LINE_LENGTH);

      phrases = phrases.map(p => p.replace(cleanRegex, '').trim())
                     .filter(p => p.length > 0);

      if (phrases.length === 0) {
        processedSubtitles.push('[无内容]');
        processedSubtitleDurations.push(totalDuration);
        return;
      }

      const totalChars = phrases.reduce((sum, p) => sum + p.length, 0);
      let accumulatedμs = 0;

      phrases.forEach((phrase, i) => {
        const ratio = phrase.length / totalChars;
        let durationμs = i === phrases.length - 1
          ? totalDuration - accumulatedμs
          : Math.round(totalDuration * ratio);

        processedSubtitles.push(phrase);
        processedSubtitleDurations.push(durationμs);
        accumulatedμs += durationμs;
      });
    });

    const textTimelines = [];
    let currentTime = startTimeμs;

    processedSubtitleDurations.forEach(durationμs => {
      const start = currentTime;
      const end = start + durationμs;

      textTimelines.push({
        start: start,
        end: end
      });

      currentTime = end;
    });

    return { textTimelines, processedSubtitles };
  };

输出变量:

  • common_timelines(对象数组)
  • text_captions(字符串列表)
  • video_timeline(对象数组)
  • max_time(整数)
  • text_timielines(对象数组)
  • audioData(字符串)
  • aideoTimelines(对象数组)
  • bgAudioData(字符串)
  • imageData(字符串)
  • kcAudioData(字符串)

第 14 步:准备背景音乐链接文本

  • 从「13-构建时间轴」拉出一个新的【文本处理】节点。重命名为:"14-文本处理"

参数设置:

  • String1(字符串):https://p3-bot-workflow-sign.byteimg.com/tos-cn-i-mdko3gqilj/de5afe4cbd304b8498e8f422a3bf400a.mp3~tplv-mdko3gqilj-image.image?rk3s=81d4c505&x-expires=1803879346&x-signature=lOOn2S0JXwkyDpreS8RiqEKBPmU%3D

拼接模板这样填:

{{String1}}

第 15 步:把背景音乐字符串转成列表

  • 从「14-文本处理」拉出一个新的【插件】节点。插件市场搜剪映小助手,选str_to_list。重命名为:"15-字符转列表"

参数设置:

  • obj(字符串):引用「14-文本处理」输出的output

第 16 步:给开场背景音乐做一段固定时间线

  • 从「15-字符转列表」拉出一个新的【插件】节点。插件市场搜剪映小助手,选timelines。重命名为:"16-时间线"

参数设置:

  • duration(整数):1500000
  • num(整数):1
  • start(整数):0

第 17 步:把正文配音整理成剪映可添加的数据

  • 从「16-时间线」拉出一个新的【插件】节点。插件市场搜剪映小助手,选audio_infos。重命名为:"17-正文数据"

参数设置:

  • mp3_urls(字符串列表):引用「10-循环」输出的link_list
  • timelines(对象数组):引用「13-构建时间轴」输出的aideoTimelines
  • audio_effect(字符串):复古电视
  • volume(小数):2

第 18 步:把背景音乐整理成剪映可添加的数据

  • 从「17-正文数据」拉出一个新的【插件】节点。插件市场搜剪映小助手,选audio_infos。重命名为:"18-背景音乐"

参数设置:

  • mp3_urls(字符串列表):引用「15-字符转列表」输出的infos
  • timelines(对象数组):引用「13-构建时间轴」输出的common_timelines
  • volume(小数):0.5

第 19 步:把每个镜头的音效整理成剪映可添加的数据

  • 从「18-背景音乐」拉出一个新的【插件】节点。插件市场搜剪映小助手,选audio_infos。重命名为:"19-音效数据"

参数设置:

  • mp3_urls(字符串列表):引用「8-批处理」输出的selected_url_list
  • timelines(对象数组):引用「11-音频时间线」输出的timelines
  • volume(小数):0.5

第 20 步:把视频链接和时间线整理成剪映视频数据

  • 从「19-音效数据」拉出一个新的【插件】节点。插件市场搜剪映小助手,选video_infos。重命名为:"20-视频数据"

参数设置:

  • timelines(对象数组):引用「11-音频时间线」输出的timelines
  • video_urls(字符串列表):引用「9-批处理视频」输出的video_urls

第 21 步:把字幕文本和字幕时间线整理成剪映字幕数据

  • 从「20-视频数据」拉出一个新的【插件】节点。插件市场搜剪映小助手,选caption_infos。重命名为:"21-字幕数据"

参数设置:

  • texts(字符串列表):引用「12-音频字幕时间线对齐」输出的text_captions
  • timelines(对象数组):引用「12-音频字幕时间线对齐」输出的text_timelines

第 22 步:创建剪映草稿

  • 从「21-字幕数据」拉出一个新的【插件】节点。插件市场搜剪映小助手,选create_draft。重命名为:"22-草稿"

参数设置:

  • height(整数):1728
  • width(整数):2304

第 23 步:把正文配音添加进草稿

  • 从「22-草稿」拉出一个新的【插件】节点。插件市场搜剪映小助手,选add_audios。重命名为:"23-添加正文音频"

参数设置:

  • audio_infos(字符串):引用「17-正文数据」输出的infos
  • draft_id(字符串):引用「22-草稿」输出的draft_id

第 24 步:把背景音乐添加进草稿

  • 从「23-添加正文音频」拉出一个新的【插件】节点。插件市场搜剪映小助手,选add_audios。重命名为:"24-添加背景音乐"

参数设置:

  • audio_infos(字符串):引用「18-背景音乐」输出的infos
  • draft_id(字符串):引用「22-草稿」输出的draft_id

第 25 步:把音效添加进草稿

  • 从「24-添加背景音乐」拉出一个新的【插件】节点。插件市场搜剪映小助手,选add_audios。重命名为:"25-添加音效"

参数设置:

  • audio_infos(字符串):引用「19-音效数据」输出的infos
  • draft_id(字符串):引用「22-草稿」输出的draft_id

第 26 步:把视频添加进草稿

  • 从「25-添加音效」拉出一个新的【插件】节点。插件市场搜剪映小助手,选add_videos。重命名为:"26-添加视频"

参数设置:

  • draft_id(字符串):引用「22-草稿」输出的draft_id
  • video_infos(字符串):引用「20-视频数据」输出的infos

第 27 步:把字幕添加进草稿

  • 从「26-添加视频」拉出一个新的【插件】节点。插件市场搜剪映小助手,选add_captions。重命名为:"27-添加字幕"

参数设置:

  • captions(字符串):引用「21-字幕数据」输出的infos
  • draft_id(字符串):引用「22-草稿」输出的draft_id
  • alignment(整数):1
  • border_color(字符串):#ffffff
  • font(字符串):楚辰体
  • font_size(整数):9
  • scale_x(小数):1.8
  • scale_y(小数):1.8
  • text_color(字符串):#414242
  • transform_x(小数):0
  • transform_y(小数):-1250

第 28 步:结束并返回草稿链接

  • 从「27-添加字幕」拉出一个新的【结束】节点。节点名字保持:"结束"

参数设置:

  • draft_url(字符串):引用「22-草稿」输出的draft_id

这就是整个工作流搭建的过程,接下来我们只需要点击试运行,成功的话就会得到这样一个草稿ID
请添加图片描述

只需要把这个草稿ID复制下来,然后打开我们的剪映小助手把草稿ID复制进去,然后点击创建剪映草稿(注意不要复制引号)
请添加图片描述

稍等一下,创建成功之后打开剪映,就会看到剪映里面多出了一个黑色的草稿,只要点进去就能看到我们的视频了

![请添加图片描述](https://i-blog.csdnimg.cn/direct/43b0f492061e4e6f9c57e41c6c32297c.png)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐