chaowenguo's picture
Upload 121 files
3b13b0e verified
import os
import re
import json
import traceback
import streamlit as st
from typing import List
from loguru import logger
from openai import OpenAI
from openai import AzureOpenAI
from moviepy import VideoFileClip
from openai.types.chat import ChatCompletion
import google.generativeai as gemini
from googleapiclient.errors import ResumableUploadError
from google.api_core.exceptions import *
from google.generativeai.types import *
import subprocess
from typing import Union, TextIO
from app.config import config
from app.utils.utils import clean_model_output
_max_retries = 5
Method = """
重要提示:每一部剧的文案,前几句必须吸引人
首先我们在看完看懂电影后,大脑里面要先有一个大概的轮廓,也就是一个类似于作文的大纲,电影主题线在哪里,首先要找到。
一般将文案分为开头、内容、结尾
## 开头部分
文案开头三句话,是留住用户的关键!
### 方式一:开头概括总结
文案的前三句,是整部电影的概括总结,2-3句介绍后,开始叙述故事剧情!
推荐新手(新号)做:(盘点型)
盘点全球最恐怖的10部电影
盘���全球最科幻的10部电影
盘点全球最悲惨的10部电影
盘全球最值得看的10部灾难电影
盘点全球最值得看的10部励志电影
下面的示例就是最简单的解说文案开头:
1.这是XXX国20年来最大尺度的一部剧,极度烧脑,却让99%的人看得心潮澎湃、无法自拔,故事开始……
2.这是有史以来电影院唯一一部全程开灯放完的电影,期间无数人尖叫昏厥,他被成为勇敢者的专属,因为99%的人都不敢看到结局,许多人看完它从此不愿再碰手机,他就是大名鼎鼎的暗黑神作《XXX》……
3.这到底是一部什么样的电影,能被55个国家公开抵制,它甚至为了上映,不惜删减掉整整47分钟的剧情……
4.是什么样的一个人被豆瓣网友称之为史上最牛P的老太太,都70岁了还要去贩毒……
5.他是M国历史上最NB/惨/猖狂/冤枉……的囚犯/抢劫犯/……
6.这到底是一部什么样的影片,他一个人就拿了4个顶级奖项,第一季8.7分,第二季直接干到9.5分,11万人给出5星好评,一共也就6集,却斩获26项国际大奖,看过的人都说,他是近年来最好的xxx剧,几乎成为了近年来xxx剧的标杆。故事发生在……
7.他是国产电影的巅峰佳作,更是许多80-90后的青春启蒙,曾入选《��代》周刊,获得年度佳片第一,可在国内却被尘封多年,至今为止都无法在各大视频网站看到完整资源,他就是《xxxxxx》
8.这是一部让所有人看得荷尔蒙飙升的爽片……
9.他被成为世界上最虐心绝望的电影,至今无人敢看第二遍,很难想象,他是根据真实事件改编而来……
10.这大概是有史以来最令人不寒而栗的电影,当年一经放映,就点燃了无数人的怒火,不少观众不等影片放完,就愤然离场,它比《xxx》更让人绝望,比比《xxx》更让人xxx,能坚持看完全片的人,更是万中无一,包括我。甚至观影结束后,有无数人抵制投诉这部电影,认为影片的导演玩弄了他们的情感!他是顶级神作《xxxx》……
11.这是X国有史以来最高赞的一部悬疑电影,然而却因为某些原因,国内90%的人,没能看过这部片子,他就是《xxx》……
12.有这样一部电影,这辈子,你绝对不想再看第二遍,并不是它剧情烂俗,而是它的结局你根本承受不起/想象不到……甚至有80%的观众在观影途中情绪崩溃中途离场,更让许多同行都不想解说这部电影,他就是大名鼎鼎的暗黑神作《xxx》…
13.它被誉为史上最牛悬疑片无数人在看完它时候,一个月不敢照镜��,这样一部仅适合部分年龄段观看的影片,究竟有什么样的魅力,竟然获得某瓣8.2的高分,很多人说这部电影到处都是看点,他就是《xxx》….
14.这是一部在某瓣上被70万人打出9.3分的高分的电影……到底是一部什么样的电影,能够在某瓣上被70万人打出9.3分的高分……
15.这是一部细思极恐的科幻大片,整部电影颠覆你的三观,它的名字叫……
16.史上最震撼的灾难片,每一点都不舍得快进的电影,他叫……
17.今天给大家带来一部基于真实事件改编的(主题介绍一句……)的故事片,这是一部连环悬疑剧,如果不看到最后绝对想不到结局竟然是这样的反转……
### 方式:情景式、假设性开头
1.他叫……你以为他是……的吗?不。他是来……然后开始叙述
2.你知道……吗?原来……然后开始叙述
3.如果给你….,你会怎么样?
4.如果你是….,你会怎么样?
### 方式三:以国家为开头!简单明了。话语不需要多,但是需要讲解透彻!
1.这是一部韩国最新灾难片,你一定没有看过……
2.这是一部印度高分悬疑片,
3.这部电影原在日本因为……而被下架,
4.这是韩国最恐怖的犯罪片,
5.这是最近国产片评分最高的悬疑��
以上均按照影片国家来区分,然后简单介绍下主题。就可以开始直接叙述作品。也是一个很不错的方法!
### 方式四:如何自由发挥
正常情况下,每一部电影都有非常关键的一个大纲,这部电影的主题其实是可以用一句话、两句话概括的。只要看懂电影,就能找到这个主题大纲。
我们提前把这个主题大纲给放到影视最前面,作为我们的前三句的文案,将会非常吸引人!
例如:
1.这不是电影,这是真实故事。两个女人和一个男人被关在可桑拿室。喊破喉咙也没有一丝回音。窒息感和热度让人抓狂,故事就是从这里开始!
2.如果你男朋友出轨了,他不爱你了,还你家暴,怎么办?接下来这部电影就会教你如何让老公服服帖帖的呆在你身边!女主是一个……开始叙述了。
3.他力大无穷,双眼放光,这不是拯救地球的超人吗?然而不是。今天给大家推荐的这部电影叫……
以上是需要看完影片,看懂影片,然后从里面提炼出精彩的几句话,当然是比较难的,当你不会自己去总结前三句的经典的话。可以用前面方式一二三!
实在想不出来如何去提炼,可以去搜索这部剧,对这部电影的影评,也会给你带过来很多灵感的!
## 内容部分
开头有了,剩下的就是开始叙述正文了。主题介绍是根据影片内容来介绍,如果实在自己想不出来。可以参考其他平台中对这部电影的精彩介绍,提取2-3句也可以!
正常情况下,我们叙述的时候其实是非常简单的,把整部电影主题线,叙述下来,其实文案就是加些修饰词把电影重点内容叙述下来。加上一些修饰词。
以悬疑剧为例:
竟然,突然,原来,但是,但,可是,结果,直到,如果,而,果然,发现,只是,出奇,之后,没错,不止,更是,当然,因为,所以……等!
以上是比较常用的,当然还有很多,需要靠平时思考和阅读的积累!因悬疑剧会有多处反转剧情。所以需要用到反转的修饰词比较多,只有用到这些词。才能体现出各种反转剧情!
建议大家在刚开始做的时候,做8分钟内的,不要太长,分成三段。每段也是不超过三分钟,这样时间刚好。可以比较好的完成完播率!
## 结尾部分
最后故事的结局,除了反转,可以来点人生的道理!如果刚开始不会,可以不写。
后面水平越来越高的时候,可以进行人生道理的讲评。
比如:这部电影告诉我们……
类似于哲理性质��作为一个总结!
也可以把最后的影视反转,原生放出来,留下悬念。
比如:也可以总结下这部短片如何的好,推荐/值得大家去观看之类的话语。
其实就是给我们的作品来一个总结,总结我们所做的三个视频,有开始就要有结束。这个结束不一定是固定的模版。但是视频一定要有结尾。让人感觉有头有尾才最舒服!
做解说第一次,可能会做两天。第二次可能就需要一天了。慢慢的。时间缩短到8个小时之内是我们平的制作全部时间!
"""
def handle_exception(err):
if isinstance(err, PermissionDenied):
raise Exception("403 用户没有权限访问该资源")
elif isinstance(err, ResourceExhausted):
raise Exception("429 您的配额已用尽。请稍后重试。请考虑设置自动重试来处理这些错误")
elif isinstance(err, InvalidArgument):
raise Exception("400 参数无效。例如,文件过大,超出了载荷大小限制。另一个事件提供了无效的 API 密钥。")
elif isinstance(err, AlreadyExists):
raise Exception("409 已存在具有相同 ID 的已调参模型。对新模型进行调参时,请指定唯一的模型 ID。")
elif isinstance(err, RetryError):
raise Exception("使用不支持 gRPC 的代理时可能会引起此错误。请尝试将 REST 传输与 genai.configure(..., transport=rest) 搭配使用。")
elif isinstance(err, BlockedPromptException):
raise Exception("400 出于安全原因,该提示已被屏蔽。")
elif isinstance(err, BrokenResponseError):
raise Exception("500 流式传输响应已损坏。在访问需要完整响应的内容(例如聊天记录)时引发。查看堆栈轨迹中提供的错误详情。")
elif isinstance(err, IncompleteIterationError):
raise Exception("500 访问需要完整 API 响应但流式响应尚未完全迭代的内容时引发。对响应对象调用 resolve() 以使用迭代器。")
elif isinstance(err, ConnectionError):
raise Exception("网络连接错误, 请检查您的网络连接(建议使用 NarratoAI 官方提供的 url)")
else:
raise Exception(f"大模型请求失败, 下面是具体报错信息: \n\n{traceback.format_exc()}")
def _generate_response(prompt: str, llm_provider: str = None) -> str:
"""
调用大模型通用方法
prompt:
llm_provider:
"""
content = ""
if not llm_provider:
llm_provider = config.app.get("llm_provider", "openai")
logger.info(f"llm provider: {llm_provider}")
if llm_provider == "g4f":
model_name = config.app.get("g4f_model_name", "")
if not model_name:
model_name = "gpt-3.5-turbo-16k-0613"
import g4f
content = g4f.ChatCompletion.create(
model=model_name,
messages=[{"role": "user", "content": prompt}],
)
else:
api_version = "" # for azure
if llm_provider == "moonshot":
api_key = config.app.get("moonshot_api_key")
model_name = config.app.get("moonshot_model_name")
base_url = "https://api.moonshot.cn/v1"
elif llm_provider == "ollama":
# api_key = config.app.get("openai_api_key")
api_key = "ollama" # any string works but you are required to have one
model_name = config.app.get("ollama_model_name")
base_url = config.app.get("ollama_base_url", "")
if not base_url:
base_url = "http://localhost:11434/v1"
elif llm_provider == "openai":
api_key = config.app.get("openai_api_key")
model_name = config.app.get("openai_model_name")
base_url = config.app.get("openai_base_url", "")
if not base_url:
base_url = "https://api.openai.com/v1"
elif llm_provider == "oneapi":
api_key = config.app.get("oneapi_api_key")
model_name = config.app.get("oneapi_model_name")
base_url = config.app.get("oneapi_base_url", "")
elif llm_provider == "azure":
api_key = config.app.get("azure_api_key")
model_name = config.app.get("azure_model_name")
base_url = config.app.get("azure_base_url", "")
api_version = config.app.get("azure_api_version", "2024-02-15-preview")
elif llm_provider == "gemini":
api_key = config.app.get("gemini_api_key")
model_name = config.app.get("gemini_model_name")
base_url = "***"
elif llm_provider == "qwen":
api_key = config.app.get("qwen_api_key")
model_name = config.app.get("qwen_model_name")
base_url = "***"
elif llm_provider == "cloudflare":
api_key = config.app.get("cloudflare_api_key")
model_name = config.app.get("cloudflare_model_name")
account_id = config.app.get("cloudflare_account_id")
base_url = "***"
elif llm_provider == "deepseek":
api_key = config.app.get("deepseek_api_key")
model_name = config.app.get("deepseek_model_name")
base_url = config.app.get("deepseek_base_url")
if not base_url:
base_url = "https://api.deepseek.com"
elif llm_provider == "ernie":
api_key = config.app.get("ernie_api_key")
secret_key = config.app.get("ernie_secret_key")
base_url = config.app.get("ernie_base_url")
model_name = "***"
if not secret_key:
raise ValueError(
f"{llm_provider}: secret_key is not set, please set it in the config.toml file."
)
else:
raise ValueError(
"llm_provider is not set, please set it in the config.toml file."
)
if not api_key:
raise ValueError(
f"{llm_provider}: api_key is not set, please set it in the config.toml file."
)
if not model_name:
raise ValueError(
f"{llm_provider}: model_name is not set, please set it in the config.toml file."
)
if not base_url:
raise ValueError(
f"{llm_provider}: base_url is not set, please set it in the config.toml file."
)
if llm_provider == "qwen":
import dashscope
from dashscope.api_entities.dashscope_response import GenerationResponse
dashscope.api_key = api_key
response = dashscope.Generation.call(
model=model_name, messages=[{"role": "user", "content": prompt}]
)
if response:
if isinstance(response, GenerationResponse):
status_code = response.status_code
if status_code != 200:
raise Exception(
f'[{llm_provider}] returned an error response: "{response}"'
)
content = response["output"]["text"]
return content.replace("\n", "")
else:
raise Exception(
f'[{llm_provider}] returned an invalid response: "{response}"'
)
else:
raise Exception(f"[{llm_provider}] returned an empty response")
if llm_provider == "gemini":
import google.generativeai as genai
genai.configure(api_key=api_key, transport="rest")
safety_settings = {
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
}
model = genai.GenerativeModel(
model_name=model_name,
safety_settings=safety_settings,
)
try:
response = model.generate_content(prompt)
return response.text
except Exception as err:
return handle_exception(err)
if llm_provider == "cloudflare":
import requests
response = requests.post(
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/{model_name}",
headers={"Authorization": f"Bearer {api_key}"},
json={
"messages": [
{"role": "system", "content": "You are a friendly assistant"},
{"role": "user", "content": prompt},
]
},
)
result = response.json()
logger.info(result)
return result["result"]["response"]
if llm_provider == "ernie":
import requests
params = {
"grant_type": "client_credentials",
"client_id": api_key,
"client_secret": secret_key,
}
access_token = (
requests.post("https://aip.baidubce.com/oauth/2.0/token", params=params)
.json()
.get("access_token")
)
url = f"{base_url}?access_token={access_token}"
payload = json.dumps(
{
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.5,
"top_p": 0.8,
"penalty_score": 1,
"disable_search": False,
"enable_citation": False,
"response_format": "text",
}
)
headers = {"Content-Type": "application/json"}
response = requests.request(
"POST", url, headers=headers, data=payload
).json()
return response.get("result")
if llm_provider == "azure":
client = AzureOpenAI(
api_key=api_key,
api_version=api_version,
azure_endpoint=base_url,
)
else:
client = OpenAI(
api_key=api_key,
base_url=base_url,
)
response = client.chat.completions.create(
model=model_name, messages=[{"role": "user", "content": prompt}]
)
if response:
if isinstance(response, ChatCompletion):
content = response.choices[0].message.content
else:
raise Exception(
f'[{llm_provider}] returned an invalid response: "{response}", please check your network '
f"connection and try again."
)
else:
raise Exception(
f"[{llm_provider}] returned an empty response, please check your network connection and try again."
)
return content.replace("\n", "")
def _generate_response_video(prompt: str, llm_provider_video: str, video_file: Union[str, TextIO]) -> str:
"""
多模态能力大模型
"""
if llm_provider_video == "gemini":
api_key = config.app.get("gemini_api_key")
model_name = config.app.get("gemini_model_name")
base_url = "***"
else:
raise ValueError(
"llm_provider 未设置,请在 config.toml 文件中进行设置。"
)
if llm_provider_video == "gemini":
import google.generativeai as genai
genai.configure(api_key=api_key, transport="rest")
safety_settings = {
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
}
model = genai.GenerativeModel(
model_name=model_name,
safety_settings=safety_settings,
)
try:
response = model.generate_content([prompt, video_file])
return response.text
except Exception as err:
return handle_exception(err)
def compress_video(input_path: str, output_path: str):
"""
压缩视频文件
Args:
input_path: 输入视频文件路径
output_path: 输出压缩后的视频文件路径
"""
# 如果压缩后的视频文件已经存在,则直接使用
if os.path.exists(output_path):
logger.info(f"压缩视频文件已存在: {output_path}")
return
try:
clip = VideoFileClip(input_path)
clip.write_videofile(output_path, codec='libx264', audio_codec='aac', bitrate="500k", audio_bitrate="128k")
except subprocess.CalledProcessError as e:
logger.error(f"视频压缩失败: {e}")
raise
def generate_script(
video_path: str, video_plot: str, video_name: str, language: str = "zh-CN", progress_callback=None
) -> str:
"""
生成视频剪辑脚本
Args:
video_path: 视频文件路径
video_plot: 视频剧情内容
video_name: 视频名称
language: 语言
progress_callback: 进度回调函数
Returns:
str: 生成的脚本
"""
try:
# 1. 压缩视频
compressed_video_path = f"{os.path.splitext(video_path)[0]}_compressed.mp4"
compress_video(video_path, compressed_video_path)
# 在关键步骤更新进度
if progress_callback:
progress_callback(15, "压缩完成") # 例如,在压缩视频后
# 2. 转录视频
transcription = gemini_video_transcription(
video_name=video_name,
video_path=compressed_video_path,
language=language,
llm_provider_video=config.app["video_llm_provider"],
progress_callback=progress_callback
)
if progress_callback:
progress_callback(60, "生成解说文案...") # 例如,在转录视频后
# 3. 编写解说文案
script = writing_short_play(video_plot, video_name, config.app["llm_provider"], count=300)
# 在关键步骤更新进度
if progress_callback:
progress_callback(70, "匹配画面...") # 例如,在生成脚本后
# 4. 文案匹配画面
if transcription != "":
matched_script = screen_matching(huamian=transcription, wenan=script, llm_provider=config.app["video_llm_provider"])
# 在关键步骤更新进度
if progress_callback:
progress_callback(80, "匹配成功")
return matched_script
else:
return ""
except Exception as e:
handle_exception(e)
raise
def gemini_video_transcription(video_name: str, video_path: str, language: str, llm_provider_video: str, progress_callback=None):
'''
使用 gemini-1.5-xxx 进行视频画面转录
'''
api_key = config.app.get("gemini_api_key")
gemini.configure(api_key=api_key)
prompt = """
请转录音频,包括时间戳,并提供视觉描述,然后以 JSON 格式输出,当前视频中使用的语言为 %s。
在转录视频时,请通过确保以下条件来完成转录:
1. 画面描述使用语言: %s 进行输出。
2. 同一个画面合并为一个转录记录。
3. 使用以下 JSON schema:
Graphics = {"timestamp": "MM:SS-MM:SS"(时间戳格式), "picture": "str"(画面描述), "speech": "str"(台词,如果没有人说话,则使用空字符串。)}
Return: list[Graphics]
4. 请以严格的 JSON 格式返回数据,不要包含任何注释、标记或其他字符。数据应符合 JSON 语法,可以被 json.loads() 函数直接解析, 不要添加 ```json 或其他标记。
""" % (language, language)
logger.debug(f"视频名称: {video_name}")
try:
if progress_callback:
progress_callback(20, "上传视频至 Google cloud")
gemini_video_file = gemini.upload_file(video_path)
logger.debug(f"视频 {gemini_video_file.name} 上传至 Google cloud 成功, 开始解析...")
while gemini_video_file.state.name == "PROCESSING":
gemini_video_file = gemini.get_file(gemini_video_file.name)
if progress_callback:
progress_callback(30, "上传成功, 开始解析") # 更新进度为20%
if gemini_video_file.state.name == "FAILED":
raise ValueError(gemini_video_file.state.name)
elif gemini_video_file.state.name == "ACTIVE":
if progress_callback:
progress_callback(40, "解析完成, 开始转录...") # 更新进度为30%
logger.debug("解析完成, 开始转录...")
except ResumableUploadError as err:
logger.error(f"上传视频至 Google cloud 失败, 用户的位置信息不支持用于该API; \n{traceback.format_exc()}")
return False
except FailedPrecondition as err:
logger.error(f"400 用户位置不支持 Google API 使用。\n{traceback.format_exc()}")
return False
if progress_callback:
progress_callback(50, "开始转录")
try:
response = _generate_response_video(prompt=prompt, llm_provider_video=llm_provider_video, video_file=gemini_video_file)
logger.success("视频转录成功")
logger.debug(response)
print(type(response))
return response
except Exception as err:
return handle_exception(err)
def generate_terms(video_subject: str, video_script: str, amount: int = 5) -> List[str]:
prompt = f"""
# Role: Video Search Terms Generator
## Goals:
Generate {amount} search terms for stock videos, depending on the subject of a video.
## Constrains:
1. the search terms are to be returned as a json-array of strings.
2. each search term should consist of 1-3 words, always add the main subject of the video.
3. you must only return the json-array of strings. you must not return anything else. you must not return the script.
4. the search terms must be related to the subject of the video.
5. reply with english search terms only.
## Output Example:
["search term 1", "search term 2", "search term 3","search term 4","search term 5"]
## Context:
### Video Subject
{video_subject}
### Video Script
{video_script}
Please note that you must use English for generating video search terms; Chinese is not accepted.
""".strip()
logger.info(f"subject: {video_subject}")
search_terms = []
response = ""
for i in range(_max_retries):
try:
response = _generate_response(prompt)
search_terms = json.loads(response)
if not isinstance(search_terms, list) or not all(
isinstance(term, str) for term in search_terms
):
logger.error("response is not a list of strings.")
continue
except Exception as e:
logger.warning(f"failed to generate video terms: {str(e)}")
if response:
match = re.search(r"\[.*]", response)
if match:
try:
search_terms = json.loads(match.group())
except Exception as e:
logger.warning(f"failed to generate video terms: {str(e)}")
pass
if search_terms and len(search_terms) > 0:
break
if i < _max_retries:
logger.warning(f"failed to generate video terms, trying again... {i + 1}")
logger.success(f"completed: \n{search_terms}")
return search_terms
def gemini_video2json(video_origin_name: str, video_origin_path: str, video_plot: str, language: str) -> str:
'''
使用 gemini-1.5-pro 进行影视解析
Args:
video_origin_name: str - 影视作品的原始名称
video_origin_path: str - 影视作品的原始路径
video_plot: str - 影视作品的简介或剧情概述
Return:
str - 解析后的 JSON 格式字符串
'''
api_key = config.app.get("gemini_api_key")
model_name = config.app.get("gemini_model_name")
gemini.configure(api_key=api_key)
model = gemini.GenerativeModel(model_name=model_name)
prompt = """
**角色设定:**
你是一位影视解说专家,擅长根据剧情生成引人入胜的短视频解说文案,特别熟悉适用于TikTok/抖音风格的快速、抓人视频解说。
**任务目标:**
1. 根据给定剧情,详细描述画面,重点突出重要场景和情节。
2. 生成符合TikTok/抖音风格的解说,节奏紧凑,语言简洁,吸引观众。
3. 解说的时候需要解说一段播放一段原视频,原视频一般为有台词的片段,原视频的控制有 OST 字段控制。
4. 结果输出为JSON格式,包含字段:
- "picture":画面描述
- "timestamp":画面出现的时间范围
- "narration":解说内容
- "OST": 是否开启原声(true / false)
**输入示例:**
```text
在一个���暗的小巷中,主角缓慢走进,四周静谧无声,只有远处隐隐传来猫的叫声。突然,背后出现一个神秘的身影。
```
**输出格式:**
```json
[
{
"picture": "黑暗的小巷,主角缓慢走入,四周安静,远处传来猫叫声。",
"timestamp": "00:00-00:17",
"narration": "静谧的小巷里,主角步步前行,气氛渐渐变得压抑。"
"OST": False
},
{
"picture": "神秘身影突然出现,紧张气氛加剧。",
"timestamp": "00:17-00:39",
"narration": "原声播放"
"OST": True
}
]
```
**提示:**
- 文案要简短有力,契合短视频平台用户的观赏习惯。
- 保持强烈的悬念和情感代入,吸引观众继续观看。
- 解说一段后播放一段原声,原声内容尽量和解说匹配。
- 文案语言为:%s
- 剧情内容:%s (为空则忽略)
""" % (language, video_plot)
logger.debug(f"视频名称: {video_origin_name}")
# try:
gemini_video_file = gemini.upload_file(video_origin_path)
logger.debug(f"上传视频至 Google cloud 成功: {gemini_video_file.name}")
while gemini_video_file.state.name == "PROCESSING":
import time
time.sleep(1)
gemini_video_file = gemini.get_file(gemini_video_file.name)
logger.debug(f"视频当前状态(ACTIVE才可用): {gemini_video_file.state.name}")
if gemini_video_file.state.name == "FAILED":
raise ValueError(gemini_video_file.state.name)
# except Exception as err:
# logger.error(f"上传视频至 Google cloud 失败, 请检查 VPN 配置和 APIKey 是否正确 \n{traceback.format_exc()}")
# raise TimeoutError(f"上传视频至 Google cloud 失败, 请检查 VPN 配置和 APIKey 是否正确; {err}")
streams = model.generate_content([prompt, gemini_video_file], stream=True)
response = []
for chunk in streams:
response.append(chunk.text)
response = "".join(response)
logger.success(f"llm response: \n{response}")
return response
def writing_movie(video_plot, video_name, llm_provider):
"""
影视解说(电影解说)
"""
prompt = f"""
**角色设定:**
你是一名有10年经验的影视解说文案的创作者,
下面是关于如何写解说文案的方法 {Method},请认真阅读它,之后我会给你一部影视作品的名称,然后让你写一篇文案
请根据方法撰写 《{video_name}》的影视解说文案,《{video_name}》的大致剧情如下: {video_plot}
文案要符合以下要求:
**任务目标:**
1. 文案字数在 1500字左右,严格要求字数,最低不得少于 1000字。
2. 避免使用 markdown 格式输出文案。
3. 仅输出解说文案,不输出任何其他内容。
4. 不要包含小标题,每个段落以 \n 进行分隔。
"""
try:
response = _generate_response(prompt, llm_provider)
logger.success("解说文案生成成功")
return response
except Exception as err:
return handle_exception(err)
def writing_short_play(video_plot: str, video_name: str, llm_provider: str, count: int = 500):
"""
影视解说(短剧解说)
"""
if not video_plot:
raise ValueError("短剧的简介不能为空")
if not video_name:
raise ValueError("短剧名称不能为空")
prompt = f"""
**角色设定:**
你是一名有10年经验的短剧解说文案的创作者,
下面是关于如何写解说文案的方法 {Method},请认真阅读它,之后我会给你一部短剧作品的简介,然后让你写一篇解说文案
请根据方法撰写 《{video_name}》的解说文案,《{video_name}》的大致剧情如下: {video_plot}
文案要符合以下要求:
**任务目标:**
1. 请严格要求文案字数, 字数控制在 {count} 字左右。
2. 避免使用 markdown 格式输出文案。
3. 仅输出解说文案,不输出任何其他内容。
4. 不要包含小标题,每个段落以 \\n 进行分隔。
"""
try:
response = _generate_response(prompt, llm_provider)
logger.success("解说文案生成成功")
logger.debug(response)
return response
except Exception as err:
return handle_exception(err)
def screen_matching(huamian: str, wenan: str, llm_provider: str):
"""
画面匹配(一次性匹配)
"""
if not huamian:
raise ValueError("画面不能为空")
if not wenan:
raise ValueError("文案不能为空")
prompt = """
你是一名有10年经验的影视解说创作者,
你的任务是根据视频转录脚本和解说文案,匹配出每段解说文案对应的画面时间戳, 结果以 json 格式输出。
注意:
转录脚本中
- timestamp: 表示视频时间戳
- picture: 表示当前画面描述
- speech": 表示当前视频中人物的台词
转录脚本和文案(由 XML 标记<PICTURE></PICTURE>和 <COPYWRITER></COPYWRITER>分隔)如下所示:
<PICTURE>
%s
</PICTURE>
<COPYWRITER>
%s
</COPYWRITER>
在匹配的过程中,请通过确保以下条件来完成匹配:
- 使用以下 JSON schema:
script = {'picture': str, 'timestamp': str(时间戳), "narration": str, "OST": bool(是否开启原声)}
Return: list[script]
- picture: 字段表示当前画面描述,与转录脚本保持一致
- timestamp: 字段表示某一段文案对应的画面的时间戳,不必和转录脚本的时间戳一致,应该充分考虑文案内容,匹配出与其描述最匹配的时间戳
- 请注意,请严格的执行已经出现的画面不能重复出现,即生成的脚本中 timestamp 不能有重叠的部分。
- narration: 字段表示需要解说文案,每段解说文案尽量不要超过30字
- OST: 字段表示是否开启原声,即当 OST 字段为 true 时,narration 字段为空字符串,当 OST 为 false 时,narration 字段为对应的解说文案
- 注意,在画面匹配的过程中,需要适当的加入原声播放,使得解说和画面更加匹配,请按照 1:1 的比例,生成原声和解说的脚本内容。
- 注意,在时间戳匹配上,一定不能原样照搬“转录脚本”,应当适当的合并或者删减一些片段。
- 注意,第一个画面一定是原声播放并且时长不少于 20 s,为了吸引观众,第一段一定是整个转录脚本中最精彩的片段。
- 请以严格的 JSON 格式返回数据,不要包含任何注释、标记或其他字符。数据应符合 JSON 语法,可以被 json.loads() 函数直接解析, 不要添加 ```json 或其他标记。
""" % (huamian, wenan)
try:
response = _generate_response(prompt, llm_provider)
logger.success("匹配成功")
logger.debug(response)
return response
except Exception as err:
return handle_exception(err)
if __name__ == "__main__":
# 1. 视频转录
video_subject = "第二十条之无罪释放"
video_path = "/Users/apple/Desktop/home/pipedream_project/downloads/jianzao.mp4"
language = "zh-CN"
gemini_video_transcription(
video_name=video_subject,
video_path=video_path,
language=language,
progress_callback=print,
llm_provider_video="gemini"
)
# # 2. 解说文案
# video_path = "/Users/apple/Desktop/home/NarratoAI/resource/videos/1.mp4"
# # video_path = "E:\\projects\\NarratoAI\\resource\\videos\\1.mp4"
# video_plot = """
# 李自忠拿着儿子李牧名下的存折,去银行取钱给儿子救命,却被要求证明"你儿子是你儿子"。
# 走投无路时碰到银行被抢劫,劫匪给了他两沓钱救命,李自忠却因此被银行以抢劫罪起诉,并顶格判处20年有期徒刑。
# 苏醒后的李牧坚决为父亲做无罪辩护,面对银行的顶级律师团队,他一个法学院大一学生,能否力挽狂澜,创作奇迹?挥法律之利剑 ,持正义之天平!
# """
# res = generate_script(video_path, video_plot, video_name="第二十条之无罪释放")
# # res = generate_script(video_path, video_plot, video_name="海岸")
# print("脚本生成成功:\n", res)
# res = clean_model_output(res)
# aaa = json.loads(res)
# print(json.dumps(aaa, indent=2, ensure_ascii=False))