336 lines
14 KiB
Python
336 lines
14 KiB
Python
|
|
from openai import OpenAI
|
|||
|
|
import os
|
|||
|
|
import json
|
|||
|
|
from datetime import datetime
|
|||
|
|
import math
|
|||
|
|
import random
|
|||
|
|
MODEL_NAME = "/root/autodl-tmp/llm/qwen3-30B-A3B"
|
|||
|
|
# MODEL_NAME = "qwen2.5-VL-32B"
|
|||
|
|
MODEL_NAME = "/root/autodl-tmp/llm/Qwen3-32B"
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
client = OpenAI(api_key="EMPTY",
|
|||
|
|
base_url="http://localhost:8000/v1")
|
|||
|
|
|
|||
|
|
video_description_file = "/root/autodl-tmp/Video_Gen/03_VideoAnalysis/VideoAnalysis_Result/科学中心_enhanced/20250612_180218_f944c885/AnalysisSummary_Origin.json"
|
|||
|
|
video_sampliing_rate = 0.8
|
|||
|
|
template_file = "/root/autodl-tmp/Video_Gen/01_InformationSource/营销模板.txt"
|
|||
|
|
product_info_file = "/root/autodl-tmp/Video_Gen/01_InformationSource/科学中心/information/科学中心产品.txt"
|
|||
|
|
product_background_file = "/root/autodl-tmp/Video_Gen/01_InformationSource/科学中心/information/科学中心.txt"
|
|||
|
|
output_base_path = "/root/autodl-tmp/Video_Gen/04_GeneratingScripts/output"
|
|||
|
|
|
|||
|
|
video_description = json.load(open(video_description_file, "r"))["processed_files"]
|
|||
|
|
print(len(video_description))
|
|||
|
|
video_description = [
|
|||
|
|
item for item in video_description
|
|||
|
|
if item["frame_descriptions"]["time"] >= 2
|
|||
|
|
]
|
|||
|
|
# print(len(video_description))
|
|||
|
|
# input("press any key to continue")
|
|||
|
|
video_description = random.sample(video_description, int(len(video_description) * video_sampliing_rate))
|
|||
|
|
print(len(video_description))
|
|||
|
|
# input("press any key to continue")
|
|||
|
|
template = open(template_file, "r").read()
|
|||
|
|
product_info = open(product_info_file, "r").read()
|
|||
|
|
product_background = open(product_background_file, "r").read()
|
|||
|
|
# print(video_description)
|
|||
|
|
|
|||
|
|
# 第一次调用的系统提示词 - 专注于内容创作和核心素材选择
|
|||
|
|
system_prompt_1 = """
|
|||
|
|
你是一名专业的短视频内容创作者,擅长根据视频片段描述和营销模版创作高质量的口播文案。你会优先阅读营销模版,并根据视频片段描述,创作口播文案。
|
|||
|
|
|
|||
|
|
## 核心任务
|
|||
|
|
根据提供的视频片段描述和营销模版,创作完整的口播文案,并选择核心的视频片段用于后续的分镜制作。
|
|||
|
|
|
|||
|
|
## 严格要求
|
|||
|
|
|
|||
|
|
### 1. 内容创作要求
|
|||
|
|
- 完整口播文案总字数约150字,语言风格以口语化表达
|
|||
|
|
- 口播内容的创作参照营销模版中的口播内容,以营销模版口播为框架,进行创作
|
|||
|
|
- **创作风格**:口播内容以口语化表达,不要做任何括号,用口语化、主观视角的方式进行表达
|
|||
|
|
- 内容必须真实可信,围绕产品核心卖点展开,不得虚构
|
|||
|
|
- 价格信息必须模糊表达:使用"几十元"、"奶茶钱"、"两位数"、"一百出头"等表述
|
|||
|
|
- 严禁提及资料中未明确说明的福利、赠品、活动等内容
|
|||
|
|
- 不可以提及不存在的福利、赠品、活动等内容
|
|||
|
|
|
|||
|
|
### 2. 营销模版遵循
|
|||
|
|
- 严格按照提供的营销模版进行创作
|
|||
|
|
- 保持模版的表达风格、句式结构和营销逻辑
|
|||
|
|
- 将产品信息自然融入模版框架中
|
|||
|
|
|
|||
|
|
### 3. 核心视频片段选择
|
|||
|
|
- 选择10-15个最具代表性的视频片段,用于核心口播内容匹配
|
|||
|
|
- 选择的片段应该能够很好地支撑口播文案的表达
|
|||
|
|
- 片段时长必须与视频描述中的时长完全一致,不得修改
|
|||
|
|
- 优先选择画面丰富、有吸引力的片段
|
|||
|
|
|
|||
|
|
## 输出格式
|
|||
|
|
请严格按照以下JSON格式输出:
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
"模版解构": "根据营销模版,分析模版结构及其可选性,争取保留大部分的模版内容,并根据视频片段描述,设计转场方式",
|
|||
|
|
"视频结构": "根据视频片段描述,设计视频结构及转场方式",
|
|||
|
|
"summary": "视频脚本核心内容总结(例如:XX产品的亮点介绍与使用场景展示)",
|
|||
|
|
"talk": "完整的口播文案,以人类口语语气输出,不要做任何括号,大约150字",
|
|||
|
|
"talk_word_count": "口播文案字数统计",
|
|||
|
|
"video_selection": [
|
|||
|
|
{
|
|||
|
|
"video_path": "视频片段的完整路径",
|
|||
|
|
"video_duration": "该片段的实际持续时间(秒,与描述完全一致)",
|
|||
|
|
"video_description": "视频片段描述,简单描述视频片段内容,应该与口播内容相关",
|
|||
|
|
"scene_type": "场景类型(如:产品展示、环境场景、人物互动等)"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"video_count": "选用的视频片段数量(10-15个)",
|
|||
|
|
"estimated_duration": "基于口播文案估算的视频总时长(口播字数÷3.5)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
请严格按照上述要求创作,专注于高质量的内容创作和核心素材选择。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 第二次调用的系统提示词 - 专注于时长匹配和分镜制作
|
|||
|
|
system_prompt_2 = """
|
|||
|
|
你是一名专业的视频剪辑师,擅长根据口播文案和视频素材制作精确的分镜脚本。你需要确保口播内容与视频时长完美匹配。
|
|||
|
|
|
|||
|
|
## 核心任务
|
|||
|
|
根据已创作的口播文案和选定的核心视频片段,制作最终的分镜脚本(storyboard),确保口播内容与视频时长完美匹配。
|
|||
|
|
|
|||
|
|
## 严格要求
|
|||
|
|
|
|||
|
|
### 1. 时长匹配原则(核心要求)
|
|||
|
|
- **口播速度标准**:正常语速约为每秒3-4个汉字,计算采用4字/秒
|
|||
|
|
- **片段时长计算**:每个片段的口播文案字数 = 片段时长(秒)× 4字/秒
|
|||
|
|
- **时长严格对应**:选用片段的实际时长必须与视频描述中的时长完全一致,不得修改
|
|||
|
|
- **文案长度适配**:每个片段的口播文案字数必须与该片段时长相匹配,误差不超过±1字
|
|||
|
|
|
|||
|
|
### 2. 分镜制作要求
|
|||
|
|
- 将口播文案按标点符号分割成多个片段,每个片段作为storyboard中的一个最小单元,每个最小单元的talk_piece应该只有一句简短、不带标点符号的最小句子
|
|||
|
|
- 确保口播文案的完整性,已创作的口播文案必须被一字不少地分配到视频片段中
|
|||
|
|
- 当口播长度和视频片段时长不匹配时,优先保证口播的完整性
|
|||
|
|
- 每个口播片段都应该至少被分配到一个视频片段
|
|||
|
|
- 不能存在没有口播的视频片段
|
|||
|
|
- 如果口播文案短于视频片段,你可以适当的对视频片段进行裁剪,通过调整time_duration来裁剪视频片段
|
|||
|
|
- 如果核心片段时长不够,需要选择额外的填充片段,也许你会需要选择多个填充片段裁剪衔接。
|
|||
|
|
- 开头需要选用吸引人的视频片段作为开场
|
|||
|
|
- 视频片段不可重复使用
|
|||
|
|
|
|||
|
|
### 3. 额外片段选择
|
|||
|
|
- 如果核心片段总时长不足以匹配口播文案,需要从剩余片段中选择合适的填充片段
|
|||
|
|
- 填充片段应该与内容相关,特别是开场片段要有吸引力
|
|||
|
|
- 可以通过调整video_duration来裁剪过长的视频片段
|
|||
|
|
|
|||
|
|
## 输出格式
|
|||
|
|
请严格按照以下JSON格式输出:
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
"时长分析": {
|
|||
|
|
"口播总字数": "口播文案总字数",
|
|||
|
|
"预估总时长": "口播总时长(字数÷3.5)",
|
|||
|
|
"核心片段总时长": "已选核心片段的总时长",
|
|||
|
|
"时长差异": "预估时长与核心片段时长的差值",
|
|||
|
|
"是否需要额外片段": "true/false"
|
|||
|
|
},
|
|||
|
|
"video_additional": [
|
|||
|
|
{
|
|||
|
|
"video_path": "视频片段的完整路径",
|
|||
|
|
"video_duration": "该片段的实际持续时间(秒,与描述完全一致)",
|
|||
|
|
"video_description": "视频片段描述,简单描述视频片段内容",
|
|||
|
|
"usage_purpose": "使用目的(如:开场吸引、内容填充、转场过渡等)"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"storyboard": [
|
|||
|
|
{
|
|||
|
|
"index": "脚本序号(从1开始递增)",
|
|||
|
|
"video_duration": "该片段的实际持续时间(秒,必须小于等于视频片段描述中的时长)",
|
|||
|
|
"talk_piece": "该片段的口播文案,从原始talk中完整提取,不要二次创作",
|
|||
|
|
"talk_word_count": "该片段口播文案字数",
|
|||
|
|
"video_description": "视频片段描述",
|
|||
|
|
"video_path": "视频片段的完整路径",
|
|||
|
|
"time_start": "该片段在完整视频中的开始时间(单位:秒)",
|
|||
|
|
"time_end": "该片段在完整视频中的结束时间(单位:秒)",
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"total_duration": "实际上视频的总时长",
|
|||
|
|
"验证结果": {
|
|||
|
|
"口播总字数": "storyboard中所有talk_piece的总字数",
|
|||
|
|
"视频总时长": "storyboard中所有片段的总时长",
|
|||
|
|
"匹配度": "字数与时长的匹配程度评估"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
请严格按照上述要求制作分镜脚本,确保口播内容与视频时长完美匹配。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
user_prompt = f"""
|
|||
|
|
视频描述:
|
|||
|
|
{video_description}
|
|||
|
|
产品信息:
|
|||
|
|
{product_info}
|
|||
|
|
产品背景:
|
|||
|
|
{product_background}
|
|||
|
|
面向人群:
|
|||
|
|
亲子向用户画像:
|
|||
|
|
1. 基本属性
|
|||
|
|
- 年龄:25-45岁(家长群体),孩子年龄集中在3-12岁.
|
|||
|
|
- 性别:女性主导型,约70%为妈妈群体.
|
|||
|
|
|
|||
|
|
2. 平台行为
|
|||
|
|
- 互动习惯:偏好收藏实用攻略.
|
|||
|
|
- 发布内容:分享带娃旅行的温馨瞬间
|
|||
|
|
|
|||
|
|
3.中国旅游出游做攻略高峰点:
|
|||
|
|
1. 春节假期:1-2月
|
|||
|
|
2. 暑假:6-8月
|
|||
|
|
3. 国庆黄金周:9-10月初
|
|||
|
|
4. 五一假期:4-5月初
|
|||
|
|
5. 元旦:12月中下旬
|
|||
|
|
6.清明:4月初
|
|||
|
|
7.端午:5-6月
|
|||
|
|
8.中秋:8月底-9月
|
|||
|
|
9.寒假:12-2月
|
|||
|
|
|
|||
|
|
营销模板 template:
|
|||
|
|
{template}
|
|||
|
|
发布时间:6月14日
|
|||
|
|
请先阅读营销模版,按照以上需求,严格按照营销模版创作口播文案,并选择核心的视频片段。
|
|||
|
|
选择10-15个视频片段,为后续制作视频脚本做准备。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
print("=== 第一次模型调用:生成口播文案和核心素材选择 ===")
|
|||
|
|
print(user_prompt)
|
|||
|
|
|
|||
|
|
# 第一次模型调用
|
|||
|
|
response_1 = client.chat.completions.create(
|
|||
|
|
model=MODEL_NAME,
|
|||
|
|
messages=[
|
|||
|
|
{"role": "system", "content": system_prompt_1},
|
|||
|
|
{"role": "user", "content": user_prompt}
|
|||
|
|
],
|
|||
|
|
temperature=0.3,
|
|||
|
|
top_p=0.4,
|
|||
|
|
stream=True,
|
|||
|
|
presence_penalty=1,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
full_response_1 = ""
|
|||
|
|
for chunk in response_1:
|
|||
|
|
if chunk.choices[0].delta.content:
|
|||
|
|
full_response_1 += chunk.choices[0].delta.content
|
|||
|
|
print(chunk.choices[0].delta.content, end="", flush=True)
|
|||
|
|
|
|||
|
|
print("\n\n=== 第一次调用完成,开始解析结果 ===")
|
|||
|
|
|
|||
|
|
# 解析第一次调用的结果
|
|||
|
|
try:
|
|||
|
|
# 处理思考标签
|
|||
|
|
if "</think>" in full_response_1:
|
|||
|
|
json_content_1 = full_response_1.split("</think>")[1].strip()
|
|||
|
|
else:
|
|||
|
|
json_content_1 = full_response_1.strip()
|
|||
|
|
|
|||
|
|
# 清理JSON格式
|
|||
|
|
json_content_1 = json_content_1.replace("```json", "").replace("```", "").strip()
|
|||
|
|
|
|||
|
|
# 解析JSON
|
|||
|
|
result_json_1 = json.loads(json_content_1)
|
|||
|
|
|
|||
|
|
print("✅ 第一次调用结果解析成功")
|
|||
|
|
print(f"口播文案字数:{result_json_1.get('talk_word_count', '未统计')}")
|
|||
|
|
print(f"选择视频片段数量:{result_json_1.get('video_count', '未统计')}")
|
|||
|
|
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
print(f"❌ 第一次调用JSON解析失败: {str(e)}")
|
|||
|
|
result_json_1 = None
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"❌ 第一次调用处理失败: {str(e)}")
|
|||
|
|
result_json_1 = None
|
|||
|
|
|
|||
|
|
# 如果第一次调用成功,进行第二次调用
|
|||
|
|
if result_json_1:
|
|||
|
|
print("\n=== 开始第二次模型调用:生成分镜脚本 ===")
|
|||
|
|
|
|||
|
|
# 构建第二次调用的用户提示词
|
|||
|
|
user_prompt_2 = f"""
|
|||
|
|
基于第一次调用的结果,请制作精确的分镜脚本:
|
|||
|
|
|
|||
|
|
口播文案:
|
|||
|
|
{result_json_1.get('talk', '')}
|
|||
|
|
|
|||
|
|
核心视频片段:
|
|||
|
|
{json.dumps(result_json_1.get('video_selection', []), ensure_ascii=False, indent=2)}
|
|||
|
|
|
|||
|
|
所有可用视频片段(用于选择额外填充片段):
|
|||
|
|
{json.dumps(video_description, ensure_ascii=False, indent=2)}
|
|||
|
|
|
|||
|
|
营销模版:
|
|||
|
|
{template}
|
|||
|
|
请确保:
|
|||
|
|
1. 口播文案与视频时长完美匹配
|
|||
|
|
2. 制作{result_json_1.get('estimated_duration', '未统计')}秒左右的完整分镜脚本
|
|||
|
|
3. 如需要,选择合适的额外片段进行填充
|
|||
|
|
4. 开场要有吸引力
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 第二次模型调用
|
|||
|
|
response_2 = client.chat.completions.create(
|
|||
|
|
model=MODEL_NAME,
|
|||
|
|
messages=[
|
|||
|
|
{"role": "system", "content": system_prompt_2},
|
|||
|
|
{"role": "user", "content": user_prompt_2}
|
|||
|
|
],
|
|||
|
|
temperature=0.1,
|
|||
|
|
top_p=0.3,
|
|||
|
|
stream=True,
|
|||
|
|
presence_penalty=0.5,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
full_response_2 = ""
|
|||
|
|
for chunk in response_2:
|
|||
|
|
if chunk.choices[0].delta.content:
|
|||
|
|
full_response_2 += chunk.choices[0].delta.content
|
|||
|
|
print(chunk.choices[0].delta.content, end="", flush=True)
|
|||
|
|
|
|||
|
|
print("\n\n=== 第二次调用完成,开始解析结果 ===")
|
|||
|
|
|
|||
|
|
# 解析第二次调用的结果
|
|||
|
|
try:
|
|||
|
|
# 处理思考标签
|
|||
|
|
if "</think>" in full_response_2:
|
|||
|
|
json_content_2 = full_response_2.split("</think>")[1].strip()
|
|||
|
|
else:
|
|||
|
|
json_content_2 = full_response_2.strip()
|
|||
|
|
|
|||
|
|
# 清理JSON格式
|
|||
|
|
json_content_2 = json_content_2.replace("```json", "").replace("```", "").strip()
|
|||
|
|
|
|||
|
|
# 解析JSON
|
|||
|
|
result_json_2 = json.loads(json_content_2)
|
|||
|
|
|
|||
|
|
print("✅ 第二次调用结果解析成功")
|
|||
|
|
print(f"分镜片段数量:{len(result_json_2.get('storyboard', []))}")
|
|||
|
|
print(f"视频总时长:{result_json_2.get('total_duration', '未统计')}秒")
|
|||
|
|
|
|||
|
|
# 合并两次调用的结果
|
|||
|
|
final_result = {
|
|||
|
|
**result_json_1, # 第一次调用的结果
|
|||
|
|
**result_json_2 # 第二次调用的结果
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 保存最终结果
|
|||
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|||
|
|
output_file = os.path.join(output_base_path, f"video_script_{timestamp}.json")
|
|||
|
|
|
|||
|
|
os.makedirs(output_base_path, exist_ok=True)
|
|||
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
|||
|
|
json.dump(final_result, f, ensure_ascii=False, indent=2)
|
|||
|
|
|
|||
|
|
print(f"\n✅ 最终脚本已保存到:{output_file}")
|
|||
|
|
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
print(f"❌ 第二次调用JSON解析失败: {str(e)}")
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"❌ 第二次调用处理失败: {str(e)}")
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
print("❌ 第一次调用失败,无法进行第二次调用")
|