This commit is contained in:
liubiren 2026-03-03 16:39:33 +08:00
parent c8003ee988
commit 1e90bab40a
42 changed files with 610 additions and 108 deletions

Binary file not shown.

View File

@ -0,0 +1,375 @@
# -*- coding: utf-8 -*-
"""
创建剪映草稿
"""
import asyncio
from pathlib import Path
from pathlib import Path
from random import choice
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Tuple, Union
import edge_tts
from pyJianYingDraft import (
AudioMaterial,
AudioSegment,
ClipSettings,
DraftFolder,
FontType,
KeyframeProperty,
TextBackground,
TextBorder,
TextSegment,
TextStyle,
TrackType,
VideoMaterial,
VideoSegment,
trange,
)
class JianYingDraftGenerator:
"""
剪映草稿生成器
"""
def __init__(
self,
drafts_folder_path: str = r"E:\JianYingPro Drafts",
):
"""
初始化
:param drafts_folder_path: 剪映草稿文件夹路径
"""
try:
# 初始化草稿文件夹管理器
self.drafts_manager = DraftFolder(
folder_path=Path(drafts_folder_path).as_posix()
)
# 初始化素材文件夹路径
self.materials_folder_path = Path(__file__).parent
except Exception as exception:
raise RuntimeError(f"发生异常:{str(exception)}") from exception
def text_to_speech(
self,
audio_path: Path,
text: str,
) -> None:
"""
合成音频并本地保存音频文件
:param audio_path: 音频文件路径
:param text: 待合成音频的文本
"""
try:
# 异步调用 EdgeTTS 合成音频
async def _async_synthetize():
communicator = edge_tts.Communicate(
text=text.strip(),
voice="zh-CN-XiaoxiaoNeural", # 语音角色
rate="+20%", # 语速
volume="+10%", # 音量
pitch="+15Hz", # 音调
)
await communicator.save(audio_path.as_posix())
return
return asyncio.run(_async_synthetize())
except Exception as exception:
raise RuntimeError(
f"合成音频并本地保存音频文件发生异常:{str(exception)}"
) from exception
def add_text_segment(
self,
track_name: str,
text: str,
timerange: Tuple[Union[int, str], Union[int, str]],
font: Optional[str] = None,
style: Optional[Dict[str, Any]] = None,
border: Optional[Dict[str, Any]] = None,
background: Optional[Dict[str, Any]] = None,
clip_settings: Optional[Dict[str, Any]] = None,
bubble: Optional[Dict[str, Any]] = None,
effect: Optional[Dict[str, Any]] = None,
animation: Optional[Dict[str, Any]] = None,
) -> None:
"""
向指定文本轨道添加文本片段
:param track_name: 轨道名称
:param add_track: 添加文本轨道默认为是
:param text: 文本
:param timerange: 文本素材在轨道上的范围包括开始时间和持续时长默认为草稿持续时长
:param font: 字体默认为系统
:param style: 字体样式默认为字号 15对齐方式 左对齐
:param border: 文本描边设置默认为无
:param background: 文本背景设置默认为无
:param clip_settings: 图像调节设置默认为移动至 (0, 0)
:param bubble: 气泡设置默认为无
:param effect: 花字设置默认为无
:param animation: 动画设置默认为无
:return:
"""
try:
# 构建文本片段
text_segment = TextSegment(
text=text.replace("\\n", "\n"),
timerange=trange(*timerange),
font=FontType(font) if font else None,
style=TextStyle(**style) if style else None,
border=TextBorder(**border) if border else None,
background=(TextBackground(**background) if background else None),
clip_settings=(
ClipSettings(**clip_settings) if clip_settings else None
),
)
# 添加气泡
if bubble:
text_segment.add_bubble(**bubble)
# 添加花字
# 将花字保存预设后在C:/Users/admin/AppData/Local/JianyingPro/User Data/Presets/Text_V2/预设文本?.textpreset获取花字resource_id
if effect:
text_segment.add_effect(**effect)
# 添加动画
if animation:
text_segment.add_animation(**animation)
# 向指定文本轨道添加文本片段
self.draft.add_segment(segment=text_segment, track_name=track_name)
except Exception as exception:
raise RuntimeError(str(exception)) from exception
def add_audio_segment(
self,
track_name: str,
material: str,
target_timerange: Tuple[Union[int, str], Union[int, str]],
source_timerange: Optional[Tuple[str, str]] = None,
speed: float = 1.0,
volume: float = 1.0,
fade: Optional[Tuple[str, str]] = None,
) -> None:
"""
向指定音频轨道添加音频片段
:param track_name: 轨道名称
:param add_track: 添加音频轨道默认为是
:param material: 音频素材
:param target_timerange: 音频素材在轨道上的范围包括开始时间和持续时长
:param source_timerange: 截取音频素材范围包括开始时间和持续时长默认根据音频素材开始时间根据播放速度截取与音频素材持续时长等长的部分
:param speed: 播放速度默认为 1.0
:param volume: 播放音量默认为 1.0
:param fade: 淡入淡出设置默认为无
:return:
"""
try:
# 构建音频片段
audio_segment = AudioSegment(
material=material,
target_timerange=trange(*target_timerange),
source_timerange=(
trange(*source_timerange) if source_timerange else None
),
speed=speed,
volume=volume,
)
# 添加淡入淡出
if fade:
audio_segment.add_fade(*fade)
# 向指定音频轨道添加音频片段
self.draft.add_segment(segment=audio_segment, track_name=track_name)
except Exception as exception:
raise RuntimeError(str(exception)) from exception
def add_video_segment(
self,
track_name: str,
material: str,
target_timerange: Tuple[Union[int, str], Union[int, str]],
speed: float = 1.0,
volume: float = 1.0,
clip_settings: Optional[Dict[str, Any]] = None,
keyframes: Optional[
List[Tuple[KeyframeProperty, Union[str, int], float]]
] = None,
animation: Optional[Dict[str, Any]] = None,
transition: Optional[Dict[str, Any]] = None,
background_filling: Optional[Dict[str, Any]] = None,
) -> None:
"""
向指定视频轨道添加视频或图片片段
:param track_name: 轨道名称
:param materia: 视频或图片素材
:param target_timerange: 视频或图片素材在轨道上的范围包括开始时间和持续时长默认为草稿持续时长
:param source_timerange: 截取视频或图片素材范围包括开始时间和持续时长默认为空
:param speed: 播放速度默认为 1.0
:param volume: 播放音量默认为 1.0
:param clip_settings: 图像调节设置默认为无
:param keyframes: 关键帧设置默认为无
:param animation: 动画设置默认为无
:param transition: 转场设置默认为无
:param background_filling: 背景填充设置默认为无
:return:
"""
try:
# 构建视频或图片片段
video_segment = VideoSegment(
material=material,
target_timerange=trange(*target_timerange),
speed=speed,
volume=volume,
clip_settings=(
ClipSettings(**clip_settings) if clip_settings else None
),
)
# 添加关键帧
if keyframes:
for _property, offset, value in keyframes:
video_segment.add_keyframe(_property, offset, value)
# 添加动画
if animation:
video_segment.add_animation(**animation)
# 添加转场
if transition:
video_segment.add_transition(**transition)
# 添加背景填充
if background_filling:
video_segment.add_background_filling(**background_filling)
# 向指定视频轨道添加视频或图片片段
self.draft.add_segment(segment=video_segment, track_name=track_name)
except Exception as exception:
raise RuntimeError(str(exception)) from exception
def create_draft(
self,
task_id: str,
contents: List[str],
):
"""
创建草稿
:param task_id: 任务标识
:param contents: 分镜内容列表
:return:
"""
# 新建草稿
self.draft = self.drafts_manager.create_draft(
draft_name=task_id,
allow_replace=True, # 允许覆盖
width=1080,
height=1920,
fps=30,
)
# 添加视频轨道
self.draft.add_track(
track_type=TrackType.video,
track_name=(video_track_name := "video"),
)
# 添加文本轨道
self.draft.add_track(
track_type=TrackType.text,
track_name=(text_track_name := "text"),
)
# 添加音频轨道(字幕音频)
self.draft.add_track(
track_type=TrackType.audio,
track_name=(subtitle_audio_track_name := "subtitle_audio"),
)
# 添加音频轨道(背景音频)
self.draft.add_track(
track_type=TrackType.audio,
track_name=(background_audio_track_name := "background_audio"),
)
# 构造字幕音频文件夹路径
subtitle_audios_folder_path = self.materials_folder_path / "subtitle_audios"
subtitle_audios_folder_path.mkdir(exist_ok=True)
duration = 0 # 视频持续时长
for i, content in enumerate(contents, start=1):
content_duration = 0 # 素材持续时长
for j, text in enumerate(
[text for text in content.split("") if text.strip()], start=1
):
# 构建片段字幕音频文件路径
audio_path = (
subtitle_audios_folder_path / f"{task_id}_{i:02d}_{j:02d}.mp3"
)
if not audio_path.exists():
# 合成片段字幕音频并本地保存片段字幕音频文件
self.text_to_speech(
audio_path=audio_path,
text=text,
)
# 片段字幕音频素材化
audio = AudioMaterial(path=audio_path.as_posix())
# 向文本轨道添加文本片段(片段字幕文本)
self.add_text_segment(
track_name=text_track_name,
text=text,
timerange=(duration, audio.duration),
style={
"size": 12.0,
"align": 1, # 水平居中0为左对齐、1为水平居中、2为右对齐
"auto_wrapping": True, # 自动换行
}, # 字体样式字号为12水平居中
clip_settings={
"transform_y": -0.85,
}, # 图像设置垂直下移60%
effect={
"effect_id": "6896137924853763336", # 使用花字
},
)
# 向字幕音频的音频轨道添加音频片段(片段字幕音频)
self.add_audio_segment(
track_name=subtitle_audio_track_name,
material=audio.path,
target_timerange=(duration, audio.duration),
volume=1.0,
)
duration += audio.duration
content_duration += audio.duration
# 构建分镜视频路径
video_path = Path(
self.materials_folder_path / "videos" / f"{task_id}_{i:02d}.mp4"
)
# 分镜视频素材化
video = VideoMaterial(path=video_path.as_posix())
# 向视频轨道添加视频片段(分镜视频)
self.add_video_segment(
track_name=video_track_name,
material=video.path,
target_timerange=(duration - content_duration, content_duration),
volume=0.0,
speed=round(video.duration / content_duration, 2) - 0.01, # 冗余安全量
)
# 构建背景音频文件路径
background_audio_path = choice(
list((self.materials_folder_path / "background_audios").glob("*.mp3"))
)
# 背景音频素材化
background_audio = AudioMaterial(path=background_audio_path.as_posix())
# 向背景音频的音频轨道添加音频片段(背景音频)
self.add_audio_segment(
track_name=background_audio_track_name,
material=background_audio.path,
target_timerange=(0, duration),
volume=0.4,
)
# 将草稿保存至剪映草稿文件夹内
self.draft.save()

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
模块 程序
""" """
import warnings import warnings
@ -8,8 +8,11 @@ warnings.filterwarnings(
action="ignore", category=UserWarning, module="volcenginesdkarkruntime.*" action="ignore", category=UserWarning, module="volcenginesdkarkruntime.*"
) )
from base64 import b64decode, b64encode
import json import json
from pathlib import Path from pathlib import Path
import sys
from time import sleep
from typing import Any, Dict, List from typing import Any, Dict, List
from uuid import uuid4 from uuid import uuid4
@ -19,6 +22,11 @@ from volcenginesdkarkruntime.types.responses import (
ResponseOutputText, ResponseOutputText,
) )
sys.path.append(Path(__file__).parent.parent.as_posix())
from utils.request import Request
from create_draft import JianYingDraftGenerator
# 初始化火山引擎 AsyncArk 客户端 # 初始化火山引擎 AsyncArk 客户端
ark_client = Ark( ark_client = Ark(
@ -26,13 +34,8 @@ ark_client = Ark(
api_key="2c28ab07-888c-45be-84a2-fc4b2cb5f3f2", api_key="2c28ab07-888c-45be-84a2-fc4b2cb5f3f2",
) # 本人火山引擎账密 ) # 本人火山引擎账密
# 初始化请求客户端
def generate_task_id() -> str: request_client = Request()
"""
生成任务标识
:return: 任务ID
"""
return uuid4().hex.upper().replace("-", "")
def get_brand_words() -> List[str]: def get_brand_words() -> List[str]:
@ -53,24 +56,12 @@ def get_brand_words() -> List[str]:
raise exception raise exception
def refactor_storyboard_prompt(brand_word: str) -> str: def generate_task_id() -> str:
""" """
重构分镜脚本的提示词 生成任务标识
:param brand_word: 品牌词 :return: 任务ID
:return: 重构后分镜脚本的提示词
""" """
try: return uuid4().hex.upper().replace("-", "")
with open(
file=Path(__file__).parent / "storyboard_prompt_template.txt",
mode="r",
encoding="utf-8",
) as file:
storyboard_prompt = file.read()
return storyboard_prompt.replace("{{品牌词}}", brand_word)
except FileNotFoundError:
raise FileNotFoundError("未找到分镜脚本的提示词模板文件")
except Exception as exception:
raise exception
def get_storyboard(brand_word: str) -> Dict[str, Any]: def get_storyboard(brand_word: str) -> Dict[str, Any]:
@ -79,13 +70,24 @@ def get_storyboard(brand_word: str) -> Dict[str, Any]:
:param brand_word: 品牌词 :param brand_word: 品牌词
:return: 分镜脚本 :return: 分镜脚本
""" """
# 重构分镜脚本的提示词 try:
storyboard_prompt = refactor_storyboard_prompt(brand_word) with open(
file=Path(__file__).parent / "storyboard_prompt_template.txt",
mode="r",
encoding="utf-8",
) as file:
storyboard_prompt = file.read()
# 重构分镜脚本的提示词
storyboard_prompt = storyboard_prompt.replace("{{品牌词}}", brand_word)
except FileNotFoundError:
raise FileNotFoundError("未找到分镜脚本的提示词模板文件")
except Exception as exception:
raise exception
retries = 0 # 重试次数 retries = 0 # 重试次数
while True: while True:
try: try:
# 调用豆包Seed语言大模型基于分镜脚本的提示词生成分镜脚本 # 调用 doubao-seed 语言大模型,基于分镜脚本的提示词生成分镜脚本
response = ark_client.responses.create( response = ark_client.responses.create(
model="doubao-seed-2-0-pro-260215", model="doubao-seed-2-0-pro-260215",
input=[ input=[
@ -107,58 +109,186 @@ def get_storyboard(brand_word: str) -> Dict[str, Any]:
continue continue
storyboard = { def generate_frame(frame_prompt: str, frame_name: str) -> None:
"核心营销要点": "淘宝闪购每日上新全品类正品好物,限时超低价开抢,购物省心又划算",
"分镜脚本": [
{
"阶段": "前段",
"口播": "买好物想省钱看这里!",
"TTS情绪": "energetic",
"首帧提示词": "橙色淘宝闪购logo 画面中央偏上 半透明小尺寸、特效未展开 浅橙色渐变背景 亮橙+米白 柔和光影 9:16竖屏 1080P 高清干净 无水印",
"尾帧提示词": "橙色立体淘宝闪购logo 画面正中央 清晰显示、周围带细碎金光闪效 浅橙色渐变背景 亮橙+暖白 明亮光影 9:16竖屏 1080P 高清干净 无水印",
"运镜提示词": "slight zoom in",
},
{
"阶段": "中段",
"口播": "淘宝闪购全是官方正品,服饰美妆数码家居全品类,每天限时开抢价格超划算",
"TTS情绪": "happy",
"首帧提示词": "服饰、美妆、数码、家居四类商品缩略图围绕淘宝闪购logo 画面中央 半透明小尺寸、特效未展开 暖白色背景 多彩明快 柔和光影 9:16竖屏 1080P 高清干净 无水印",
"尾帧提示词": "服饰、美妆、数码 、家居爆款商品清晰展示旁附「直降50%」「限时抢」标签淘宝闪购logo居上方中央 高亮饱满、爆闪优惠光效完全展开 暖白色背景 多彩明亮 充足光影 9:16竖屏 1080P 高清干净 无水印",
"运镜提示词": "slow push in",
},
{
"阶段": "后段",
"口播": "快点击下方链接抢!",
"TTS情绪": "cheer",
"首帧提示词": "黄色「点击下方链接」按钮+淘宝闪购logo 画面中央 半透明小尺寸、特效未展开 亮橙色背景 亮黄+橙红 柔和光影 9:16竖屏 1080P 高清干净 无水印",
"尾帧提示词": "高亮立体「点击下方链接」按钮+闪烁箭头指向屏幕下方淘宝闪购logo居按钮上方 完全展示、高亮饱满、脉冲光效完整 亮橙色背景 亮黄+橙红 明亮耀眼光影 9:16竖屏 1080P 高清干净 无水印",
"运镜提示词": "slight zoom in",
},
],
}
def generate_frame(frame_prompt: str) -> str:
""" """
生成视频帧 生成视频帧
:param frame_prompt: 视频帧提示词 :param frame_prompt: 视频帧提示词
:return: 视频帧 :param frame_name: 视频帧名称
:return: None
""" """
# 调用豆包Seed图像大模型基于视频帧提示词生成视频帧 # 构建视频帧路径
response = ark_client.responses.create( frame_path = Path(__file__).parent / "frames" / frame_name
model="doubao-seed-2-0-pro-260215", # 若视频帧已存在则直接返回
input=[ if frame_path.exists():
{ return
"role": "user",
"content": [{"type": "input_text", "text": frame_prompt}], retries = 0 # 重试次数
} while True:
], try:
) # 调用 doubao-seedream 图像大模型,基于视频帧提示词生成视频帧
# 解析响应并反序列化,以此作为视频帧 #
frame = json.loads( response = ark_client.images.generate(
[item for item in [item for item in response.output if isinstance(item, ResponseOutputMessage)][0].content if isinstance(item, ResponseOutputText)][0].text # type: ignore model="doubao-seedream-4-5-251128",
) prompt=frame_prompt,
return frame sequential_image_generation="disabled", # 关闭组图输出
size="1600x2848",
watermark=False, # 关闭水印
response_format="b64_json", # 图像数据为base64编码的JSON字符串
)
# 解析响应
frame_base64 = b64decode(response.data[0].b64_json)
# 本地保存视频帧
with open(file=frame_path, mode="wb") as file:
file.write(frame_base64)
return
except Exception as exception:
retries += 1
if retries > 2:
raise Exception(f"生成视频帧发生异常,{str(exception)}")
continue
print(storyboard) def generate_video(
video_name: str,
first_frame_name: str,
last_frame_name: str,
shot_prompt: str,
video_duration: int,
) -> None:
"""
生成视频
:param video_name: 视频名称
:param first_frame_name: 首帧名称
:param last_frame_name: 尾帧名称
:param shot_prompt: 运镜提示词
:param video_duration: 视频时长
:return: None
"""
# 若视频已存在则直接返回
if (Path(__file__).parent / "videos" / video_name).exists():
return
# 构建首帧路径
first_frame_path = Path(__file__).parent / "frames" / first_frame_name
if not first_frame_path.exists():
raise RuntimeError(f"首帧 {first_frame_name} 不存在")
with open(file=first_frame_path, mode="rb") as file:
first_frame_base64 = b64encode(file.read()).decode("utf-8")
# 构建尾帧路径
last_frame_path = Path(__file__).parent / "frames" / last_frame_name
if not last_frame_path.exists():
raise RuntimeError(f"尾帧 {last_frame_name} 不存在")
with open(file=last_frame_path, mode="rb") as file:
last_frame_base64 = b64encode(file.read()).decode("utf-8")
retries = 0 # 重试次数
while True:
try:
# 调用 doubao-seedrance 视频大模型,基于首尾帧和运镜提示词生成视频
# 创建视频生成任务
create_response = ark_client.content_generation.tasks.create(
model="doubao-seedance-1-5-pro-251215",
content=[
{"type": "text", "text": shot_prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{first_frame_base64}"
},
"role": "first_frame",
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{last_frame_base64}"
},
"role": "last_frame",
},
],
ratio="9:16", # 视频的宽高比例
duration=video_duration, # 视频时长doubao-seedance-1-5-pro-251215 有效范围为[4, 12]
watermark=False, # 关闭水印
)
# 轮询查询视频生成任务
while True:
# 查询视频生成任务
query_response = ark_client.content_generation.tasks.get(
task_id=create_response.id,
)
# 根据视频生成任务的状态匹配处理方法
match query_response.status:
case "succeeded":
video_url = query_response.content.video_url
# 下载视频
chunk_generator = request_client.download(
url=video_url,
stream_enabled=True, # 开启流式传输
)
# 本地保存视频
with open(
file=Path(__file__).parent / "videos" / video_name,
mode="wb",
) as file:
for chunk in chunk_generator:
file.write(chunk)
return
case "failed":
raise Exception(f"{query_response.error}")
case _:
sleep(5) # 避免频繁请求查询视频生成任务故显性等待
except Exception as exception:
retries += 1
if retries > 2:
raise Exception(f"生成视频发生异常,{str(exception)}")
continue
if __name__ == "__main__":
# 分镜视频时长列表
video_durations = [15, 4, 8, 8, 4]
# 遍历品牌词
for brand_word in get_brand_words():
# 生成任务标识
task_id = generate_task_id()
# 获取分镜脚本
storyboard = get_storyboard(
brand_word=brand_word,
)
# 遍历分镜
for i, shot in enumerate(storyboard["分镜脚本"], start=1):
# 构建分镜首帧名称
first_frame_name = f"{task_id}_{i:02d}_first.jpeg"
# 生成分镜首帧
generate_frame(
frame_prompt=shot["首帧提示词"], # 分镜首帧提示词
frame_name=first_frame_name,
)
# 构建分镜尾帧名称
last_frame_name = f"{task_id}_{i:02d}_last.jpeg"
# 生成分镜尾帧
generate_frame(
frame_prompt=shot["尾帧提示词"], # 分镜尾帧提示词
frame_name=last_frame_name,
)
# 生成视频
generate_video(
video_name=f"{task_id}_{i:02d}.mp4", # 分镜视频名称
video_duration=video_durations[i], # 分镜视频时长
first_frame_name=first_frame_name,
last_frame_name=last_frame_name,
shot_prompt=shot["运镜提示词"], # 分镜运镜提示词
)
# 生成剪映草稿
draft_generator = JianYingDraftGenerator()
draft_generator.create_draft(
task_id=task_id,
contents=[shot["口播"] for shot in storyboard["分镜脚本"]],
)

View File

@ -1,41 +1,35 @@
你作为一名专业、优秀的广告编导,请为品牌“{{品牌词}}”生成一条15秒营销广告分镜脚本。该脚本用于AI生成短视频投放于快手、今日头条、视频号等短视频平台。 你作为一名专业、优秀的广告编导,请为品牌“{{品牌词}}”生成一条24秒营销广告分镜脚本。该脚本用于AI生成短视频投放于快手、今日头条、视频号等短视频平台。
必须严格遵守以下所有规则,不可遗漏任何字段: 必须严格遵守以下所有规则,不可遗漏任何字段:
1. 内容要求 1. 内容要求
- 符合品牌真实经营范围与营销风格 - 符合品牌真实经营范围与营销风格
- 符合短视频平台用户偏好
- 不得虚构、不得违规 - 不得虚构、不得违规
2. 时长结构总15秒 2. 时长结构
- 前段3秒吸引注意力 - 第一段4秒就该品牌所解决的核心痛点构思场景吸引用户注意
- 中段10秒传递核心卖点 - 第二段8秒体现该品牌并构思该品牌2~3个价值点
- 后段2秒引导点击下方链接 - 第三段8秒围绕价值点构思实际场景
- 第四段4秒引导用户点击视频下方链接
3. 口播要求(用于 EdgeTTS 语音合成) 3. 口播要求(用于 EdgeTTS
- 前段口播812个汉字 - 第一段口播一句话515个汉字
- 中段口播2535个汉字 - 第二段口播1530个汉字
- 后段口播510个汉字 - 第三段口播1530个汉字
- 第四段口播一句话515个汉字
- 口语化、情绪饱满、适合短视频传播 - 口语化、情绪饱满、适合短视频传播
4. TTS 情绪要求(必须返回英文单词) 4. 首帧 / 尾帧提示词(用于 Doubao-Seedream-5.0 文生图)
- 从以下列表中选择cheer, happy, energetic, warm, friendly, confident - 用简洁连贯的自然语言完整且详细描述:主体+位置+状态+环境+色彩+光影
- 字段名固定为TTS情绪 - 9:16 竖屏1080P 高清,干净 无水印
5. 首帧 / 尾帧提示词(用于 Seedream 4.5 文生图)
- 格式:主体+位置+状态+环境+色彩+光影
- 9:16 竖屏1080P高清干净无水印
- 首帧:元素刚出现,半透明,小尺寸,特效未展开 - 首帧:元素刚出现,半透明,小尺寸,特效未展开
- 尾帧:元素完全展示,高亮饱满,特效完整 - 尾帧:元素完全展示,高亮饱满,特效完整
- 画面自然、有质感、适合电商营销 - 画面和本段口播匹配,自然、有质感,适合广告营销,
6. 运镜提示词(用于 Seedance 1.5 图生视频,必须英文) 5. 运镜提示词(用于 Doubao-Seedance-2.0 首尾帧生视频,必须英文)
- 运镜平稳、无抖动 - 运镜平稳、无抖动
- 主体居中不变形
- 可选slow push in, slow pan, slight zoom in, slow circle
- 与本段时长匹配
7. 输出强制要求 6. 输出强制要求
- 只输出标准 JSON无任何多余文字 - 只输出标准 JSON无任何多余文字
- 不要解释、不要前言、不要后缀 - 不要解释、不要前言、不要后缀
- 不要 ```json 标记 - 不要 ```json 标记
@ -45,25 +39,29 @@
"核心营销要点": "一句话总结品牌核心价值", "核心营销要点": "一句话总结品牌核心价值",
"分镜脚本": [ "分镜脚本": [
{ {
"阶段": "前段", "阶段": "第一段",
"口播": "...", "口播": "一句话口播",
"TTS情绪": "...",
"首帧提示词": "...", "首帧提示词": "...",
"尾帧提示词": "...", "尾帧提示词": "...",
"运镜提示词": "..." "运镜提示词": "..."
}, },
{ {
"阶段": "段", "阶段": "第二段",
"口播": "...", "口播": "...",
"TTS情绪": "...",
"首帧提示词": "...", "首帧提示词": "...",
"尾帧提示词": "...", "尾帧提示词": "...",
"运镜提示词": "..." "运镜提示词": "..."
}, },
{ {
"阶段": "后段", "阶段": "第三段",
"口播": "...",
"首帧提示词": "...",
"尾帧提示词": "...",
"运镜提示词": "..."
},
{
"阶段": "第四段",
"口播": "...", "口播": "...",
"TTS情绪": "...",
"首帧提示词": "...", "首帧提示词": "...",
"尾帧提示词": "...", "尾帧提示词": "...",
"运镜提示词": "..." "运镜提示词": "..."

Binary file not shown.

View File

@ -63,7 +63,7 @@ class EdgeTTS:
) )
await communicator.save(file_path.as_posix()) await communicator.save(file_path.as_posix())
# 持续时长(单位为微秒) # 持续时长(单位为微秒)
duration = int(round(MP3(file_path.as_posix()).info.length * 1000000)) duration = int(round(MP3(file_path.as_posix()).info.length * 1_000_000))
return file_path, duration return file_path, duration
return asyncio.run(_async_synthetize()) return asyncio.run(_async_synthetize())

View File

@ -10,7 +10,6 @@ from pathlib import WindowsPath
import random import random
import re import re
import shutil import shutil
import subprocess
import sys import sys
import time import time
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional