hot_video_analyse/talkGenerator.py

336 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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("❌ 第一次调用失败,无法进行第二次调用")