mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-08-01 19:05:52 +00:00
feat(short_drama_editing): 添加强短剧混剪剪辑脚本生成完整功能
- 新增短剧混剪脚本生成专用提示词类并完成注册 - 优化merge_script工具函数,支持多视频路径输入、自动填充视频信息 - 扩展SDP处理流水线,新增直接基于剧情分析和字幕生成剪辑脚本的逻辑 - 更新WebUI相关组件与工具函数,适配新的短剧混剪脚本生成流程 - 添加字幕时间戳校验与路径规范化工具,确保生成脚本合法性
This commit is contained in:
parent
8b1fcbafa5
commit
ed4a5d07e5
@ -1,6 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
视频脚本生成pipeline,串联各个处理步骤
|
视频脚本生成pipeline,串联各个处理步骤
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@ -16,6 +18,10 @@ def generate_script_result(
|
|||||||
base_url: str = None,
|
base_url: str = None,
|
||||||
custom_clips: int = 5,
|
custom_clips: int = 5,
|
||||||
provider: str = None,
|
provider: str = None,
|
||||||
|
video_paths=None,
|
||||||
|
plot_analysis: Optional[str] = None,
|
||||||
|
short_name: str = "",
|
||||||
|
drama_genre: str = "",
|
||||||
*,
|
*,
|
||||||
srt_path: Optional[str] = None,
|
srt_path: Optional[str] = None,
|
||||||
subtitle_content: Optional[str] = None,
|
subtitle_content: Optional[str] = None,
|
||||||
@ -30,6 +36,10 @@ def generate_script_result(
|
|||||||
base_url: API基础URL,可选
|
base_url: API基础URL,可选
|
||||||
custom_clips: 自定义片段数量,默认5
|
custom_clips: 自定义片段数量,默认5
|
||||||
provider: LLM服务提供商,可选
|
provider: LLM服务提供商,可选
|
||||||
|
video_paths: 原始视频路径列表,用于生成 video_id/video_name
|
||||||
|
plot_analysis: 已完成的剧情理解文本,提供时会跳过混剪内部剧情理解
|
||||||
|
short_name: 短剧名称
|
||||||
|
drama_genre: 短剧类型
|
||||||
srt_path: 字幕文件路径(向后兼容)
|
srt_path: 字幕文件路径(向后兼容)
|
||||||
subtitle_content: 字幕文本内容
|
subtitle_content: 字幕文本内容
|
||||||
subtitle_file_path: 字幕文件路径(推荐)
|
subtitle_file_path: 字幕文件路径(推荐)
|
||||||
@ -56,10 +66,23 @@ def generate_script_result(
|
|||||||
provider=provider,
|
provider=provider,
|
||||||
srt_path=resolved_path,
|
srt_path=resolved_path,
|
||||||
subtitle_content=resolved_content,
|
subtitle_content=resolved_content,
|
||||||
|
plot_analysis=plot_analysis,
|
||||||
|
video_paths=video_paths,
|
||||||
|
short_name=short_name,
|
||||||
|
drama_genre=drama_genre,
|
||||||
)
|
)
|
||||||
|
|
||||||
adjusted_results = openai_analysis['plot_points']
|
if openai_analysis.get("script_items"):
|
||||||
final_script = merge_script(adjusted_results, output_path)
|
final_script = openai_analysis["script_items"]
|
||||||
|
if not output_path or not str(output_path).strip():
|
||||||
|
raise ValueError("output_path不能为空")
|
||||||
|
os.makedirs(os.path.dirname(str(output_path)) or ".", exist_ok=True)
|
||||||
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(final_script, f, ensure_ascii=False, indent=4)
|
||||||
|
logger.info(f"短剧混剪脚本生成完成:{output_path}")
|
||||||
|
else:
|
||||||
|
adjusted_results = openai_analysis['plot_points']
|
||||||
|
final_script = merge_script(adjusted_results, output_path, video_paths=video_paths)
|
||||||
|
|
||||||
return {"status": "success", "script": final_script}
|
return {"status": "success", "script": final_script}
|
||||||
|
|
||||||
@ -79,6 +102,10 @@ def generate_script(
|
|||||||
base_url: str = None,
|
base_url: str = None,
|
||||||
custom_clips: int = 5,
|
custom_clips: int = 5,
|
||||||
provider: str = None,
|
provider: str = None,
|
||||||
|
video_paths=None,
|
||||||
|
plot_analysis: Optional[str] = None,
|
||||||
|
short_name: str = "",
|
||||||
|
drama_genre: str = "",
|
||||||
*,
|
*,
|
||||||
subtitle_content: Optional[str] = None,
|
subtitle_content: Optional[str] = None,
|
||||||
subtitle_file_path: Optional[str] = None,
|
subtitle_file_path: Optional[str] = None,
|
||||||
@ -93,6 +120,10 @@ def generate_script(
|
|||||||
base_url: API基础URL,可选
|
base_url: API基础URL,可选
|
||||||
custom_clips: 自定义片段数量,默认5
|
custom_clips: 自定义片段数量,默认5
|
||||||
provider: LLM服务提供商,可选
|
provider: LLM服务提供商,可选
|
||||||
|
video_paths: 原始视频路径列表,用于生成 video_id/video_name
|
||||||
|
plot_analysis: 已完成的剧情理解文本
|
||||||
|
short_name: 短剧名称
|
||||||
|
drama_genre: 短剧类型
|
||||||
subtitle_content: 字幕文本内容(可选)
|
subtitle_content: 字幕文本内容(可选)
|
||||||
subtitle_file_path: 字幕文件路径(推荐使用,可选)
|
subtitle_file_path: 字幕文件路径(推荐使用,可选)
|
||||||
|
|
||||||
@ -110,6 +141,10 @@ def generate_script(
|
|||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
custom_clips=custom_clips,
|
custom_clips=custom_clips,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
|
video_paths=video_paths,
|
||||||
|
plot_analysis=plot_analysis,
|
||||||
|
short_name=short_name,
|
||||||
|
drama_genre=drama_genre,
|
||||||
srt_path=srt_path,
|
srt_path=srt_path,
|
||||||
subtitle_content=subtitle_content,
|
subtitle_content=subtitle_content,
|
||||||
subtitle_file_path=subtitle_file_path,
|
subtitle_file_path=subtitle_file_path,
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
使用统一LLM服务,分析字幕文件,返回剧情梗概和爆点
|
使用统一LLM服务,分析字幕文件,返回剧情梗概和爆点
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from app.services.subtitle_text import has_timecodes, normalize_subtitle_text, read_subtitle_text
|
from app.services.subtitle_text import has_timecodes, normalize_subtitle_text, read_subtitle_text
|
||||||
|
from app.services.short_drama_narration_validation import (
|
||||||
|
build_subtitle_index,
|
||||||
|
parse_script_timestamp_range,
|
||||||
|
)
|
||||||
# 导入新的提示词管理系统
|
# 导入新的提示词管理系统
|
||||||
from app.services.prompts import PromptManager
|
from app.services.prompts import PromptManager
|
||||||
# 导入统一LLM服务
|
# 导入统一LLM服务
|
||||||
@ -14,6 +19,176 @@ from app.services.llm.unified_service import UnifiedLLMService
|
|||||||
from app.services.llm.migration_adapter import _run_async_safely
|
from app.services.llm.migration_adapter import _run_async_safely
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_paths(paths):
|
||||||
|
if isinstance(paths, str):
|
||||||
|
paths = [paths]
|
||||||
|
if not paths:
|
||||||
|
return []
|
||||||
|
|
||||||
|
normalized_paths = []
|
||||||
|
seen = set()
|
||||||
|
for path in paths:
|
||||||
|
if not isinstance(path, str):
|
||||||
|
continue
|
||||||
|
path = path.strip()
|
||||||
|
if not path or path in seen:
|
||||||
|
continue
|
||||||
|
normalized_paths.append(path)
|
||||||
|
seen.add(path)
|
||||||
|
return normalized_paths
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_positive_int(value):
|
||||||
|
try:
|
||||||
|
number = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
return number if number > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _match_video_id_by_name(video_name, video_paths):
|
||||||
|
video_name = os.path.basename(str(video_name or "").strip())
|
||||||
|
if not video_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for index, video_path in enumerate(video_paths, start=1):
|
||||||
|
if os.path.basename(video_path) == video_name:
|
||||||
|
return index
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _default_video_name(video_id, video_paths):
|
||||||
|
if 1 <= video_id <= len(video_paths):
|
||||||
|
return os.path.basename(video_paths[video_id - 1])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_short_mix_items(items, video_paths, subtitle_content):
|
||||||
|
if not isinstance(items, list) or not items:
|
||||||
|
raise ValueError("短剧混剪脚本 items 必须是非空数组")
|
||||||
|
|
||||||
|
normalized_video_paths = _normalize_paths(video_paths)
|
||||||
|
subtitle_index = build_subtitle_index(subtitle_content, normalized_video_paths)
|
||||||
|
available_video_ids = {cue.video_id for cue in subtitle_index}
|
||||||
|
if normalized_video_paths:
|
||||||
|
available_video_ids.update(range(1, len(normalized_video_paths) + 1))
|
||||||
|
|
||||||
|
normalized_items = []
|
||||||
|
ranges_by_video = {}
|
||||||
|
for index, raw_item in enumerate(items, start=1):
|
||||||
|
if not isinstance(raw_item, dict):
|
||||||
|
raise ValueError(f"第 {index} 个混剪片段必须是对象")
|
||||||
|
|
||||||
|
item_id = index
|
||||||
|
video_id = (
|
||||||
|
_match_video_id_by_name(raw_item.get("video_name") or raw_item.get("source_video"), normalized_video_paths)
|
||||||
|
or _coerce_positive_int(raw_item.get("video_id") or raw_item.get("video_index"))
|
||||||
|
or 1
|
||||||
|
)
|
||||||
|
if available_video_ids and video_id not in available_video_ids:
|
||||||
|
raise ValueError(f"片段 {item_id} 的 video_id={video_id} 不在已选视频范围内")
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_ms, end_ms, timestamp = parse_script_timestamp_range(raw_item.get("timestamp", ""))
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(f"片段 {item_id}: {exc}") from exc
|
||||||
|
if start_ms >= end_ms:
|
||||||
|
raise ValueError(f"片段 {item_id} 的开始时间必须早于结束时间")
|
||||||
|
|
||||||
|
video_cues = [cue for cue in subtitle_index if cue.video_id == video_id]
|
||||||
|
if video_cues:
|
||||||
|
min_start = min(cue.start_ms for cue in video_cues)
|
||||||
|
max_end = max(cue.end_ms for cue in video_cues)
|
||||||
|
if start_ms < min_start or end_ms > max_end:
|
||||||
|
raise ValueError(f"片段 {item_id} 的时间戳不在视频 {video_id} 的字幕范围内")
|
||||||
|
if not any(start_ms < cue.end_ms and end_ms > cue.start_ms for cue in video_cues):
|
||||||
|
raise ValueError(f"片段 {item_id} 的时间戳没有命中视频 {video_id} 的字幕内容")
|
||||||
|
|
||||||
|
picture = str(
|
||||||
|
raw_item.get("picture")
|
||||||
|
or raw_item.get("title")
|
||||||
|
or raw_item.get("narrative_function")
|
||||||
|
or raw_item.get("intent")
|
||||||
|
or raw_item.get("story_role")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
if not picture:
|
||||||
|
raise ValueError(f"片段 {item_id} 的 picture 不能为空")
|
||||||
|
|
||||||
|
video_name = str(raw_item.get("video_name") or "").strip()
|
||||||
|
if normalized_video_paths:
|
||||||
|
video_name = _default_video_name(video_id, normalized_video_paths)
|
||||||
|
|
||||||
|
normalized_items.append(
|
||||||
|
{
|
||||||
|
"_id": item_id,
|
||||||
|
"video_id": video_id,
|
||||||
|
"video_name": video_name,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"picture": picture,
|
||||||
|
"narration": f"播放原片{item_id}",
|
||||||
|
"OST": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ranges_by_video.setdefault(video_id, []).append((start_ms, end_ms, item_id))
|
||||||
|
|
||||||
|
for video_id, ranges in ranges_by_video.items():
|
||||||
|
ranges = sorted(ranges, key=lambda item: (item[0], item[1], item[2]))
|
||||||
|
previous_start, previous_end, previous_id = ranges[0]
|
||||||
|
for start_ms, end_ms, item_id in ranges[1:]:
|
||||||
|
if start_ms < previous_end:
|
||||||
|
raise ValueError(f"视频 {video_id} 的片段 {item_id} 与片段 {previous_id} 时间戳重叠")
|
||||||
|
if end_ms > previous_end:
|
||||||
|
previous_start, previous_end, previous_id = start_ms, end_ms, item_id
|
||||||
|
|
||||||
|
return normalized_items
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_short_mix_script(
|
||||||
|
*,
|
||||||
|
subtitle_content,
|
||||||
|
plot_analysis,
|
||||||
|
custom_clips,
|
||||||
|
provider,
|
||||||
|
model_name,
|
||||||
|
api_key,
|
||||||
|
base_url,
|
||||||
|
video_paths=None,
|
||||||
|
short_name="",
|
||||||
|
drama_genre="",
|
||||||
|
):
|
||||||
|
script_generation_prompt = PromptManager.get_prompt(
|
||||||
|
category="short_drama_editing",
|
||||||
|
name="script_generation",
|
||||||
|
parameters={
|
||||||
|
"drama_name": short_name or "短剧",
|
||||||
|
"drama_genre": drama_genre or "短剧",
|
||||||
|
"plot_analysis": plot_analysis,
|
||||||
|
"subtitle_content": subtitle_content,
|
||||||
|
"custom_clips": int(custom_clips or 5),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = _run_async_safely(
|
||||||
|
UnifiedLLMService.generate_text,
|
||||||
|
prompt=script_generation_prompt,
|
||||||
|
provider=provider,
|
||||||
|
model=model_name,
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=base_url,
|
||||||
|
temperature=0.1,
|
||||||
|
max_tokens=4000,
|
||||||
|
)
|
||||||
|
|
||||||
|
from webui.tools.generate_short_summary import parse_and_fix_json
|
||||||
|
script_data = parse_and_fix_json(response)
|
||||||
|
if not script_data:
|
||||||
|
raise ValueError("无法解析短剧混剪脚本JSON")
|
||||||
|
|
||||||
|
script_items = script_data.get("items") or script_data.get("segments") or script_data.get("plot_points")
|
||||||
|
return _normalize_short_mix_items(script_items, video_paths, subtitle_content)
|
||||||
|
|
||||||
|
|
||||||
def analyze_subtitle(
|
def analyze_subtitle(
|
||||||
model_name: str,
|
model_name: str,
|
||||||
api_key: str = None,
|
api_key: str = None,
|
||||||
@ -21,7 +196,11 @@ def analyze_subtitle(
|
|||||||
custom_clips: int = 5,
|
custom_clips: int = 5,
|
||||||
provider: str = None,
|
provider: str = None,
|
||||||
srt_path: str = None,
|
srt_path: str = None,
|
||||||
subtitle_content: str = None
|
subtitle_content: str = None,
|
||||||
|
plot_analysis: str = None,
|
||||||
|
video_paths=None,
|
||||||
|
short_name: str = "",
|
||||||
|
drama_genre: str = "",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分析字幕内容,返回完整的分析结果
|
"""分析字幕内容,返回完整的分析结果
|
||||||
|
|
||||||
@ -33,6 +212,10 @@ def analyze_subtitle(
|
|||||||
provider (str, optional): LLM服务提供商. Defaults to None.
|
provider (str, optional): LLM服务提供商. Defaults to None.
|
||||||
srt_path (str, optional): SRT字幕文件路径(与subtitle_content二选一)
|
srt_path (str, optional): SRT字幕文件路径(与subtitle_content二选一)
|
||||||
subtitle_content (str, optional): SRT字幕文本内容(与srt_path二选一)
|
subtitle_content (str, optional): SRT字幕文本内容(与srt_path二选一)
|
||||||
|
plot_analysis (str, optional): 已审核/缓存的剧情理解文本,提供时直接进入混剪脚本生成
|
||||||
|
video_paths (list, optional): 原始视频路径列表,用于补齐 video_id/video_name
|
||||||
|
short_name (str, optional): 短剧名称
|
||||||
|
drama_genre (str, optional): 短剧类型
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: 包含剧情梗概和结构化的时间段分析的字典
|
dict: 包含剧情梗概和结构化的时间段分析的字典
|
||||||
@ -87,6 +270,27 @@ def analyze_subtitle(
|
|||||||
|
|
||||||
logger.info(f"使用LLM服务分析字幕,提供商: {provider}, 模型: {model_name}")
|
logger.info(f"使用LLM服务分析字幕,提供商: {provider}, 模型: {model_name}")
|
||||||
|
|
||||||
|
if plot_analysis and str(plot_analysis).strip():
|
||||||
|
logger.info("使用已有剧情理解直接生成短剧混剪脚本")
|
||||||
|
script_items = _generate_short_mix_script(
|
||||||
|
subtitle_content=subtitle_content,
|
||||||
|
plot_analysis=str(plot_analysis).strip(),
|
||||||
|
custom_clips=custom_clips,
|
||||||
|
provider=provider,
|
||||||
|
model_name=model_name,
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=base_url,
|
||||||
|
video_paths=video_paths,
|
||||||
|
short_name=short_name,
|
||||||
|
drama_genre=drama_genre,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"summary": str(plot_analysis).strip(),
|
||||||
|
"plot_titles": [],
|
||||||
|
"plot_points": [],
|
||||||
|
"script_items": script_items,
|
||||||
|
}
|
||||||
|
|
||||||
# 使用新的提示词管理系统
|
# 使用新的提示词管理系统
|
||||||
subtitle_analysis_prompt = PromptManager.get_prompt(
|
subtitle_analysis_prompt = PromptManager.get_prompt(
|
||||||
category="short_drama_editing",
|
category="short_drama_editing",
|
||||||
@ -120,6 +324,28 @@ def analyze_subtitle(
|
|||||||
logger.info(f"字幕分析完成,找到 {len(summary_data.get('plot_titles', []))} 个关键情节")
|
logger.info(f"字幕分析完成,找到 {len(summary_data.get('plot_titles', []))} 个关键情节")
|
||||||
logger.debug(json.dumps(summary_data, indent=4, ensure_ascii=False))
|
logger.debug(json.dumps(summary_data, indent=4, ensure_ascii=False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
script_items = _generate_short_mix_script(
|
||||||
|
subtitle_content=subtitle_content,
|
||||||
|
plot_analysis=json.dumps(summary_data, ensure_ascii=False, indent=2),
|
||||||
|
custom_clips=custom_clips,
|
||||||
|
provider=provider,
|
||||||
|
model_name=model_name,
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=base_url,
|
||||||
|
video_paths=video_paths,
|
||||||
|
short_name=short_name,
|
||||||
|
drama_genre=drama_genre,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"summary": summary_data.get("summary", ""),
|
||||||
|
"plot_titles": summary_data.get("plot_titles", []),
|
||||||
|
"plot_points": [],
|
||||||
|
"script_items": script_items,
|
||||||
|
}
|
||||||
|
except Exception as direct_script_error:
|
||||||
|
logger.warning(f"直接生成短剧混剪脚本失败,回退到时间段定位: {direct_script_error}")
|
||||||
|
|
||||||
# 构建爆点标题列表
|
# 构建爆点标题列表
|
||||||
plot_titles_text = ""
|
plot_titles_text = ""
|
||||||
logger.info(f"找到 {len(summary_data.get('plot_titles', []))} 个片段")
|
logger.info(f"找到 {len(summary_data.get('plot_titles', []))} 个片段")
|
||||||
|
|||||||
@ -8,7 +8,8 @@ from typing import Dict, List
|
|||||||
|
|
||||||
def merge_script(
|
def merge_script(
|
||||||
plot_points: List[Dict],
|
plot_points: List[Dict],
|
||||||
output_path: str
|
output_path: str,
|
||||||
|
video_paths=None,
|
||||||
):
|
):
|
||||||
"""合并生成最终脚本
|
"""合并生成最终脚本
|
||||||
|
|
||||||
@ -19,6 +20,10 @@ def merge_script(
|
|||||||
Returns:
|
Returns:
|
||||||
str: 最终合并的脚本
|
str: 最终合并的脚本
|
||||||
"""
|
"""
|
||||||
|
if isinstance(video_paths, str):
|
||||||
|
video_paths = [video_paths]
|
||||||
|
video_paths = [path for path in (video_paths or []) if isinstance(path, str) and path.strip()]
|
||||||
|
|
||||||
# 创建包含所有信息的临时列表
|
# 创建包含所有信息的临时列表
|
||||||
final_script = []
|
final_script = []
|
||||||
|
|
||||||
@ -29,9 +34,12 @@ def merge_script(
|
|||||||
"_id": number,
|
"_id": number,
|
||||||
"timestamp": plot_point["timestamp"],
|
"timestamp": plot_point["timestamp"],
|
||||||
"picture": plot_point["picture"],
|
"picture": plot_point["picture"],
|
||||||
"narration": f"播放原生_{os.urandom(4).hex()}",
|
"narration": f"播放原片{number}",
|
||||||
"OST": 1, # OST=0 仅保留解说 OST=2 保留解说和原声
|
"OST": 1, # OST=0 仅保留解说 OST=2 保留解说和原声
|
||||||
}
|
}
|
||||||
|
if video_paths:
|
||||||
|
script_item["video_id"] = 1
|
||||||
|
script_item["video_name"] = os.path.basename(video_paths[0])
|
||||||
final_script.append(script_item)
|
final_script.append(script_item)
|
||||||
number += 1
|
number += 1
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
from .subtitle_analysis import SubtitleAnalysisPrompt
|
from .subtitle_analysis import SubtitleAnalysisPrompt
|
||||||
from .plot_extraction import PlotExtractionPrompt
|
from .plot_extraction import PlotExtractionPrompt
|
||||||
|
from .script_generation import ScriptGenerationPrompt
|
||||||
from ..manager import PromptManager
|
from ..manager import PromptManager
|
||||||
|
|
||||||
|
|
||||||
@ -25,9 +26,14 @@ def register_prompts():
|
|||||||
plot_extraction_prompt = PlotExtractionPrompt()
|
plot_extraction_prompt = PlotExtractionPrompt()
|
||||||
PromptManager.register_prompt(plot_extraction_prompt, is_default=True)
|
PromptManager.register_prompt(plot_extraction_prompt, is_default=True)
|
||||||
|
|
||||||
|
# 注册混剪脚本生成提示词
|
||||||
|
script_generation_prompt = ScriptGenerationPrompt()
|
||||||
|
PromptManager.register_prompt(script_generation_prompt, is_default=True)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SubtitleAnalysisPrompt",
|
"SubtitleAnalysisPrompt",
|
||||||
"PlotExtractionPrompt",
|
"PlotExtractionPrompt",
|
||||||
|
"ScriptGenerationPrompt",
|
||||||
"register_prompts"
|
"register_prompts"
|
||||||
]
|
]
|
||||||
|
|||||||
113
app/services/prompts/short_drama_editing/script_generation.py
Normal file
113
app/services/prompts/short_drama_editing/script_generation.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
@Project: 短剧混剪-剪辑脚本生成
|
||||||
|
@File : script_generation.py
|
||||||
|
@Description: 基于剧情理解和字幕直接生成短剧混剪脚本
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ..base import ParameterizedPrompt, PromptMetadata, ModelType, OutputFormat
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptGenerationPrompt(ParameterizedPrompt):
|
||||||
|
"""短剧混剪脚本生成提示词"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
metadata = PromptMetadata(
|
||||||
|
name="script_generation",
|
||||||
|
category="short_drama_editing",
|
||||||
|
version="v1.0",
|
||||||
|
description="基于剧情理解和原始字幕直接生成短剧混剪脚本,不生成解说文案",
|
||||||
|
model_type=ModelType.TEXT,
|
||||||
|
output_format=OutputFormat.JSON,
|
||||||
|
tags=["短剧", "混剪", "剪辑脚本", "时间戳", "多视频", "原声"],
|
||||||
|
parameters=[
|
||||||
|
"drama_name",
|
||||||
|
"drama_genre",
|
||||||
|
"plot_analysis",
|
||||||
|
"subtitle_content",
|
||||||
|
"custom_clips",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
metadata,
|
||||||
|
required_parameters=["plot_analysis", "subtitle_content", "custom_clips"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self._system_prompt = (
|
||||||
|
"你是一名专业短剧混剪剪辑师。你必须严格输出JSON,"
|
||||||
|
"只从字幕中选择真实存在的可剪辑原声片段,不生成解说文案。"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_template(self) -> str:
|
||||||
|
return """# 短剧混剪脚本生成任务
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
根据剧情理解和原始字幕,为短剧《${drama_name}》生成一份可直接裁剪的混剪 JSON 脚本。
|
||||||
|
|
||||||
|
短剧混剪与短剧解说的区别:
|
||||||
|
- 不生成解说文案。
|
||||||
|
- 不需要用户审核旁白。
|
||||||
|
- 直接从剧情理解中选择能串成故事线的原片片段。
|
||||||
|
- 每个片段默认保留原声,OST 必须为 1。
|
||||||
|
|
||||||
|
## 用户选择的短剧类型
|
||||||
|
<drama_genre>
|
||||||
|
${drama_genre}
|
||||||
|
</drama_genre>
|
||||||
|
|
||||||
|
## 需要生成的片段数量
|
||||||
|
<custom_clips>
|
||||||
|
${custom_clips}
|
||||||
|
</custom_clips>
|
||||||
|
|
||||||
|
## 剧情理解材料
|
||||||
|
<plot>
|
||||||
|
${plot_analysis}
|
||||||
|
</plot>
|
||||||
|
|
||||||
|
## 原始字幕(含视频编号和局部时间戳)
|
||||||
|
<subtitles>
|
||||||
|
${subtitle_content}
|
||||||
|
</subtitles>
|
||||||
|
|
||||||
|
## 选择原则
|
||||||
|
1. 选择 ${custom_clips} 个片段,尽量形成“开端 -> 冲突升级 -> 高潮/反转 -> 悬念或阶段结果”的完整观看路径。
|
||||||
|
2. 只能使用原始字幕中真实存在的视频编号、视频文件名和时间范围。
|
||||||
|
3. timestamp 必须是对应 video_id 内部的局部时间戳,格式为 "HH:MM:SS,mmm-HH:MM:SS,mmm"。
|
||||||
|
4. 同一个 video_id 内的片段不得交叉或重叠;整体顺序要服务剧情理解,单个视频内尽量按时间顺序。
|
||||||
|
5. 优先选择关键对白、身份揭露、情绪爆发、反转、冲突升级和能看懂前因后果的片段。
|
||||||
|
6. 单个片段建议 5-45 秒;不要只截 1-2 秒的孤立金句,也不要截过长的流水账。
|
||||||
|
7. 如果两个关键剧情之间跳跃太大,优先选择包含上下文的连续时间段,而不是硬切爆点。
|
||||||
|
8. picture 要描述画面中人物、动作、情绪、场景和该片段的剧情作用。
|
||||||
|
9. narration 字段必须写成“播放原片+_id”,例如 _id 为 3 时写“播放原片3”。
|
||||||
|
10. OST 必须为 1,表示保留原片原声。
|
||||||
|
|
||||||
|
## 字段规则
|
||||||
|
- _id:从 1 开始连续递增。
|
||||||
|
- video_id:来自字幕分段标题,例如“视频 2”就填 2;单视频填 1。
|
||||||
|
- video_name:对应视频文件名,必须从字幕分段标题提取;单视频也要填写。
|
||||||
|
- timestamp:必须来自对应视频字幕时间轴。
|
||||||
|
- picture:非空字符串。
|
||||||
|
- narration:固定为“播放原片+_id”。
|
||||||
|
- OST:固定为 1。
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
只输出严格 JSON:
|
||||||
|
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"_id": 1,
|
||||||
|
"video_id": 1,
|
||||||
|
"video_name": "1.mp4",
|
||||||
|
"timestamp": "00:00:01,000-00:00:12,500",
|
||||||
|
"picture": "女主被当众羞辱仍然强撑,冲突正式爆发,为后续逆袭埋下情绪钩子",
|
||||||
|
"narration": "播放原片1",
|
||||||
|
"OST": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
现在请生成短剧混剪脚本。"""
|
||||||
@ -26,6 +26,7 @@ from webui.tools.generate_short_summary import (
|
|||||||
|
|
||||||
|
|
||||||
SCRIPT_TABLE_BASE_COLUMNS = ["_id", "video_id", "video_name", "timestamp", "picture", "narration", "OST"]
|
SCRIPT_TABLE_BASE_COLUMNS = ["_id", "video_id", "video_name", "timestamp", "picture", "narration", "OST"]
|
||||||
|
SCRIPT_TABLE_TEXT_COLUMNS = {"video_name", "timestamp", "picture", "narration", "value"}
|
||||||
MODE_FILE = "file_selection"
|
MODE_FILE = "file_selection"
|
||||||
MODE_AUTO = "auto"
|
MODE_AUTO = "auto"
|
||||||
MODE_SHORT = "short"
|
MODE_SHORT = "short"
|
||||||
@ -666,16 +667,42 @@ def render_short_generate_options(tr):
|
|||||||
渲染Short Generate模式下的特殊选项
|
渲染Short Generate模式下的特殊选项
|
||||||
在Short Generate模式下,替换原有的输入框为自定义片段选项
|
在Short Generate模式下,替换原有的输入框为自定义片段选项
|
||||||
"""
|
"""
|
||||||
summary_narration_panel(tr, SUMMARY_MODE_CONFIGS[MODE_SHORT_SUMMARY])
|
summary_config = SUMMARY_MODE_CONFIGS[MODE_SHORT_SUMMARY]
|
||||||
# 显示自定义片段数量选择器
|
summary_narration_panel(tr, summary_config)
|
||||||
custom_clips = st.number_input(
|
|
||||||
tr("自定义片段"),
|
type_option_key = _summary_state_key(summary_config, "type_option")
|
||||||
min_value=1,
|
custom_type_key = _summary_state_key(summary_config, "custom_type")
|
||||||
max_value=20,
|
type_options = [code for code, _ in summary_config["type_options"]]
|
||||||
value=st.session_state.get('custom_clips', 5),
|
if st.session_state.get(type_option_key) not in type_options:
|
||||||
help=tr("设置需要生成的短视频片段数量"),
|
st.session_state[type_option_key] = summary_config["default_type"]
|
||||||
key="custom_clips_input"
|
|
||||||
)
|
show_custom_type = st.session_state.get(type_option_key, summary_config["default_type"]) == "custom"
|
||||||
|
option_cols = st.columns([1.1, 1.1, 1], vertical_alignment="bottom") if show_custom_type else st.columns([1.1, 1], vertical_alignment="bottom")
|
||||||
|
with option_cols[0]:
|
||||||
|
st.selectbox(
|
||||||
|
tr(summary_config["type_label_key"]),
|
||||||
|
options=type_options,
|
||||||
|
format_func=lambda code: tr(dict(summary_config["type_options"]).get(code, code)),
|
||||||
|
key=type_option_key,
|
||||||
|
)
|
||||||
|
option_index = 1
|
||||||
|
if show_custom_type:
|
||||||
|
with option_cols[option_index]:
|
||||||
|
st.text_input(
|
||||||
|
tr(summary_config["custom_type_label_key"]),
|
||||||
|
key=custom_type_key,
|
||||||
|
placeholder=tr(summary_config["custom_type_placeholder_key"]),
|
||||||
|
)
|
||||||
|
option_index += 1
|
||||||
|
with option_cols[option_index]:
|
||||||
|
custom_clips = st.number_input(
|
||||||
|
tr("自定义片段"),
|
||||||
|
min_value=1,
|
||||||
|
max_value=20,
|
||||||
|
value=st.session_state.get('custom_clips', 5),
|
||||||
|
help=tr("设置需要生成的短视频片段数量"),
|
||||||
|
key="custom_clips_input"
|
||||||
|
)
|
||||||
st.session_state['custom_clips'] = custom_clips
|
st.session_state['custom_clips'] = custom_clips
|
||||||
|
|
||||||
|
|
||||||
@ -729,6 +756,7 @@ def summary_narration_panel(tr, summary_config):
|
|||||||
plot_analysis_key = _summary_state_key(summary_config, "plot_analysis")
|
plot_analysis_key = _summary_state_key(summary_config, "plot_analysis")
|
||||||
plot_source_key = _summary_state_key(summary_config, "plot_analysis_subtitle_path")
|
plot_source_key = _summary_state_key(summary_config, "plot_analysis_subtitle_path")
|
||||||
plot_signature_key = _summary_state_key(summary_config, "plot_analysis_signature")
|
plot_signature_key = _summary_state_key(summary_config, "plot_analysis_signature")
|
||||||
|
pending_plot_key = _summary_state_key(summary_config, "pending_plot_analysis")
|
||||||
|
|
||||||
st.markdown(
|
st.markdown(
|
||||||
f"""
|
f"""
|
||||||
@ -815,6 +843,15 @@ def summary_narration_panel(tr, summary_config):
|
|||||||
st.session_state[plot_analysis_key] = ""
|
st.session_state[plot_analysis_key] = ""
|
||||||
st.session_state[plot_source_key] = ""
|
st.session_state[plot_source_key] = ""
|
||||||
st.session_state[plot_signature_key] = ""
|
st.session_state[plot_signature_key] = ""
|
||||||
|
st.session_state.pop(pending_plot_key, None)
|
||||||
|
else:
|
||||||
|
pending_plot = st.session_state.pop(pending_plot_key, None)
|
||||||
|
if isinstance(pending_plot, dict) and pending_plot.get("signature") == current_signature:
|
||||||
|
pending_analysis = str(pending_plot.get("plot_analysis") or "")
|
||||||
|
if pending_analysis:
|
||||||
|
st.session_state[plot_analysis_key] = pending_analysis
|
||||||
|
st.session_state[plot_source_key] = pending_plot.get("subtitle_path") or current_subtitle_path
|
||||||
|
st.session_state[plot_signature_key] = current_signature
|
||||||
|
|
||||||
if analyze_plot_clicked:
|
if analyze_plot_clicked:
|
||||||
with st.spinner(tr("Analyzing plot...")):
|
with st.spinner(tr("Analyzing plot...")):
|
||||||
@ -1003,10 +1040,17 @@ def _script_json_to_table(script_data):
|
|||||||
{"value": json.dumps(item, ensure_ascii=False)}
|
{"value": json.dumps(item, ensure_ascii=False)}
|
||||||
for item in script_data
|
for item in script_data
|
||||||
]
|
]
|
||||||
return pd.DataFrame(rows, columns=["value"])
|
return _normalize_script_table_types(pd.DataFrame(rows, columns=["value"]))
|
||||||
|
|
||||||
columns = _ordered_script_columns(script_data)
|
columns = _ordered_script_columns(script_data)
|
||||||
return pd.DataFrame(script_data, columns=columns)
|
return _normalize_script_table_types(pd.DataFrame(script_data, columns=columns))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_script_table_types(table_data):
|
||||||
|
for column in SCRIPT_TABLE_TEXT_COLUMNS:
|
||||||
|
if column in table_data.columns:
|
||||||
|
table_data[column] = table_data[column].where(table_data[column].notna(), "").astype(str).astype("object")
|
||||||
|
return table_data
|
||||||
|
|
||||||
|
|
||||||
def _normalize_script_table_value(column, value):
|
def _normalize_script_table_value(column, value):
|
||||||
@ -1723,8 +1767,66 @@ def render_script_buttons(tr, params):
|
|||||||
generate_script_docu(params, tr)
|
generate_script_docu(params, tr)
|
||||||
elif script_path == "short":
|
elif script_path == "short":
|
||||||
# 执行 短剧混剪 脚本生成
|
# 执行 短剧混剪 脚本生成
|
||||||
|
summary_config = SUMMARY_MODE_CONFIGS[MODE_SHORT_SUMMARY]
|
||||||
|
type_option_key = _summary_state_key(summary_config, "type_option")
|
||||||
|
custom_type_key = _summary_state_key(summary_config, "custom_type")
|
||||||
|
web_search_key = _summary_state_key(summary_config, "web_search_enabled")
|
||||||
|
plot_analysis_key = _summary_state_key(summary_config, "plot_analysis")
|
||||||
|
plot_source_key = _summary_state_key(summary_config, "plot_analysis_subtitle_path")
|
||||||
|
plot_signature_key = _summary_state_key(summary_config, "plot_analysis_signature")
|
||||||
|
pending_plot_key = _summary_state_key(summary_config, "pending_plot_analysis")
|
||||||
|
if (
|
||||||
|
st.session_state.get(type_option_key) == "custom"
|
||||||
|
and not str(st.session_state.get(custom_type_key, '') or '').strip()
|
||||||
|
):
|
||||||
|
st.error(tr(summary_config["custom_type_empty_key"]))
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
subtitle_paths = _selected_subtitle_paths()
|
||||||
|
subtitle_path = subtitle_paths[0] if subtitle_paths else None
|
||||||
|
video_theme = st.session_state.get('video_theme')
|
||||||
|
web_search_enabled = bool(st.session_state.get(web_search_key, False))
|
||||||
|
current_signature = _short_drama_plot_analysis_signature(
|
||||||
|
subtitle_paths,
|
||||||
|
video_theme,
|
||||||
|
web_search_enabled,
|
||||||
|
_selected_video_paths(),
|
||||||
|
)
|
||||||
|
plot_analysis = ""
|
||||||
|
if st.session_state.get(plot_signature_key) == current_signature:
|
||||||
|
plot_analysis = st.session_state.get(plot_analysis_key, '')
|
||||||
|
elif (
|
||||||
|
not web_search_enabled
|
||||||
|
and st.session_state.get(plot_source_key) == subtitle_path
|
||||||
|
):
|
||||||
|
plot_analysis = st.session_state.get(plot_analysis_key, '')
|
||||||
|
|
||||||
custom_clips = st.session_state.get('custom_clips')
|
custom_clips = st.session_state.get('custom_clips')
|
||||||
generate_script_short(tr, params, custom_clips)
|
short_result = generate_script_short(
|
||||||
|
tr,
|
||||||
|
params,
|
||||||
|
custom_clips,
|
||||||
|
subtitle_paths=subtitle_paths,
|
||||||
|
video_theme=video_theme,
|
||||||
|
temperature=st.session_state.get('temperature', 0.7),
|
||||||
|
plot_analysis=plot_analysis,
|
||||||
|
subtitle_content=st.session_state.get('subtitle_content', ''),
|
||||||
|
enable_web_search=web_search_enabled,
|
||||||
|
video_paths=_selected_video_paths(),
|
||||||
|
drama_genre=_resolve_short_drama_type(),
|
||||||
|
prompt_category=summary_config["prompt_category"],
|
||||||
|
search_keywords=summary_config["search_keywords"],
|
||||||
|
empty_title_message_key=summary_config["empty_title_message_key"],
|
||||||
|
web_search_context_description=summary_config["web_search_context_description"],
|
||||||
|
)
|
||||||
|
if short_result and short_result.get("plot_analysis"):
|
||||||
|
st.session_state[pending_plot_key] = {
|
||||||
|
"plot_analysis": short_result["plot_analysis"],
|
||||||
|
"subtitle_path": subtitle_path,
|
||||||
|
"signature": current_signature,
|
||||||
|
}
|
||||||
|
st.session_state[plot_source_key] = subtitle_path
|
||||||
|
st.session_state[plot_signature_key] = current_signature
|
||||||
else:
|
else:
|
||||||
load_script(tr, script_path)
|
load_script(tr, script_path)
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,32 @@ from loguru import logger
|
|||||||
from app.config import config
|
from app.config import config
|
||||||
from app.services.upload_validation import ensure_existing_file, InputValidationError
|
from app.services.upload_validation import ensure_existing_file, InputValidationError
|
||||||
from app.utils import utils
|
from app.utils import utils
|
||||||
|
from webui.tools.generate_short_summary import (
|
||||||
|
SHORT_DRAMA_PROMPT_CATEGORY,
|
||||||
|
SHORT_DRAMA_SEARCH_KEYWORDS,
|
||||||
|
_build_combined_subtitle_content,
|
||||||
|
_normalize_paths,
|
||||||
|
analyze_short_drama_plot,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_script_short(tr, params, custom_clips=5):
|
def generate_script_short(
|
||||||
|
tr,
|
||||||
|
params,
|
||||||
|
custom_clips=5,
|
||||||
|
subtitle_paths=None,
|
||||||
|
video_theme=None,
|
||||||
|
temperature=0.7,
|
||||||
|
plot_analysis=None,
|
||||||
|
subtitle_content=None,
|
||||||
|
enable_web_search=False,
|
||||||
|
video_paths=None,
|
||||||
|
drama_genre="逆袭/复仇",
|
||||||
|
prompt_category=SHORT_DRAMA_PROMPT_CATEGORY,
|
||||||
|
search_keywords=SHORT_DRAMA_SEARCH_KEYWORDS,
|
||||||
|
empty_title_message_key="Please enter short drama name before web search",
|
||||||
|
web_search_context_description="短剧名称、人物关系、剧情背景和公开剧情梗概",
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
生成短视频脚本
|
生成短视频脚本
|
||||||
|
|
||||||
@ -18,6 +41,14 @@ def generate_script_short(tr, params, custom_clips=5):
|
|||||||
tr: 翻译函数
|
tr: 翻译函数
|
||||||
params: 视频参数对象
|
params: 视频参数对象
|
||||||
custom_clips: 自定义片段数量,默认为5
|
custom_clips: 自定义片段数量,默认为5
|
||||||
|
subtitle_paths: 已转写/上传/翻译/校准后的字幕路径列表
|
||||||
|
video_theme: 短剧名称
|
||||||
|
temperature: LLM温度
|
||||||
|
plot_analysis: 已完成的剧情理解文本
|
||||||
|
subtitle_content: 已合并的字幕文本
|
||||||
|
enable_web_search: 是否在剧情理解前联网搜索
|
||||||
|
video_paths: 原始视频路径列表
|
||||||
|
drama_genre: 用户选择的短剧类型
|
||||||
"""
|
"""
|
||||||
progress_bar = st.progress(0)
|
progress_bar = st.progress(0)
|
||||||
status_text = st.empty()
|
status_text = st.empty()
|
||||||
@ -33,38 +64,47 @@ def generate_script_short(tr, params, custom_clips=5):
|
|||||||
with st.spinner(tr("Generating script...")):
|
with st.spinner(tr("Generating script...")):
|
||||||
# ========== 严格验证:必须上传视频和字幕(与短剧解说保持一致)==========
|
# ========== 严格验证:必须上传视频和字幕(与短剧解说保持一致)==========
|
||||||
# 1. 验证视频文件
|
# 1. 验证视频文件
|
||||||
video_path = getattr(params, "video_origin_path", None)
|
selected_video_paths = _normalize_paths(
|
||||||
if not video_path or not str(video_path).strip():
|
video_paths
|
||||||
|
or getattr(params, "video_origin_paths", [])
|
||||||
|
or getattr(params, "video_origin_path", "")
|
||||||
|
)
|
||||||
|
if not selected_video_paths:
|
||||||
st.error(tr("Please select video file first"))
|
st.error(tr("Please select video file first"))
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
try:
|
for video_path in selected_video_paths:
|
||||||
ensure_existing_file(
|
try:
|
||||||
str(video_path),
|
ensure_existing_file(
|
||||||
label=tr("Video"),
|
str(video_path),
|
||||||
allowed_exts=(".mp4", ".mov", ".avi", ".flv", ".mkv"),
|
label=tr("Video"),
|
||||||
)
|
allowed_exts=(".mp4", ".mov", ".avi", ".flv", ".mkv"),
|
||||||
except InputValidationError as e:
|
)
|
||||||
st.error(str(e))
|
except InputValidationError as e:
|
||||||
st.stop()
|
st.error(str(e))
|
||||||
|
st.stop()
|
||||||
|
|
||||||
# 2. 验证字幕文件(移除推断逻辑,必须上传)
|
# 2. 验证字幕文件(移除推断逻辑,必须上传)
|
||||||
subtitle_path = st.session_state.get("subtitle_path")
|
subtitle_paths = _normalize_paths(subtitle_paths or st.session_state.get("subtitle_paths") or st.session_state.get("subtitle_path"))
|
||||||
if not subtitle_path or not str(subtitle_path).strip():
|
if not subtitle_paths:
|
||||||
st.error(tr("Please upload subtitle file first"))
|
st.error(tr("Please upload subtitle file first"))
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
|
validated_subtitle_paths = []
|
||||||
try:
|
try:
|
||||||
subtitle_path = ensure_existing_file(
|
for subtitle_path in subtitle_paths:
|
||||||
str(subtitle_path),
|
validated_subtitle_paths.append(
|
||||||
label=tr("Subtitle"),
|
ensure_existing_file(
|
||||||
allowed_exts=(".srt",),
|
str(subtitle_path),
|
||||||
)
|
label=tr("Subtitle"),
|
||||||
|
allowed_exts=(".srt",),
|
||||||
|
)
|
||||||
|
)
|
||||||
except InputValidationError as e:
|
except InputValidationError as e:
|
||||||
st.error(str(e))
|
st.error(str(e))
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
logger.info(f"使用用户上传的字幕文件: {subtitle_path}")
|
logger.info(f"使用用户处理后的字幕文件: {validated_subtitle_paths}")
|
||||||
|
|
||||||
# ========== 获取 LLM 配置 ==========
|
# ========== 获取 LLM 配置 ==========
|
||||||
text_provider = config.app.get('text_llm_provider', 'gemini').lower()
|
text_provider = config.app.get('text_llm_provider', 'gemini').lower()
|
||||||
@ -80,18 +120,40 @@ def generate_script_short(tr, params, custom_clips=5):
|
|||||||
|
|
||||||
update_progress(20, tr("Preparing script generation"))
|
update_progress(20, tr("Preparing script generation"))
|
||||||
|
|
||||||
|
subtitle_content = str(subtitle_content or "").strip() or _build_combined_subtitle_content(
|
||||||
|
validated_subtitle_paths,
|
||||||
|
selected_video_paths,
|
||||||
|
)
|
||||||
|
if not subtitle_content:
|
||||||
|
st.error(tr("Subtitle file is empty or unreadable"))
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
plot_analysis = str(plot_analysis or "").strip()
|
||||||
|
if not plot_analysis:
|
||||||
|
update_progress(35, tr("Analyzing subtitles with model..."))
|
||||||
|
plot_analysis = analyze_short_drama_plot(
|
||||||
|
validated_subtitle_paths,
|
||||||
|
temperature,
|
||||||
|
tr,
|
||||||
|
subtitle_content=subtitle_content,
|
||||||
|
short_name=video_theme,
|
||||||
|
enable_web_search=enable_web_search,
|
||||||
|
video_paths=selected_video_paths,
|
||||||
|
prompt_category=prompt_category,
|
||||||
|
search_keywords=search_keywords,
|
||||||
|
empty_title_message_key=empty_title_message_key,
|
||||||
|
web_search_context_description=web_search_context_description,
|
||||||
|
)
|
||||||
|
if not plot_analysis:
|
||||||
|
st.error(tr("Script generation failed check logs"))
|
||||||
|
st.stop()
|
||||||
|
|
||||||
# ========== 调用后端生成脚本 ==========
|
# ========== 调用后端生成脚本 ==========
|
||||||
from app.services.SDP.generate_script_short import generate_script_result
|
from app.services.SDP.generate_script_short import generate_script_result
|
||||||
|
|
||||||
output_path = os.path.join(utils.script_dir(), "merged_subtitle.json")
|
output_path = os.path.join(utils.script_dir(), "merged_subtitle.json")
|
||||||
|
|
||||||
subtitle_content = st.session_state.get("subtitle_content")
|
update_progress(55, tr("Generating script..."))
|
||||||
subtitle_kwargs = (
|
|
||||||
{"subtitle_content": str(subtitle_content)}
|
|
||||||
if subtitle_content is not None and str(subtitle_content).strip()
|
|
||||||
else {"subtitle_file_path": subtitle_path}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = generate_script_result(
|
result = generate_script_result(
|
||||||
api_key=text_api_key,
|
api_key=text_api_key,
|
||||||
model_name=text_model,
|
model_name=text_model,
|
||||||
@ -99,7 +161,11 @@ def generate_script_short(tr, params, custom_clips=5):
|
|||||||
base_url=text_base_url,
|
base_url=text_base_url,
|
||||||
custom_clips=custom_clips,
|
custom_clips=custom_clips,
|
||||||
provider=text_provider,
|
provider=text_provider,
|
||||||
**subtitle_kwargs,
|
subtitle_content=subtitle_content,
|
||||||
|
video_paths=selected_video_paths,
|
||||||
|
plot_analysis=plot_analysis,
|
||||||
|
short_name=video_theme or "",
|
||||||
|
drama_genre=drama_genre or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.get("status") != "success":
|
if result.get("status") != "success":
|
||||||
@ -120,8 +186,14 @@ def generate_script_short(tr, params, custom_clips=5):
|
|||||||
progress_bar.progress(100)
|
progress_bar.progress(100)
|
||||||
status_text.text(tr("Script generation completed!"))
|
status_text.text(tr("Script generation completed!"))
|
||||||
st.success(tr("Video script generated successfully"))
|
st.success(tr("Video script generated successfully"))
|
||||||
|
return {
|
||||||
|
"script": st.session_state.get('video_clip_json', []),
|
||||||
|
"plot_analysis": plot_analysis,
|
||||||
|
"subtitle_content": subtitle_content,
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
progress_bar.progress(100)
|
progress_bar.progress(100)
|
||||||
st.error(f"{tr('Generation error')}: {str(err)}")
|
st.error(f"{tr('Generation error')}: {str(err)}")
|
||||||
logger.exception(f"生成脚本时发生错误\n{traceback.format_exc()}")
|
logger.exception(f"生成脚本时发生错误\n{traceback.format_exc()}")
|
||||||
|
return None
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user