初始提交:添加热门视频分析项目代码

This commit is contained in:
yujie_jiang 2025-05-28 13:52:34 +08:00
commit db3413a8dc
479 changed files with 3919 additions and 0 deletions

771
base_line/prompt_manager.py Normal file
View File

@ -0,0 +1,771 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Manages the construction of prompts for different AI generation tasks.
"""
import os
import traceback
import logging # Add logging
import re # 添加正则表达式支持
import random # 添加随机模块支持
from .resource_loader import ResourceLoader # Use relative import within the same package
import json
class PromptManager:
"""Handles the loading and construction of prompts."""
def __init__(self,
topic_system_prompt_path: str,
topic_user_prompt_path: str,
content_system_prompt_path: str,
prompts_dir: str = None, # 兼容旧配置
prompts_config: list = None, # 新的配置方式
resource_dir_config: list = None,
topic_gen_num: int = 1, # Default values if needed
topic_gen_date: str = "",
content_judger_system_prompt_path: str = None # 添加内容审核系统提示词路径参数
):
self.topic_system_prompt_path = topic_system_prompt_path
self.topic_user_prompt_path = topic_user_prompt_path
self.content_system_prompt_path = content_system_prompt_path
self.content_judger_system_prompt_path = content_judger_system_prompt_path # 添加成员变量
self.prompts_dir = prompts_dir # 保留兼容旧配置
self.prompts_config = prompts_config or [] # 新的配置方式
self.resource_dir_config = resource_dir_config or []
self.topic_gen_num = topic_gen_num
self.topic_gen_date = topic_gen_date
# 缓存加载的文件内容
self._style_cache = {}
self._demand_cache = {}
self._refer_cache = {}
self._system_prompt_cache = {} # 新增:系统提示词缓存
self._user_prompt_cache = {} # 新增:用户提示词缓存
self._dateline_cache = None # 新增:日期线缓存
self._sample_rate = 0.1 # 提高随机抽样率
# 初始化时预加载配置的文件
self._preload_prompt_files()
def _preload_prompt_files(self):
"""预加载配置中的提示文件到缓存"""
# 预加载系统提示词和用户提示词文件
if self.topic_system_prompt_path and os.path.exists(self.topic_system_prompt_path):
content = ResourceLoader.load_file_content(self.topic_system_prompt_path)
if content:
self._system_prompt_cache["topic"] = content
logging.info(f"预加载系统提示词: {self.topic_system_prompt_path}")
if self.topic_user_prompt_path and os.path.exists(self.topic_user_prompt_path):
content = ResourceLoader.load_file_content(self.topic_user_prompt_path)
if content:
self._user_prompt_cache["topic"] = content
logging.info(f"预加载用户提示词: {self.topic_user_prompt_path}")
if self.content_system_prompt_path and os.path.exists(self.content_system_prompt_path):
content = ResourceLoader.load_file_content(self.content_system_prompt_path)
if content:
self._system_prompt_cache["content"] = content
logging.info(f"预加载内容系统提示词: {self.content_system_prompt_path}")
# 预加载内容审核系统提示词
if self.content_judger_system_prompt_path and os.path.exists(self.content_judger_system_prompt_path):
content = ResourceLoader.load_file_content(self.content_judger_system_prompt_path)
if content:
self._system_prompt_cache["judger_system_prompt"] = content
logging.info(f"预加载内容审核系统提示词: {self.content_judger_system_prompt_path}")
# 预加载日期线文件
if self.topic_user_prompt_path:
user_prompt_dir = os.path.dirname(self.topic_user_prompt_path)
dateline_path = os.path.join(user_prompt_dir, "2025各月节日宣传节点时间表.md")
if os.path.exists(dateline_path):
self._dateline_cache = ResourceLoader.load_file_content(dateline_path)
logging.info(f"预加载日期线文件: {dateline_path}")
# 加载prompts_config配置的文件
if not self.prompts_config:
return
for config_item in self.prompts_config:
prompt_type = config_item.get("type", "").lower()
file_paths = config_item.get("file_path", [])
if prompt_type == "style":
for path in file_paths:
if os.path.exists(path):
filename = os.path.basename(path)
content = ResourceLoader.load_file_content(path)
if content:
self._style_cache[filename] = content
name_without_ext = os.path.splitext(filename)[0]
self._style_cache[name_without_ext] = content # 同时缓存不带扩展名的版本
elif prompt_type == "demand":
for path in file_paths:
# print(path)
if os.path.exists(path):
filename = os.path.basename(path)
# print(filename)
content = ResourceLoader.load_file_content(path)
# print(content)
if content:
self._demand_cache[filename] = content
name_without_ext = os.path.splitext(filename)[0]
self._demand_cache[name_without_ext] = content # 同时缓存不带扩展名的版本
elif prompt_type == "refer":
for path in file_paths:
# print(path)
if os.path.exists(path):
filename = os.path.basename(path)
# print(filename)
# 检测是否为JSON文件
if filename.lower().endswith('.json'):
# 直接加载和解析JSON文件
try:
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
# 移除可能的BOM头
if content.startswith('\ufeff'):
content = content[1:]
# 解析JSON
json_data = json.loads(content)
# 存储解析后的对象
self._refer_cache[filename] = json_data
logging.info(f"预加载并解析JSON参考文件: {filename}")
except Exception as e:
logging.error(f"预加载JSON文件{filename}失败: {str(e)}")
# 失败时尝试以普通文本加载
content = ResourceLoader.load_file_content(path)
if content:
self._refer_cache[filename] = content
else:
# 非JSON文件使用普通加载方式
content = ResourceLoader.load_all_refer_files(path, 1)
if content:
self._refer_cache[filename] = content
logging.info(f"预加载普通参考文件: {filename}")
def find_directory_fuzzy_match(self, name, directory=None, files=None):
"""
对文件名进行模糊匹配查找最匹配目标名称的文件
Args:
name: 目标名称
directory: 目录路径如果提供则从目录中读取文件列表
files: 文件名列表如果提供则直接使用
Returns:
tuple: (最佳匹配文件名, 匹配分数) 如果没有匹配则返回 (None, 0)
"""
logging.info(f"尝试对文件名进行模糊匹配: {name}")
try:
# 准备文件列表
all_files = []
if files:
all_files = files
elif directory and os.path.isdir(directory):
all_files = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
if not all_files:
logging.warning(f"没有可用于匹配的文件")
return None, 0
logging.info(f"找到 {len(all_files)} 个文件可用于模糊匹配")
# 从名称中提取关键词
# 通过常见分隔符分割(+、空格、_、-等)
parts = re.split(r'[+\s_\-]', name)
keywords = []
for part in parts:
# 只保留长度大于1的有意义关键词
if len(part) > 1:
keywords.append(part)
# 尝试匹配更短的语义单元例如中文的2-3个字的词语
for i in range(len(name) - 1):
keyword = name[i:i+2] # 提取2个字符
if len(keyword) == 2 and all('\u4e00' <= c <= '\u9fff' for c in keyword):
keywords.append(keyword)
logging.info(f"用于文件模糊匹配的关键词: {keywords}")
# 对每个文件进行评分
file_scores = {}
for filename in all_files:
score = 0
file_lower = filename.lower()
# 精确匹配,去掉扩展名比较
name_without_ext = os.path.splitext(name)[0].lower()
file_without_ext = os.path.splitext(filename)[0].lower()
if name_without_ext == file_without_ext:
# 精确匹配给高分
score += 10
else:
# 为每个匹配的关键词增加分数
for keyword in keywords:
if keyword.lower() in file_lower:
score += 1
# 如果得分大于0至少有匹配记录该文件
if score > 0:
file_scores[filename] = score
# 选择得分最高的文件
if file_scores:
best_match = max(file_scores.items(), key=lambda x: x[1])
found_file = best_match[0]
score = best_match[1]
logging.info(f"模糊匹配成功!匹配文件: {found_file},匹配分数: {score}")
return found_file, score
else:
logging.warning(f"模糊匹配未找到任何包含关键词的文件")
return None, 0
except Exception as e:
logging.exception(f"文件模糊匹配过程中出错: {e}")
return None, 0
def _get_style_content(self, style_name):
"""获取Style文件内容优先从缓存获取如果不存在则尝试从目录加载"""
# 首先检查缓存
if style_name in self._style_cache:
return self._style_cache[style_name]
# 确保有扩展名
if not style_name.lower().endswith('.txt'):
style_file = f"{style_name}.txt"
else:
style_file = style_name
style_name = os.path.splitext(style_name)[0] # 移除扩展名
# 尝试模糊匹配缓存中的文件名
cache_files = list(self._style_cache.keys())
matched_key, score = self.find_directory_fuzzy_match(style_name, files=cache_files)
if matched_key and score > 0:
return self._style_cache[matched_key]
# 如果没有在缓存中找到模糊匹配尝试从prompts_dir加载
if self.prompts_dir:
style_dir = os.path.join(self.prompts_dir, "Style")
style_path = os.path.join(style_dir, style_file)
# 精确路径匹配
if os.path.exists(style_path):
content = ResourceLoader.load_file_content(style_path)
if content:
# 保存到缓存
self._style_cache[style_name] = content
self._style_cache[style_file] = content
return content
# 如果精确匹配失败,尝试目录中的模糊匹配
if os.path.isdir(style_dir):
matched_file, score = self.find_directory_fuzzy_match(style_name, directory=style_dir)
if matched_file and score > 0:
matched_path = os.path.join(style_dir, matched_file)
content = ResourceLoader.load_file_content(matched_path)
if content:
# 保存到缓存
self._style_cache[style_name] = content
self._style_cache[matched_file] = content
file_without_ext = os.path.splitext(matched_file)[0]
self._style_cache[file_without_ext] = content # 同时缓存不带扩展名的版本
return content
logging.warning(f"未能找到Style文件: '{style_name}',尝试过以下位置: 缓存, {self.prompts_dir}/Style/")
return None
def _get_demand_content(self, demand_name):
"""获取Demand文件内容优先从缓存获取如果不存在则尝试从目录加载"""
# 首先检查缓存
if demand_name in self._demand_cache:
return self._demand_cache[demand_name]
# 确保有扩展名
if not demand_name.lower().endswith('.txt'):
demand_file = f"{demand_name}.txt"
else:
demand_file = demand_name
demand_name = os.path.splitext(demand_name)[0] # 移除扩展名
# 尝试模糊匹配缓存中的文件名
cache_files = list(self._demand_cache.keys())
matched_key, score = self.find_directory_fuzzy_match(demand_name, files=cache_files)
if matched_key and score > 0:
return self._demand_cache[matched_key]
# 如果没有在缓存中找到模糊匹配尝试从prompts_dir加载向后兼容
if self.prompts_dir:
demand_dir = os.path.join(self.prompts_dir, "Demand")
demand_path = os.path.join(demand_dir, demand_file)
# 精确路径匹配
if os.path.exists(demand_path):
content = ResourceLoader.load_file_content(demand_path)
if content:
# 保存到缓存
self._demand_cache[demand_name] = content
self._demand_cache[demand_file] = content
return content
# 如果精确匹配失败,尝试目录中的模糊匹配
if os.path.isdir(demand_dir):
matched_file, score = self.find_directory_fuzzy_match(demand_name, directory=demand_dir)
if matched_file and score > 0:
matched_path = os.path.join(demand_dir, matched_file)
content = ResourceLoader.load_file_content(matched_path)
if content:
# 保存到缓存
self._demand_cache[demand_name] = content
self._demand_cache[matched_file] = content
file_without_ext = os.path.splitext(matched_file)[0]
self._demand_cache[file_without_ext] = content # 同时缓存不带扩展名的版本
return content
# 如果所有尝试都失败
logging.warning(f"未能找到Demand文件: '{demand_name}',尝试过以下位置: 缓存, {self.prompts_dir}/Demand/")
return None
def _get_refer_content(self, refer_dir):
"""从Refer目录加载JSON和文本参考文件
Args:
refer_dir: Refer目录路径
Returns:
dict: 文件名到内容的映射
"""
results = {}
if not os.path.isdir(refer_dir):
return results
# 列出所有文件
refer_files = [f for f in os.listdir(refer_dir) if os.path.isfile(os.path.join(refer_dir, f))]
for refer_file in refer_files:
refer_path = os.path.join(refer_dir, refer_file)
content = None
# 对JSON文件特殊处理
if refer_file.lower().endswith('.json'):
try:
with open(refer_path, 'r', encoding='utf-8') as f:
content = f.read()
# 移除可能的BOM头
if content.startswith('\ufeff'):
content = content[1:]
# 验证是否为有效的JSON
try:
json.loads(content) # 只验证,不存储结果
logging.info(f"成功验证JSON文件: {refer_file}")
except json.JSONDecodeError as e:
logging.warning(f"文件{refer_file}内容不是有效的JSON: {str(e)}")
except Exception as e:
logging.error(f"读取JSON文件{refer_file}时出错: {str(e)}")
content = None
else:
# 使用ResourceLoader加载非JSON文件
content = ResourceLoader.load_file_content(refer_path)
if content:
results[refer_file] = content
return results
def _load_and_parse_json(self, file_path):
"""安全地加载和解析JSON文件
Args:
file_path: JSON文件路径
Returns:
dict or None: 解析后的JSON对象解析失败则返回None
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 移除可能的BOM头
if content.startswith('\ufeff'):
content = content[1:]
return json.loads(content)
except Exception as e:
logging.error(f"加载和解析JSON文件{file_path}时出错: {str(e)}")
return None
def _get_all_refer_contents(self, random_sample=True):
"""获取所有Refer文件内容可选择随机抽样文件内容
Args:
random_sample: 是否对文件内容进行随机抽样默认为True
Returns:
str: 组合后的refer内容
"""
import json
# 初始化结果字符串
refer_content_all = ""
# 如果缓存为空,尝试加载文件
if not self._refer_cache and self.prompts_dir:
refer_dir = os.path.join(self.prompts_dir, "Refer")
if os.path.isdir(refer_dir):
refer_files = [f for f in os.listdir(refer_dir) if os.path.isfile(os.path.join(refer_dir, f))]
for refer_file in refer_files:
file_path = os.path.join(refer_dir, refer_file)
# 对JSON文件特殊处理
if refer_file.lower().endswith('.json'):
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 移除可能的BOM头
if content.startswith('\ufeff'):
content = content[1:]
# 解析并存储JSON对象
json_data = json.loads(content)
self._refer_cache[refer_file] = json_data
logging.info(f"加载并解析JSON参考文件: {refer_file}")
except Exception as e:
logging.error(f"加载JSON文件{refer_file}失败: {str(e)}")
# 失败时尝试以普通文本加载
content = ResourceLoader.load_file_content(file_path)
if content:
self._refer_cache[refer_file] = content
else:
# 非JSON文件使用普通加载方式
content = ResourceLoader.load_file_content(file_path)
if content:
self._refer_cache[refer_file] = content
if not self._refer_cache:
logging.warning("没有找到任何Refer文件")
return refer_content_all
logging.info(f"找到{len(self._refer_cache)}个Refer文件")
# 处理所有文件
for filename, content in self._refer_cache.items():
# 添加文件头部信息
refer_content_all += f"--- Refer File: {filename} ---\n"
# 检查内容类型
if isinstance(content, dict) and 'title' in content and 'examples' in content:
# 已解析的JSON对象
title = content.get("title", "未命名参考资料")
description = content.get("description", "")
examples = content.get("examples", [])
refer_content_all += f"标题: {title}\n"
refer_content_all += f"描述: {description}\n\n"
if examples:
# 处理examples数组
if random_sample and len(examples) > 10:
# 对examples进行随机抽样
sample_size = max(10, int(len(examples) * self._sample_rate))
sampled_examples = random.sample(examples, sample_size)
logging.info(f"从文件{filename}的JSON中随机抽样了{sample_size}/{len(examples)}个示例")
refer_content_all += "示例:\n"
for idx, example in enumerate(sampled_examples, 1):
content_text = example.get("content", "")
refer_content_all += f"{idx}. {content_text}\n"
else:
# 不进行抽样或examples数量较少使用全部
refer_content_all += "示例:\n"
for idx, example in enumerate(examples, 1):
content_text = example.get("content", "")
refer_content_all += f"{idx}. {content_text}\n"
elif isinstance(content, str):
# 文本内容
if random_sample:
lines = content.split('\n')
if len(lines) > 10: # 只对较长的内容进行抽样
sample_size = max(10, int(len(lines) * self._sample_rate))
sampled_lines = random.sample(lines, sample_size)
# 保持原有顺序
sampled_lines.sort(key=lambda line: lines.index(line))
sampled_content = '\n'.join(sampled_lines)
logging.info(f"从文件{filename}中随机抽样了{sample_size}/{len(lines)}行内容")
refer_content_all += f"{sampled_content}\n"
else:
# 内容较短,不进行抽样
refer_content_all += f"{content}\n"
else:
# 不进行抽样
refer_content_all += f"{content}\n"
else:
# 内容是其他类型
refer_content_all += f"未知内容类型: {type(content)}\n"
# 添加文件之间的分隔
refer_content_all += "\n"
return refer_content_all
def get_topic_prompts(self):
"""Constructs the system and user prompts for topic generation."""
logging.info("Constructing prompts for topic generation...")
try:
# --- System Prompt ---
system_prompt = self._system_prompt_cache.get("topic")
if not system_prompt:
if not self.topic_system_prompt_path:
logging.error("Topic system prompt path not provided during PromptManager initialization.")
return None, None
system_prompt = ResourceLoader.load_file_content(self.topic_system_prompt_path)
if system_prompt:
self._system_prompt_cache["topic"] = system_prompt
else:
logging.error(f"Failed to load topic system prompt from '{self.topic_system_prompt_path}'.")
return None, None
# --- User Prompt ---
base_user_prompt = self._user_prompt_cache.get("topic")
if not base_user_prompt:
if not self.topic_user_prompt_path:
logging.error("Topic user prompt path not provided during PromptManager initialization.")
return None, None
base_user_prompt = ResourceLoader.load_file_content(self.topic_user_prompt_path)
if base_user_prompt:
self._user_prompt_cache["topic"] = base_user_prompt
else:
logging.error(f"Failed to load base topic user prompt from '{self.topic_user_prompt_path}'.")
return None, None
# --- Build the dynamic part of the user prompt ---
user_prompt_dynamic = "你拥有的创作资料如下:\n"
# 添加prompts_config配置的文件信息
if self.prompts_config:
for config_item in self.prompts_config:
prompt_type = config_item.get("type", "").lower()
file_paths = config_item.get("file_path", [])
if file_paths:
user_prompt_dynamic += f"{prompt_type.capitalize()}文件列表:\n"
for path in file_paths:
filename = os.path.basename(path)
user_prompt_dynamic += f"- {filename}\n"
user_prompt_dynamic += "\n"
# 兼容旧配置Add genPrompts directory structure
elif self.prompts_dir and os.path.isdir(self.prompts_dir):
try:
gen_prompts_list = os.listdir(self.prompts_dir)
for gen_prompt_folder in gen_prompts_list:
folder_path = os.path.join(self.prompts_dir, gen_prompt_folder)
if os.path.isdir(folder_path):
try:
# List files, filter out subdirs if needed
gen_prompts_files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
user_prompt_dynamic += f"{gen_prompt_folder}\n{gen_prompts_files}\n"
except OSError as e:
logging.warning(f"Could not list directory {folder_path}: {e}")
except OSError as e:
logging.warning(f"Could not list base prompts directory {self.prompts_dir}: {e}")
else:
logging.warning(f"Neither prompts_config nor prompts_dir provided or valid.")
# Add resource directory contents
for dir_info in self.resource_dir_config:
source_type = dir_info.get("type", "UnknownType")
source_file_paths = dir_info.get("file_path", [])
for file_path in source_file_paths:
# Use ResourceLoader's static method
file_content = ResourceLoader.load_file_content(file_path)
if file_content:
user_prompt_dynamic += f"{source_type}信息:\n{os.path.basename(file_path)}\n{file_content}\n\n"
else:
logging.warning(f"Could not load resource file {file_path}")
# Add dateline information (optional)
if self._dateline_cache:
user_prompt_dynamic += f"\n{self._dateline_cache}"
else:
user_prompt_dir = os.path.dirname(self.topic_user_prompt_path)
dateline_path = os.path.join(user_prompt_dir, "2025各月节日宣传节点时间表.md") # Consider making this configurable
if os.path.exists(dateline_path):
dateline_content = ResourceLoader.load_file_content(dateline_path)
if dateline_content:
self._dateline_cache = dateline_content
user_prompt_dynamic += f"\n{dateline_content}"
# Combine dynamic part, base template, and final parameters
user_prompt = user_prompt_dynamic + base_user_prompt
user_prompt += f"\n选题数量:{self.topic_gen_num}\n选题日期:{self.topic_gen_date}\n"
logging.info(f"Topic prompts constructed. System: {len(system_prompt)} chars, User: {len(user_prompt)} chars.")
return system_prompt, user_prompt
except Exception as e:
logging.exception("Error constructing topic prompts:")
return None, None
def get_content_prompts(self, topic_item):
"""Constructs the system and user prompts for content generation."""
logging.info("Constructing prompts for content generation...")
try:
# --- System Prompt ---
system_prompt = self._system_prompt_cache.get("content")
if not system_prompt:
if not self.content_system_prompt_path:
logging.error("Content system prompt path not provided.")
return None, None
system_prompt = ResourceLoader.load_file_content(self.content_system_prompt_path)
if system_prompt:
self._system_prompt_cache["content"] = system_prompt
else:
logging.error(f"Failed to load content system prompt from '{self.content_system_prompt_path}'.")
return None, None
# --- User Prompt ---
style = ""
demand = ""
refers = ""
object_info = ""
# Extract style from topic
if "style" in topic_item and topic_item["style"]:
style_name = topic_item["style"]
style_content = self._get_style_content(style_name)
if style_content:
style = f"Style: {style_name}\n{style_content}\n\n"
else:
logging.warning(f"Style content for '{style_name}' not found.")
# Extract demand from topic
if "target_audience" in topic_item and topic_item["target_audience"]:
demand_name = topic_item["target_audience"]
demand_content = self._get_demand_content(demand_name)
if demand_content:
demand = f"Demand: {demand_name}\n{demand_content}\n\n"
else:
logging.warning(f"Demand content for '{demand_name}' not found.")
# Add refer contents - 现在使用随机抽样
refers_content = self._get_all_refer_contents(random_sample=True)
if refers_content:
refers = f"Reference:\n{refers_content}\n\n"
# Get object information
object_name = topic_item.get("object", "")
if object_name:
# 优化遍历resource_dir_config查找对象描述
found_object_info = False
# 1. 搜集所有可能的资源文件
all_description_files = []
for dir_info in self.resource_dir_config:
if dir_info.get("type") in ["Object", "Description"]:
all_description_files.extend(dir_info.get("file_path", []))
# 2. 尝试精确匹配
for file_path in all_description_files:
if object_name in os.path.basename(file_path):
info = ResourceLoader.load_file_content(file_path)
if info:
object_info = f"Object: {object_name}\n{info}\n\n"
logging.info(f"找到对象'{object_name}'的精确匹配资源文件: {file_path}")
found_object_info = True
break
# 3. 如果精确匹配失败,尝试模糊匹配
if not found_object_info and all_description_files:
logging.info(f"尝试模糊匹配对象'{object_name}'的资源文件")
# 提取所有文件名
file_names = [os.path.basename(f) for f in all_description_files]
# 模糊匹配
matched_filename, score = self.find_directory_fuzzy_match(object_name, files=file_names)
if matched_filename and score > 0:
# 找到匹配的完整路径
for file_path in all_description_files:
if os.path.basename(file_path) == matched_filename:
info = ResourceLoader.load_file_content(file_path)
if info:
object_info = f"Object: {object_name}\n{info}\n\n"
logging.info(f"模糊匹配找到对象'{object_name}'的资源文件: {file_path},匹配分数: {score}")
found_object_info = True
break
if not found_object_info:
logging.warning(f"未找到对象'{object_name}'的任何匹配资源文件")
# Get product information if any
product_name = topic_item.get("product", "")
if product_name:
product_info = ""
# 优化遍历resource_dir_config查找产品描述
found_product_info = False
# 搜集所有可能的产品资源文件
all_product_files = []
for dir_info in self.resource_dir_config:
if dir_info.get("type") == "Product":
all_product_files.extend(dir_info.get("file_path", []))
# 尝试精确匹配
for file_path in all_product_files:
if product_name in os.path.basename(file_path):
info = ResourceLoader.load_file_content(file_path)
if info:
product_info = f"Product: {product_name}\n{info}\n\n"
logging.info(f"找到产品'{product_name}'的精确匹配资源文件: {file_path}")
found_product_info = True
break
# 如果精确匹配失败,尝试模糊匹配
if not found_product_info and all_product_files:
logging.info(f"尝试模糊匹配产品'{product_name}'的资源文件")
# 提取所有文件名
file_names = [os.path.basename(f) for f in all_product_files]
# 模糊匹配
matched_filename, score = self.find_directory_fuzzy_match(product_name, files=file_names)
if matched_filename and score > 0:
# 找到匹配的完整路径
for file_path in all_product_files:
if os.path.basename(file_path) == matched_filename:
info = ResourceLoader.load_file_content(file_path)
if info:
product_info = f"Product: {product_name}\n{info}\n\n"
logging.info(f"模糊匹配找到产品'{product_name}'的资源文件: {file_path},匹配分数: {score}")
found_product_info = True
break
if not found_product_info:
logging.warning(f"未找到产品'{product_name}'的任何匹配资源文件")
# 添加产品信息到对象信息中
if product_info:
object_info += product_info
# Construct final user prompt
user_prompt = f"""请为我创建一个旅游文案。
{style}{demand}{refers}{object_info}
请考虑以上所有信息创作一篇{topic_item.get('target_audience','')}文旅内容"""
return system_prompt, user_prompt
except Exception as e:
traceback.print_exc()
logging.error(f"Error creating content prompts: {str(e)}")
return None, None

509
base_line/tweet_prompt.txt Normal file
View File

@ -0,0 +1,509 @@
请为我创建一个旅游文案。
Style: 攻略风文案提示词
你是景区小红书爆款文案策划,你将根据要求创作爆款文案,根据以下规则一步步执行:
一、标题创作
步骤:先速览景区资料内容,找到与用户需求适配的景点亮点,然后通读用户给的爆款标题素材,挑选一个在用户画像+节点节日+标题吸睛三个方面最契合的进行改写,创作出全新的爆款标题。在此期间以下几点需要你遵循:
1.必加1个emoji标题字数18字以内
2.有网感,爆破式标题,吸睛,结合所在地
3.标题结合<标题参考格式>进行仿写
二、正文创作
注意:正文以<我><你>这种有人味的人称代词视角创作输出,不要出现<宝子们><姐妹们>这些很假的称呼
1. 通过了解产品和当前节点需求,如情人节要约会、儿童节要带孩子玩、临近周末会考虑周边游、传统节日偏好民俗活动等以此类推,直击用户痛点。
2. 正文围绕所选定的风格提示词进行创作,有场景感,让人感觉是你的真是体验
3. 开头引入部分可以参考我给你的<正文开头引入段落参考>文档,结合所给景区/民宿资料进行改写创作
4. 分段+分点论述,开头引入+优势亮点(根据所给材料选择角度:交通优势、住宿优势、周边资源、必打卡项、路线推荐等等...+你认为的最大优势
5. 如果用户给出范文案例,请参考范文案例的写作风格和内容
6. 一定要有一种和目标用户对话的感觉,语言平实靠谱,娓娓道来
三、TAG标签创作
每一篇笔记需要携带13个TAG标签以下举例的加号+)这个符号不要体现,仅代表内容连接的意思
1.前三个TAG标签的内容围绕<#产品所在地区+旅游><#产品所在省份+旅游><#产品所在地区+周边游><#产品周边一线城市+周边游>
2.接着三个TAG标签的内容围绕<#景区/酒店名称><#景区/酒店名称+攻略><#景区/酒店名称+推荐>
3.再来三个TAG标签的内容围绕<#对应节日+出游><#对应节日+反向旅行><#对应人群+好去处>
4.最后三个TAG标签的内容围绕<#周末去哪儿玩><#小众旅游地><##产品所在地区+周末好去处>
注意以上TAG直接按要求直出内容不需要在直出的时候进行分类
输出注意点:
1.按要求输出标签+正文+TAG标签
2.正文直白清楚围绕产品,不要出现比喻和联想
3.千万不要虚构景区信息没有的设施设备和活动信息,如果材料中没有指明,不要提及价格信息;如果没有明确的产品价格或者优惠活动信息,不要提及价格和优惠
4.输出内容中,要符合社会道德规范和法律规范
5.TAG标签按要求顺序在正文最后直出进行罗列
6.所有的价格如XXX元改成XXXr即用<r>代替<元>200元变成200r
7.关于所有节假日补差价都不要在文中提及
具体输出格式参照以下格式:
{
"content": "(正文内容)",
"tag": "TAG内容, 如#地点 #节日 #需求人群旅游 #周末去哪儿玩)",
"title": "(标题内容)"
}
Demand: 亲子向文旅需求
亲子向用户画像:
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月
Reference:
--- Refer File: 标题参考格式.json ---
标题: 标题参考格式
描述: 你创作标题的时候可以直接按照以下参考标题,结合景区/酒店资料进行仿写。
示例:
1. 呼伦贝尔亲子游!娃哭着不愿回来的攻略~
2. 打工人首选🙋广州出发周末两日游
3. 2-3月广东赏花季🌸6个绝美赏花地必收藏
4. 去了8次扬州❗这条高质量遛娃路线藏不住了
5. 东莞小洱海🌊来吹吹蓝色的风
6. 🦒佛山长鹿遛娃一日游,节日氛围已经拉满
7. 南京2天1晚亲子游旅游攻略! 不踩雷!
8. 周末去哪玩|电玩🎮天河客运站
9. 广州欢乐模拟经营密室🧑🏻🍳2人可开‼
10. 广东周边亲子游适合6岁以下娃踏春好去处❗
11. 假期打开新方式🥳一天玩转古镇+春日赏花🌸
12. 广东亲子游ᵕ̈过年还要再来赏樱花🌸
13. 天呐🤣这里已经颠覆了我的想象‼️实在太好玩
--- Refer File: 正文范文参考.json ---
标题: 正文范文参考
描述: 你需要用这些内容作为参考,创作正文笔记。你可以整合多段获取材料,创作出更丰富的内容。参考写法和视角
示例:
1. 夏天和假期准备来啦!
我又发现一个新的玩水避暑胜地!
这是全新的一个网红夏威夷泳道啦~
-
🏨阳江 DS 温泉酒店
超级推荐的是夏威夷亲水别墅
无限次免费游玩的夏威夷网红泳道
踏出房门就能跳进泳池夏天太爽了!
-
💰599🉐套餐👇
【住】酒店别墅星空双床房1间1晚;
【吃】价值168元两大一小自助早餐
【泡】赠送2池阳台私家泡池温泉水
【泡】无限次爽泡高热偏硅酸公共温泉
【玩】畅玩网红超长泳道、水上乐园
【玩】打卡儿童狮子星空中儿童乐园
【赏】天香大草坪、花森林婚庆广场
【赠】公区温泉泳道免费水果、茶饮、清食等
【赠】管家服务,一次洗漱用品,酒店免费停车
---------------
💥怎么订:一定要先关.🐖后下方留言咨询999稍后会一一回复哈
2. 来广州外下午三点多的时候外面太热了 实在不知道去哪 突然决定去长隆水上乐园
临时起意 即刻出发!
目的地:地铁三号线汉溪长隆
1⃣什么时候去票价多少
建议工作日去人少些
下午4点后进场 美团69r/人 !!!很便宜!
我觉得这个时间点太棒了
太阳已经不是很毒辣 也不是太热了 不用担心晒伤
2⃣没带泳衣怎么办园内买泳衣太贵怎么办
不用担心!万能的小🍠
我直接搜“广州长隆水上乐园泳衣”几个关键字
果然出现了很多5分钟内送泳衣的帖子
随便找了一家直接➕了联系方式
在地铁上就看商家发的图
选好了泳衣款式➕一个男士泳裤➕防水袋
三个东西一共才105
力省几百!!
而且店家还给我把东西送到地铁口,还顺便把我人免费送到水上乐园园区门口
当然也可以直接把泳衣送到园区门口 (切记先不要进园!进园他就送不了了!问了好几家都是这样说!只能在没进园之前送到地铁口或者园区门口!)
没带泳衣的姐妹千万不要在园区里买!!
3⃣进去玩什么
我们是五点多才进园
但是到八点半已经玩完了六七个项目
所有大项目几乎都玩了
介绍一下时间紧任务重的 我们的玩法
离心滑道—摇滚巨轮-合家大滑板-竞速赛道-巨兽碗-巨洪峡
-上面全部玩完之后也才七点五十多
然后八点电音节开场觉得没什么意思
就去刷之前排队人太多没玩过的项目
当然是直接冲大喇叭!!
这个时候的大喇叭直接没人!上去就玩没排队!!!
迅速玩完 又去玩了一些小项目比如 垂直极限这种 都没人排队了 直接随便玩
点击左下角查看更多,
听说现在力度超级给力呢!
3. ☀️暑假来了,同学们想去广州长隆水上乐园玩吗?如果想去,那首先要做攻略,下面是阿毅替你们写的攻略。
·
🧳出发前要做哪些准备?
1、订票🎫
门票分为全天票09:30-22:30、夜场票17:00-22:30
可在欢乐长隆gzh、某携、某哪、某评上订
2、做交通攻略🚘
自驾导航长隆水上乐园景区有停车场收20
打车用顺风车比较划算,目的地填长隆水上乐园
坐地铁3号线到汉溪长隆站从E口出来步行1.2公里可到景区门口
3、收拾行李🪪
身份证、毛巾、手机防水袋、游泳衣、防晒霜、拖鞋、手机、塑料袋、零食、水
·
🗺️进园步骤?
1、刷身份证进园
2、先去租个柜子储存东西小柜40大柜60另外还要给20押金等还了手环会返回
3、去更衣室换衣服
4、可以去玩了
·
🤹‍♀️必看演出:
1、长隆水上电音派对🪩
地点:造浪池舞台
时间20:00-21:00
2、玩水激乐大巡游💃
地点:园区
时间17:00-17:35
3、水战派对🔫
地点:沙滩区舞台
时间17:00-17:35
4、泡泡派对🫧
地点:亲子市集
时间16:00-16:20
·
🎢必玩项目:
1、超级大喇叭📢
坐在四叶草浮圈里,从六层高的平台出发,滑进一个巨大的喇叭中,在快速滑行中尽情欢叫
2、摇滚巨轮🪇
竖立自转滑道整体外型犹如一座水上摩天轮高达26米每个转角都十分圆滑
3、巨洪峡🌊
在狭长的峡谷中只有滔天巨浪与你相伴,在跌宕起伏中挑战山洪暴发的澎湃,领略被抛向浪尖的快感
4、超级巨兽碗👾
首先在紫水晶通道俯冲穿越,来到一个巨碗之中。当你稍觉平稳,突然出现一个黑洞将你吸入,让你措手不及
5、垂直极限↕
有3条滑道黄色滑道滑下时速度能达到近40公里蓝色滑道为旋转滑道体验离心冲击橙色滑道垂直度高体验水花四溅的感觉
6、巨蟒滑道🐍
从弯形盘旋滑道划出后会进入长达6米的全封闭滑道集合了扭转螺旋和振动的感觉
7、大滑板滑道🛹
6人一组坐在浮圈上从高处快速滑到底部然后继续向上滑行到呈 90度的滑道上然后再从垂直的滑道上滑落到平缓的地段
·
❤️温馨提示:
建议9点半刚开园就去玩那会人很多如果下午去人会很多玩一个项目要等一两个小时。
4. 🏨广州森林海温泉度假酒店
室内水乐园水温30度以上温泉泡池全面
😭千万别错过这个森林海美食专属套餐
-
现在只要1399就可以享受3大3小的的Chao值套餐哦~真的是太划算啦!
🔥3大3小自助早餐+3大3小自助晚餐+2天无限次嬉水乐园十亲子房
现在还有限时活动每个成人住客可以免费带两个1.5以下的小朋友一起畅玩嬉水乐园💦
-
限时特价房型是森林海爆款房型
🔹海洋亲子/嬉水海洋亲子/星空亲子
🎈自助早餐
🎈私家露台温泉泡池
🎈无限次威尼斯泳道
🎈嬉水乐园【天阳湖区、神秘海岛、古海森林、森林温泉、抓鱼摸虾]
-
怎么订❓先关㊗然后下方留言【999】看到会一一回复
5. 深圳海边酒店已经住过洲际和万豪
这两家酒店海滩都很漂亮
没想到这次住桔钓沙莱华直接来了个玻璃海震撼
庆幸遇上大晴天☀️
才能看到这美到窒息的景色
唯一缺点是有围网也不能下海游泳🏊…
🏨酒店很大不过偏老旧,公区维护一般。房间正海景,躺在床上就能看到海滩,阳台有大浴缸可以一边看海一边泡澡🛀,度假感拉满!
🎡入住每间房都有赠送一次免费小火车体验+儿童乐园100个币+帆船/皮划艇项目,赠送的游戏币可以在儿童乐园电玩区使用,室内乐园还有攀爬区、蹦床区、波波球… 足够小朋友玩很久了
🍽️酒店自助早餐一般,不太好吃。晚餐在🍠找的友友农庄,家常菜,偏贵,味道还行吧,窑鸡不好吃,感觉附近就没什么好吃的东西。
💗莱华酒店虽然没有很完美,比起洲际不够豪华,比起万豪不够漂亮,但是它游玩项目足够多,性价比也很高,而且为了这片玻璃海景色我一定会再刷的!!🏖️
6. 位于大鹏新区的桔钓沙,这里沙子超白超好干净还很细腻 脚踩下去都软乎乎的 !
不出深圳,也能实现海滨度假🏖
.
推门见大海🌊下楼即沙滩🏖
【深圳桔钓沙莱华度假酒店🏨】
作为深圳为数不多拥有独.立沙滩的度假酒店私密性ji高
而且酒店附近的可玩项目好多
杨梅坑踩单车🚴♀、看美人鱼拍摄地🧜♀、徒步七娘山⛰、观国家地质博物馆等都离酒店不到2公里路程妥妥的遛娃好去处
·
🏊‍♂还有超大的私人泳池,沙滩就在泳池旁边,随时可以下海畅游!
☀️沙滩边还有各种娱乐设施,小朋友可以挖沙踏浪,大人则可以玩滑皮划艇
👧儿童乐园也是一大亮点!室内室外都有各种儿童游乐设施
室内攀岩、积木乐园、、4D动感影院、音乐厅和电子游戏等
爸妈们可以省心省力地遛娃,让孩子们尽情享受快乐的时光!🎉
·
🏨酒店拥有246间客房间间都带有宽阔独.立景观阳台
有多种房型可供选择,房间设施齐全,有超大浴缸.
·
【双人下午茶套票】
💰1299
🔹豪华园景房一间一晚
🔹双人自助早餐
🔹双人下午茶1套(大堂吧/池畔吧 二选一)
-
想要购买的宝子们,戳我即可获取,酒店住宿套餐
7. 发现了一家神仙度假酒店!
不在三亚,也不在东南亚
而是在深圳⛱️
这片果冻海真的美到心颤💕
-
🌟桔钓沙莱华度假酒店
✅封神级沙滩体验🏝️
有深圳一绝的牛奶沙+玻璃海,光脚踩在沙滩上,就像踩在热奶粉上一样巨舒服。海水呈渐变色,如果冻一般
-
✅东南亚秘境园林🌴
15万㎡东南亚热带花园秒穿普吉岛
-
✅100%阳台景观房⛱️
所有房型都有超大阳台,山海园林尽享眼底
-
✅亲子友好天花板🛝
拥有1600㎡的儿童奇妙世界含欢乐海洋泡泡池、室内攀岩、积木乐园、知识海洋、DIY手工坊等玩到不想走
-
✅海上玩乐不停⛵️
游艇出海、摩托艇、香蕉船、帆船、皮划艇等超多水上活动
-
🌟现在这个「小马代」错峰大促人均最低只要400+,即享:
海景豪华房1间1晚
✔️双人自助早餐
双人小帆船或皮划艇套餐2选1
✔️客房迷你吧冰箱饮品
✔️奇妙世界项目:鲸鱼大滑梯、潜艇通道、海马秋千、波波球池、儿童攀岩、儿童蹦床、手工制作等
✔️沙滩排球、沙滩足球
赠送双人精美旅拍1份
✔️每间房赠送一张免费小火车票
-
*使用有效期至2025年4月29日
-
今年第一场海边度假,准备好开始了吗?
在牛奶沙上踩出脚印👣
看着波光粼粼的海发呆🏝️
等一场橘子汽水味的日落🌅
这才是度假啊~
-
8.
伙计们,都给我冲‼️
北洛秘境是真的有点东西
🚗出行方式:自驾(酒店本身没有停车场)
🏨住宿day1北洛秘境度假酒店1栋
1属于外部干干净净内部缝缝补补……
230层天际泳池🉑去大海带来的疗愈……
3包含早晚餐附赠的悬崖泳池、山体公园、临海栈道套票整体还是相当划算❤
🏨住宿day2铂悦度假公寓
属于民宿类,大落地窗➕独立阳台,三面看海😍,晚上看沙滩的烟花都达不到我住的高度
🌟打卡推荐🌟
1⃣首先需要知道山体公园、悬崖泳池、悬崖咖啡、临海栈道都是在一块只能单次进出‼一条道走到底
2⃣山体公园8:00-18:30栈道式爬山建议早上11:00前或者下午15点后完成打卡它主要包括
1🌈彩虹步道比较脏顶上损坏待维修在入口打卡即可✅
2不走回头路🚏路标鸟巢🪹分布在道路两边可以拍到海和马路上的“阳江”logo
3不上班行不行🚏路标露天小房子主打一个社畜的精神支柱房子目前有点像半竣工状态
4破破烂烂的粉色车尾巴我合理怀疑它是报废状态就跟我的精神状态蛮契合
5悬崖咖啡馆😐比较有个性但饮品看个人口味拍照的话我想说手残党上午大概率是会废片
6粉色观景台圆形⭕设计可以上二楼看海但太晒了我没去……可以在外围把建筑做成背景进去拍的话一整个粉掉……
7网红天际秋千两根大铁链子⛓💥垂下来的是景点摄影付费装置平时秋千的板板都是收起来的😳
3⃣悬崖泳池8:30-18:30这里需要二次检票游泳需要泳装泳帽拍照的话检票进去即可可以付费寄存20rmb真的没必要😐……
❤️很出片是一回事,很晒是另一回事❤️
4⃣海边栈道去‼都给我去这里‼真的好好看😻果冻海岩石远处的船脚下的石头路一整个波妞附体
‼️建议
真的要做好防晒😳,感觉已经黑一圈了
穿衣是自由的,但姐妹儿,穿鞋一定是自律的
防蚊虫是必须的,拍照是要大胆尝试的
旅游要自己开心的😃
9. ▶️清远新世界酒店
还没退房就已经想要二刷的酒店
真的太适合躺平溜娃了!
🌟酒店环境服务
很干净整洁的酒店,整体装修风格很舒服,房间也很大。入住登记后有工作人员帮忙拿行李带路,管家服务也很周到,大晚上有需求也回复得很及时,遇到的工作人员都很热情友好~
入住的时候尽量让安排到3座玩的地方基本都在这边我们住2座每次都要经过主楼走很远。
🎪儿童乐园(嗨玩)
儿童乐园区域看似不大,但有很多东西玩,三个不同类型的室内游戏房,户外区域有滑梯、蹦床、沙池、小水池、蔬菜园,还有平衡车可以骑,适合不同年龄段的小朋友。我们工作日去几乎包场,完全解放双手,阿乐自己玩得很开心。
⛳️娱乐项目(活馆)
在3座1楼射箭、迷你高尔夫、地上桌球在一个房间进去室内球类在对面房。有些项目需要提前预约但我们去的时候没什么人都是去了就能玩。迷你高尔夫和地上桌球很适合小朋友尤其地上桌球真的太好玩了我觉得每个宝宝都会喜欢的哈哈哈。
温泉900-2230
🏊🏻‍♂️泳池 900-2000
室外游泳池不是恒温的,我们去的时候天气有点凉,就没去了。带阿乐去泡了温泉,室内有一个泡池一个汗蒸房,室外区域不大,但环境很舒服,泡池不大,有五六个可以泡,建议人少的时候去,体验会好很多。
💡以上的酒店活动项目入住前退房后也是可以玩的这点很人性化毕竟2天1夜真的安排不过来哈哈
🍱餐饮
酒店的自助早餐在主楼的1楼环境很好我们在室外用餐很惬意早餐种类挺多出品我觉得比很多酒店早餐都要好吃很多哈哈
正餐我们没有在酒店吃附近有很多农庄当晚我们选了【来来农庄】开车5min就到老板很热情菜品也很不错第二天退房后去了【三禾稻里】10min路程我们到店太晚了只有西餐价格偏贵但味道挺好的。不得不说这里真的好美啊如果不带娃的话我可能会选这个民宿哈哈很适合躺平发呆
🌟总结:是一家非常不错的亲子酒店,很适合亲子家庭入住,必二刷!
10. 一开始看朋友去了觉得不错还心想清远酒店应该很便宜吧一查居然要1300+当时觉得有点奢侈,但是去了之后觉得好好玩,好舒服,还想再来!
👨‍👩‍👦 酒店设施——超多亲子项目
有三个主题的儿童屋,小厨房、钢琴、玩具等,户外可以玩沙子、滑滑梯,还有小农场可以浇花,有大型桌球台、小型高尔夫场,还可以射箭。我儿子玩得很开心,我自己也觉得很有趣!
♨️ 酒店房间&温泉——
房间挺新,可以加装围栏,房间有个独立的小池子,我们晚上把娃带睡之后,在露台吃宵夜泡温泉,带娃人幸福时刻。公域的温泉环境也不错,不过池子有点小
🥣 酒店自助早餐——很丰盛 选择很多
餐厅环境很好,吃早餐的时候看窗外简直心旷神怡。选择也很多,光是那碟绿色肠粉就拿了好几盘!早餐吃得很满足,也挺好吃
🌴 酒店户外环境——很大 绿化很好
超级适合我崽骑车!吃完早餐之后,在酒店户外绕了一圈,有种像逛公园的感觉,光合作用拉满,真的很舒服!
⭐️⭐️⭐️⭐️⭐️
已经是两个月前去的了还是忍不住mark篇帖子记录。当时选了工作日来的人少体验好真的觉得很适合两岁起的娃我们应该还会再去玩滴
Object: 北洛秘境盛季酒店
{
"主体名称": "北洛秘境盛季酒店",
"产品有效期": "2025年5月13日-6月30日",
“产品售卖期”:"2025年5月13日-6月30日",
"地址": "闸坡镇北洛大道199号碧桂园北洛明珠花园3栋1层",
"交通指南": "【交通建议】导航酒店地址自驾前往或高铁到站后导航前往,广州到阳江站1.5H到了高铁站可以搭顺风车1H到酒店人均30。人多也可以直接打顺风车。",
"使用规则、加价说明、预约规则、退改政策、优惠内容": "【使用规则】
① 本产品购买成功未预约支持随时退,预约成功不改不退
② 本产品具体节假日加收最终以国家法定节假日为准,如漏补或少补会有客服联系补回,介意勿拍
③ 本产品为特价秒杀,如突遇特殊情况无法正常安排入住,不做任何赔偿,介意者勿拍(订单特殊情况无法安排会有客服联系改期或退款)
【加收说明】
①5月6月平日不加收周六+50元端午节5月31日-6月1日加收150元
②指定海景房在基础加收上叠加30元/间
③升级2房1厅海景房5月加收80元周六加收2306月6月加收130元周六加收280元端午节5月31日-6月1日加收330元
【预约规则】
库存有限,先约先得,加收均为线上加收
【注意事项】
①床型大小双床1.2M*2
②入住时间15点后    退房时间12点前
③本套餐不包含门票,周边游玩景点需自费
④注:仅供参考,实际以现场为准
",
"产品最突出优势":"①低于某程、某团价格
②可以升级海景2房1厅
③节假日:端午节可用",
"产品亮点":"周边游玩:
酒店位置超优越,能畅游马尾岛,那里的海水清澈见底,沙滩细腻柔软 ,漫步海边,吹着海风,真的好惬意。还能去北洛东边免费沙滩,玩沙踏浪、晒太阳。
游玩路线:山体公园→悬崖无边泳池→海边栈道→椰林沙滩
①靠近海陵岛北洛湾沙滩有12处风景各异的天然海滩异彩纷呈地点缀私人海滩、悬崖栈道、天际泳池、山体公园的山顶秋千、粉红观景台超多景点打卡
-悬崖临海栈道
沿着礁石悬崖蜿蜒的橙色步道,俯拍海浪拍岸的瞬间,仿佛走在世界尽头!(穿防滑鞋!)
-珍珠白沙滩
沙子细软得像面粉,一点都不硌脚。椰林树影,水清,还有海边夜市~
-山体公园
登顶10分钟就能get俯瞰全岛的视角记得和“海边秋千”合影
-彩虹灯塔
上山的路有一段彩虹楼梯,楼梯的尽头是彩色灯塔,在这里可以俯瞰整个白沙滩。
-临海栈道
一边是悬崖,一边是大海,临海栈道依山而建,海浪不断拍打着脚下的礁石,浪花飞溅,场面太壮观了
②酒店楼下小区免费使用儿童游客设施
③自费打卡北洛湾沙滩泳池、秘境悬崖泳池悬崖泳池自东向西方向全线视野可以将整片蔚蓝的大海一览无遗泳池建于马尾岛山顶的悬崖边108米高空俯瞰大海Tiffany蓝的池水
美食TIP
▪️ 闸坡渔港的海鲜大排档!皮皮虾/海胆炒饭
▪️ 本地糖水铺的“椰子冻”清爽解暑
④游玩建议2天1晚酒店自身处处是网红拍照打卡点周边景点也多值得去逛逛比如天麓山景区、月光殿堂、风车山等
"
}
Product: 北洛秘境盛季酒店-海景或山景双床房
{
"产品名称":"北洛秘境盛季酒店-海景或山景双床房",
"产品门市价":199,
"产品实际售价":99,
"套票详情": " 【阳江·北洛秘境盛季酒店】可升级海景2房99元抢全新海景/山景双床房(随机安排),毗邻悬崖泳池,打卡“东方夏威夷”海陵岛!感受天空海阔融为一体的奇妙体验~ ",
"产品售卖期":"2025年5月13日-6月30日",
"产品库存":100,
"产品最突出优势":"①低于某程、某团价格
②可以升级海景2房1厅
③节假日:端午节可用",
"产品亮点":"周边游玩:
酒店位置超优越,能畅游马尾岛,那里的海水清澈见底,沙滩细腻柔软 ,漫步海边,吹着海风,真的好惬意。还能去北洛东边免费沙滩,玩沙踏浪、晒太阳。
游玩路线:山体公园→悬崖无边泳池→海边栈道→椰林沙滩
①靠近海陵岛北洛湾沙滩有12处风景各异的天然海滩异彩纷呈地点缀私人海滩、悬崖栈道、天际泳池、山体公园的山顶秋千、粉红观景台超多景点打卡
-悬崖临海栈道
沿着礁石悬崖蜿蜒的橙色步道,俯拍海浪拍岸的瞬间,仿佛走在世界尽头!(穿防滑鞋!)
-珍珠白沙滩
沙子细软得像面粉,一点都不硌脚。椰林树影,水清,还有海边夜市~
-山体公园
登顶10分钟就能get俯瞰全岛的视角记得和“海边秋千”合影
-彩虹灯塔
上山的路有一段彩虹楼梯,楼梯的尽头是彩色灯塔,在这里可以俯瞰整个白沙滩。
-临海栈道
一边是悬崖,一边是大海,临海栈道依山而建,海浪不断拍打着脚下的礁石,浪花飞溅,场面太壮观了
②酒店楼下小区免费使用儿童游客设施
③自费打卡北洛湾沙滩泳池、秘境悬崖泳池悬崖泳池自东向西方向全线视野可以将整片蔚蓝的大海一览无遗泳池建于马尾岛山顶的悬崖边108米高空俯瞰大海Tiffany蓝的池水
美食TIP
▪️ 闸坡渔港的海鲜大排档!皮皮虾/海胆炒饭
▪️ 本地糖水铺的“椰子冻”清爽解暑
④游玩建议2天1晚酒店自身处处是网红拍照打卡点周边景点也多值得去逛逛比如天麓山景区、月光殿堂、风车山等
",
"产品须知": "【使用规则】
① 本产品购买成功未预约支持随时退,预约成功不改不退
② 本产品具体节假日加收最终以国家法定节假日为准,如漏补或少补会有客服联系补回,介意勿拍
③ 本产品为特价秒杀,如突遇特殊情况无法正常安排入住,不做任何赔偿,介意者勿拍(订单特殊情况无法安排会有客服联系改期或退款)
【加收说明】
①5月6月平日不加收周六+50元端午节5月31日-6月1日加收150元
②指定海景房在基础加收上叠加30元/间
③升级2房1厅海景房5月加收80元周六加收2306月6月加收130元周六加收280元端午节5月31日-6月1日加收330元
【预约规则】
库存有限,先约先得,加收均为线上加收
【注意事项】
①床型大小双床1.2M*2
②入住时间15点后    退房时间12点前
③本套餐不包含门票,周边游玩景点需自费
④注:仅供参考,实际以现场为准
"
}
请考虑以上所有信息,创作一篇亲子向文旅需求文旅内容。

View File

@ -0,0 +1,420 @@
{
"title": "标题参考格式",
"description": "你创作标题的时候可以直接按照以下参考标题,结合景区/酒店资料进行仿写。",
"examples": [
{
"content": "没有人敢说惠州希尔顿酒店的事吗?"
},
{
"content": "暑假遛娃来青甘亲子游听听这5条良心建议"
},
{
"content": "湖州🦒亲子圣地5天4晚行程分享"
},
{
"content": "🐼成都也太适合亲子游了吧3天2晚攻略来喽"
},
{
"content": "带娃去大理后,我后悔了! 😭后悔没早来!"
},
{
"content": "🦒长隆二天一夜❸园🏨亲子游经验🐘"
},
{
"content": "2024旅行结算超🐔大型亲子乐园平躺攻略"
},
{
"content": "国庆广州亲子游❄️冰雪游乐园室内遛娃好去处"
},
{
"content": "去了8次扬州❗这条高质量遛娃路线藏不住了"
},
{
"content": "追鲸🐳桂西南带娃已回🔥跟风最成功的一次🔥"
},
{
"content": "济南春季亲子游‼3天2晚看这一篇就够了💯"
},
{
"content": "广州亲子游🚗1.5h到温泉乐园宫崎骏树屋遛娃"
},
{
"content": "三刷乌村‼2天1晚保姆级吃住玩攻略‼"
},
{
"content": "低幼友好京郊2h直达懒人遛娃地解放爸妈"
},
{
"content": "清远新世界酒店|遛娃真的太爽太舒服了♨️"
},
{
"content": "2月18陵水穷游已回说点掏心窝子的话"
},
{
"content": "带娃避寒首选,广西崇左+北海+涠洲岛攻略!"
},
{
"content": "嘉兴三天两夜亲子游杭州🚗1.5h达!"
},
{
"content": "江浙沪亲子酒店排名,还有漏的吗?"
},
{
"content": "崇左带娃已回...说点难听的实话"
},
{
"content": "江浙沪遛娃❤️徐州欧乐堡两天一晚亲子游"
},
{
"content": "成都带娃已回,亲子游真实体验(不删)"
},
{
"content": "周末游 | 江门市赤坎古镇,是谁说不推荐的?"
},
{
"content": "亲子游天花板❗️小众不挤还省钱✨抄作业啦"
},
{
"content": "三亚家庭游5天四晚这样安排人均1800"
},
{
"content": "两天一夜!宁波亲子游攻略"
},
{
"content": "西双版纳 5 天 4 晚亲子游get\"不费妈\"攻略"
},
{
"content": "📍广州顺德佛山丨省心路线🗺️避雷指南"
},
{
"content": "迄今为止我最喜欢的亲子游目的地出现了!"
},
{
"content": "初次海南亲子游海口、陵水7日总结"
},
{
"content": "四天三晚广州亲子游攻略"
},
{
"content": "北京牛马不请假25年带爸妈遛娃这么玩1"
},
{
"content": "📍清远!带娃去生态度假村做了一回野孩子"
},
{
"content": "拜托🙏去万宁就死磕威斯汀1088/三天"
},
{
"content": "5个亲子游地带娃超轻松1⃣岁宝宝亲测"
},
{
"content": "广州动物园亲子游 | 千万不要临近中午才去"
},
{
"content": "在成都带宝贝在6000平游乐园里寻找宝藏吧"
},
{
"content": "千岛湖很chill的一次亲子游"
},
{
"content": "今年五一我只带娃去一个地方:徽州!🤩"
},
{
"content": "成都周末去哪儿I古镇星空亲子游"
},
{
"content": "解锁亲子游的「瘦shen副本」攻略】✨"
},
{
"content": "宁波奈尔宝/遛娃乐园天花板"
},
{
"content": "敦煌带娃已回,亲子游真实体验(不删)"
},
{
"content": "来广州过个暖冬‼4天3晚保姆级亲子游攻略"
},
{
"content": "天呐🤣这里已经颠覆了我的想象‼️实在太好玩"
},
{
"content": "划重点3月适合带幼儿园孩子去的5个地方"
},
{
"content": "N次扬州三天两夜亲子游🧸看这里就够全了🎈"
},
{
"content": "我的年度最爱 甩娃亲子游天花板!"
},
{
"content": "大连亲子游本地人推荐的4天3夜✨"
},
{
"content": "说点实话:贵州🆚版纳 已回 慎重 附路线"
},
{
"content": "🦒佛山长鹿遛娃一日游,节日氛围已经拉满"
},
{
"content": "2025年带娃必去的🔟个旅游地点"
},
{
"content": "冷空气杀到😎我反手带娃跳进38℃温泉海💦"
},
{
"content": "🌈迄今为止,我最喜欢的亲子游城市出现了"
},
{
"content": "广东亲子游ᵕ̈过年还要再来赏樱花🌸"
},
{
"content": "高铁遛娃|济南太适合亲子游了!📋超实用攻略"
},
{
"content": "亲子游的松弛感拿捏住了"
},
{
"content": "江浙沪徒步上海1-2h可达的亲子路线合集🍃"
},
{
"content": "我宣布‼这个亲子酒店399我们玩回本了"
},
{
"content": "云南弥勒☀️和版纳相比,更推荐带娃去这里"
},
{
"content": "听劝❗广东旅游保姆级攻略,吃住玩全都有!"
},
{
"content": "昆明抚仙湖4天3晚🌈亲子游分享"
},
{
"content": "全新💯香港海洋公园亲子游不走回头路攻略"
},
{
"content": "抄作业‼周五出发3日2夜成都亲子游不请假"
},
{
"content": "🌿 华南植物园亲子游 赏花科普一站搞定❗"
},
{
"content": "广东周边亲子游适合6岁以下娃踏春好去处❗"
},
{
"content": "广西遛娃plog🌈 | 带娃实现嗦粉自由🍜"
},
{
"content": "呼伦贝尔亲子游!娃哭着不愿回来的攻略~"
},
{
"content": "南京2天1晚亲子游旅游攻略! 不踩雷!"
},
{
"content": "东莞小洱海🌊来吹吹蓝色的风"
},
{
"content": "广州周边游|高铁0-4小时可到达城市汇总"
},
{
"content": "广州番禺该去哪里❓看这篇就够了,快收藏吧"
},
{
"content": "3.4实拍 松山湖这里已进入盛花期~"
},
{
"content": "重庆本地人周末都去哪儿玩 ‼️"
},
{
"content": "🔥终于公布了。广州39个蕞值得去的地方"
},
{
"content": "广州🆓新展|好治愈!! 掉进奇妙春日幻境✨⛲️"
},
{
"content": "国庆假期🚄从广州出发4小时内可达城市汇总"
},
{
"content": "春日限定🌼在广州拍到了✨故事里的小黄花~"
},
{
"content": "石家庄出发,周末两天出游攻略来啦!"
},
{
"content": "2-3月广东赏花季🌸6个绝美赏花地必收藏"
},
{
"content": "终于整理好了😭武汉春日必去的①⑥个徒步路线"
},
{
"content": "大胆周末48H❗天津➡徐州两日游"
},
{
"content": "🆘除了七星岩,端州大学生周末还能去哪?"
},
{
"content": "春游踏青| 十条适合周末出行玩乐新手路线"
},
{
"content": "📘Infj|男大学生 ✨周末无课plog"
},
{
"content": "广州一日往返9个春天上春山周末小众散心处"
},
{
"content": "深圳📍误入莫奈瀑布花海🌷春日浪漫💕免费!"
},
{
"content": "广州🔥线下活动 3.4-3.15丨不准你不知道"
},
{
"content": "广州1h直达‼被严重低估了的百年骑楼小镇‼"
},
{
"content": "📍3.7 广州已回。。😭我都崩溃了"
},
{
"content": "周末遛娃别再发朋友圈了!都发给我!"
},
{
"content": "杭州车程1H千亩白梅 本周花开成海!"
},
{
"content": "广州大学生周末去哪玩8/100"
},
{
"content": "打工人首选🙋广州出发周末两日游"
},
{
"content": "广东版小洪崖洞❗️❓东莞西溪古村小众秘境"
},
{
"content": "南昌3.4实况🌸艾溪湖粉色樱花盛开‼️快来拍"
},
{
"content": "地铁直达🚇深圳12个游玩景点攻略合集🔥"
},
{
"content": "💥超齐全攻略|广东省内春季两天一夜团建📝"
},
{
"content": "被问爆的春日踏青地图广东9城花海巡礼指南"
},
{
"content": "接下来的武汉,免费带娃见世面方式是这样的!"
},
{
"content": "广州➡️阳朔⛰️ 两天一夜 人均700💰详细攻"
},
{
"content": "广州地铁直达的绝美看海地|打工人周末充电"
},
{
"content": "苏州可以逛一整天的市场推荐!全部🚇直达"
},
{
"content": "【广州周边游】周末逃离城市计划🏖️桂山"
},
{
"content": "清远!广东\"小桂林\"和男友三刷了太chill了"
},
{
"content": "广州citywalk | 长洲岛真的好治愈啊"
},
{
"content": "女生最期待你✨带她去的约会项目🥰"
},
{
"content": "女生最期待男生带她去的约会项目!"
},
{
"content": "【广州情侣私奔地图】人少景美悄悄话圣地"
},
{
"content": "绝了❗佛山新开的!山间弯道超车太炫酷了😭"
},
{
"content": "周末去哪玩|电玩🎮天河客运站"
},
{
"content": "假期打开新方式🥳一天玩转古镇+春日赏花🌸"
},
{
"content": "📍佛山南海|\"洱海\"般的日落免费小众露营地"
},
{
"content": "广州欢乐模拟经营密室🧑🏻🍳2人可开‼"
},
{
"content": "3月学生党穷游海边城市TOP9省钱又好玩"
},
{
"content": "3月来广州邂逅这5片免费花海吧🥰"
},
{
"content": "周末在广州可以做的30件事"
},
{
"content": "广州出发1h | 直达广东\"小九寨\"🍃"
},
{
"content": "广州!居然还有人没去过这个天花板公园?"
},
{
"content": "春天总要来一趟广州吧!满城鲜花真的好美🌸"
},
{
"content": "广东周边2-4天游10个高铁🚄直达宝藏地"
},
{
"content": "在广州99%的人都不知道的小众徒步公园❗️"
},
{
"content": "3-4月旅行淡季这5个地方美到窒息人少到哭"
},
{
"content": "广州周末去哪玩❓陈家祠深度游攻略‼️"
},
{
"content": "五一丨在这个奇幻乐园魔法降临啦🎩 🔮"
},
{
"content": "520心动企划💗看不见你的笑我怎么睡得着"
},
{
"content": "昆明遛娃丨遛娃YYDS🤙🏻古滇新春大庙会🐯"
},
{
"content": "中秋攻略丨最低255.8 全家6口畅玩欢乐世界"
},
{
"content": "昆明赏樱踏青|看过一场樱吹雪,才不负春光"
},
{
"content": "醉云南的火把节🔥当然要配最云南的乐队🪘🎸"
},
{
"content": "仅99💰|带你体验不一样的云南异域狂欢!"
},
{
"content": "五百里音乐节🎸|双人票👫酒店套餐来咯"
},
{
"content": "六一去哪儿?欢乐世界全攻略来了‼️建议收藏"
},
{
"content": "六一带娃玩七彩云南🦄这份身高攻略请收好!"
},
{
"content": "10月27日超级NPC互动拍照时间地点来咯💓"
},
{
"content": "任何人不知道温泉节特惠♨我都会伤心的OK⁉"
},
{
"content": "昆明万圣|整蛊狂欢全攻略🎃建议收藏背诵"
},
{
"content": "古滇温泉山庄粉丝福利👒限时138♨速来泡汤"
}
]
}

View File

@ -0,0 +1,35 @@
{ "title": "正文范文参考",
"description": "你需要用这些内容作为参考,创作正文笔记。你可以整合多段获取材料,创作出更丰富的内容。参考写法和视角",
"examples":[
{
"content": "夏天和假期准备来啦!\n我又发现一个新的玩水避暑胜地\n这是全新的一个网红夏威夷泳道啦\n-\n🏨阳江 DS 温泉酒店\n超级推荐的是夏威夷亲水别墅\n无限次免费游玩的夏威夷网红泳道\n踏出房门就能跳进泳池夏天太爽了\n-\n💰599🉐套餐👇\n【住】酒店别墅星空双床房1间1晚;\n【吃】价值168元两大一小自助早餐\n【泡】赠送2池阳台私家泡池温泉水\n【泡】无限次爽泡高热偏硅酸公共温泉\n【玩】畅玩网红超长泳道、水上乐园\n【玩】打卡儿童狮子星空中儿童乐园\n【赏】天香大草坪、花森林婚庆广场\n【赠】公区温泉泳道免费水果、茶饮、清食等\n【赠】管家服务一次洗漱用品酒店免费停车\n---------------\n💥怎么订一定要先关.🐖后下方留言咨询999稍后会一一回复哈"
},
{
"content": "来广州外下午三点多的时候外面太热了 实在不知道去哪 突然决定去长隆水上乐园\n临时起意 即刻出发!\n目的地地铁三号线汉溪长隆\n\n1⃣什么时候去票价多少\n建议工作日去人少些\n下午4点后进场 美团69r/人 !!!很便宜!\n我觉得这个时间点太棒了\n太阳已经不是很毒辣 也不是太热了 不用担心晒伤\n\n2⃣没带泳衣怎么办园内买泳衣太贵怎么办\n不用担心万能的小🍠\n我直接搜“广州长隆水上乐园泳衣”几个关键字\n果然出现了很多5分钟内送泳衣的帖子\n随便找了一家直接了联系方式\n在地铁上就看商家发的图\n选好了泳衣款式一个男士泳裤防水袋\n三个东西一共才105\n力省几百\n而且店家还给我把东西送到地铁口还顺便把我人免费送到水上乐园园区门口\n\n当然也可以直接把泳衣送到园区门口 (切记先不要进园!进园他就送不了了!问了好几家都是这样说!只能在没进园之前送到地铁口或者园区门口!)\n\n没带泳衣的姐妹千万不要在园区里买\n\n3⃣进去玩什么\n我们是五点多才进园\n但是到八点半已经玩完了六七个项目\n所有大项目几乎都玩了\n介绍一下时间紧任务重的 我们的玩法 \n离心滑道—摇滚巨轮-合家大滑板-竞速赛道-巨兽碗-巨洪峡\n-上面全部玩完之后也才七点五十多\n然后八点电音节开场觉得没什么意思\n就去刷之前排队人太多没玩过的项目\n当然是直接冲大喇叭\n这个时候的大喇叭直接没人上去就玩没排队\n迅速玩完 又去玩了一些小项目比如 垂直极限这种 都没人排队了 直接随便玩\n\n点击左下角查看更多\n听说现在力度超级给力呢"
},
{
"content": "☀️暑假来了,同学们想去广州长隆水上乐园玩吗?如果想去,那首先要做攻略,下面是阿毅替你们写的攻略。\n·\n🧳出发前要做哪些准备\n1、订票🎫\n门票分为全天票09:30-22:30、夜场票17:00-22:30\n可在欢乐长隆gzh、某携、某哪、某评上订\n2、做交通攻略🚘\n自驾导航长隆水上乐园景区有停车场收20\n打车用顺风车比较划算目的地填长隆水上乐园\n坐地铁3号线到汉溪长隆站从E口出来步行1.2公里可到景区门口\n3、收拾行李🪪\n身份证、毛巾、手机防水袋、游泳衣、防晒霜、拖鞋、手机、塑料袋、零食、水\n·\n🗺进园步骤\n1、刷身份证进园\n2、先去租个柜子储存东西小柜40大柜60另外还要给20押金等还了手环会返回\n3、去更衣室换衣服\n4、可以去玩了\n·\n🤹必看演出\n1、长隆水上电音派对🪩\n地点造浪池舞台\n时间20:00-21:00\n2、玩水激乐大巡游💃\n地点园区\n时间17:00-17:35\n3、水战派对🔫\n地点沙滩区舞台\n时间17:00-17:35\n4、泡泡派对🫧\n地点亲子市集\n时间16:00-16:20\n·\n🎢必玩项目\n1、超级大喇叭📢\n坐在四叶草浮圈里从六层高的平台出发滑进一个巨大的喇叭中在快速滑行中尽情欢叫\n2、摇滚巨轮🪇\n竖立自转滑道整体外型犹如一座水上摩天轮高达26米每个转角都十分圆滑\n3、巨洪峡🌊\n在狭长的峡谷中只有滔天巨浪与你相伴在跌宕起伏中挑战山洪暴发的澎湃领略被抛向浪尖的快感\n4、超级巨兽碗👾\n首先在紫水晶通道俯冲穿越来到一个巨碗之中。当你稍觉平稳突然出现一个黑洞将你吸入让你措手不及\n5、垂直极限↕\n有3条滑道黄色滑道滑下时速度能达到近40公里蓝色滑道为旋转滑道体验离心冲击橙色滑道垂直度高体验水花四溅的感觉\n6、巨蟒滑道🐍\n从弯形盘旋滑道划出后会进入长达6米的全封闭滑道集合了扭转螺旋和振动的感觉\n7、大滑板滑道🛹\n6人一组坐在浮圈上从高处快速滑到底部然后继续向上滑行到呈 90度的滑道上然后再从垂直的滑道上滑落到平缓的地段\n·\n❤温馨提示\n建议9点半刚开园就去玩那会人很多如果下午去人会很多玩一个项目要等一两个小时。"
},
{
"content": "🏨广州森林海温泉度假酒店\n♨室内水乐园水温30度以上温泉泡池全面\n😭千万别错过这个森林海美食专属套餐\n-\n现在只要1399就可以享受3大3小的的Chao值套餐哦~真的是太划算啦!\n🔥3大3小自助早餐+3大3小自助晚餐+2天无限次嬉水乐园十亲子房\n现在还有限时活动每个成人住客可以免费带两个1.5以下的小朋友一起畅玩嬉水乐园💦\n-\n限时特价房型是森林海爆款房型\n🔹海洋亲子/嬉水海洋亲子/星空亲子\n🎈自助早餐\n🎈私家露台温泉泡池\n🎈无限次威尼斯泳道\n🎈嬉水乐园【天阳湖区、神秘海岛、古海森林、森林温泉、抓鱼摸虾]\n-\n怎么订❓先关㊗然后下方留言【999】看到会一一回复"
},
{
"content": "深圳海边酒店已经住过洲际和万豪\n这两家酒店海滩都很漂亮\n没想到这次住桔钓沙莱华直接来了个玻璃海震撼\n庆幸遇上大晴天☀\n才能看到这美到窒息的景色\n唯一缺点是有围网也不能下海游泳🏊…\n\n🏨酒店很大不过偏老旧公区维护一般。房间正海景躺在床上就能看到海滩阳台有大浴缸可以一边看海一边泡澡🛀度假感拉满\n\n🎡入住每间房都有赠送一次免费小火车体验+儿童乐园100个币+帆船/皮划艇项目,赠送的游戏币可以在儿童乐园电玩区使用,室内乐园还有攀爬区、蹦床区、波波球… 足够小朋友玩很久了\n\n🍽酒店自助早餐一般不太好吃。晚餐在🍠找的友友农庄家常菜偏贵味道还行吧窑鸡不好吃感觉附近就没什么好吃的东西。\n\n💗莱华酒店虽然没有很完美比起洲际不够豪华比起万豪不够漂亮但是它游玩项目足够多性价比也很高而且为了这片玻璃海景色我一定会再刷的🏖"
},
{
"content": "位于大鹏新区的桔钓沙,这里沙子超白超好干净还很细腻 脚踩下去都软乎乎的 !\n不出深圳也能实现海滨度假🏖\n.\n推门见大海🌊下楼即沙滩🏖\n【深圳桔钓沙莱华度假酒店🏨】\n作为深圳为数不多拥有独.立沙滩的度假酒店私密性ji高\n而且酒店附近的可玩项目好多\n杨梅坑踩单车🚴♀、看美人鱼拍摄地🧜♀、徒步七娘山⛰、观国家地质博物馆等都离酒店不到2公里路程妥妥的遛娃好去处\n·\n🏊♂还有超大的私人泳池沙滩就在泳池旁边随时可以下海畅游\n☀沙滩边还有各种娱乐设施小朋友可以挖沙踏浪大人则可以玩滑皮划艇\n👧儿童乐园也是一大亮点室内室外都有各种儿童游乐设施\n室内攀岩、积木乐园、、4D动感影院、音乐厅和电子游戏等\n爸妈们可以省心省力地遛娃让孩子们尽情享受快乐的时光🎉\n·\n🏨酒店拥有246间客房间间都带有宽阔独.立景观阳台\n有多种房型可供选择房间设施齐全有超大浴缸.\n·\n【双人下午茶套票】\n💰1299\n🔹豪华园景房一间一晚\n🔹双人自助早餐\n🔹双人下午茶1套(大堂吧/池畔吧 二选一)\n-\n想要购买的宝子们戳我即可获取酒店住宿套餐"
},
{
"content": "发现了一家神仙度假酒店!\n不在三亚也不在东南亚\n而是在深圳⛱\n这片果冻海真的美到心颤💕\n-\n🌟桔钓沙莱华度假酒店\n✅封神级沙滩体验🏝\n有深圳一绝的牛奶沙+玻璃海,光脚踩在沙滩上,就像踩在热奶粉上一样巨舒服。海水呈渐变色,如果冻一般\n-\n✅东南亚秘境园林🌴\n15万㎡东南亚热带花园秒穿普吉岛\n-\n✅100%阳台景观房⛱️\n所有房型都有超大阳台山海园林尽享眼底\n-\n✅亲子友好天花板🛝\n拥有1600㎡的儿童奇妙世界含欢乐海洋泡泡池、室内攀岩、积木乐园、知识海洋、DIY手工坊等玩到不想走\n-\n✅海上玩乐不停⛵\n游艇出海、摩托艇、香蕉船、帆船、皮划艇等超多水上活动\n-\n🌟现在这个「小马代」错峰大促人均最低只要400+,即享:\n✔海景豪华房1间1晚\n✔双人自助早餐\n✔双人小帆船或皮划艇套餐2选1\n✔客房迷你吧冰箱饮品\n✔奇妙世界项目鲸鱼大滑梯、潜艇通道、海马秋千、波波球池、儿童攀岩、儿童蹦床、手工制作等\n✔沙滩排球、沙滩足球\n✔赠送双人精美旅拍1份\n✔每间房赠送一张免费小火车票\n-\n*使用有效期至2025年4月29日\n-\n今年第一场海边度假准备好开始了吗\n在牛奶沙上踩出脚印👣\n看着波光粼粼的海发呆🏝\n等一场橘子汽水味的日落🌅\n这才是度假啊\n-"
},
{
"content": "\n伙计们都给我冲‼\n北洛秘境是真的有点东西\n🚗出行方式自驾酒店本身没有停车场\n\n🏨住宿day1北洛秘境度假酒店1栋\n1属于外部干干净净内部缝缝补补……\n230层天际泳池🉑去大海带来的疗愈……\n3包含早晚餐附赠的悬崖泳池、山体公园、临海栈道套票整体还是相当划算❤\n🏨住宿day2铂悦度假公寓\n\n属于民宿类大落地窗独立阳台三面看海😍晚上看沙滩的烟花都达不到我住的高度\n\n🌟打卡推荐🌟\n\n1⃣首先需要知道山体公园、悬崖泳池、悬崖咖啡、临海栈道都是在一块只能单次进出‼一条道走到底\n\n2⃣山体公园8:00-18:30栈道式爬山建议早上11:00前或者下午15点后完成打卡它主要包括\n\n1🌈彩虹步道比较脏顶上损坏待维修在入口打卡即可✅\n\n2不走回头路🚏路标鸟巢🪹分布在道路两边可以拍到海和马路上的“阳江”logo\n\n3不上班行不行🚏路标露天小房子主打一个社畜的精神支柱房子目前有点像半竣工状态\n\n4破破烂烂的粉色车尾巴我合理怀疑它是报废状态就跟我的精神状态蛮契合\n\n5悬崖咖啡馆😐比较有个性但饮品看个人口味拍照的话我想说手残党上午大概率是会废片\n\n6粉色观景台圆形⭕设计可以上二楼看海但太晒了我没去……可以在外围把建筑做成背景进去拍的话一整个粉掉……\n\n7网红天际秋千两根大铁链子⛓💥垂下来的是景点摄影付费装置平时秋千的板板都是收起来的😳\n\n3⃣悬崖泳池8:30-18:30这里需要二次检票游泳需要泳装泳帽拍照的话检票进去即可可以付费寄存20rmb真的没必要😐……\n\n❤很出片是一回事很晒是另一回事❤\n\n4⃣海边栈道去‼都给我去这里‼真的好好看😻果冻海岩石远处的船脚下的石头路一整个波妞附体\n\n‼建议\n真的要做好防晒😳感觉已经黑一圈了\n穿衣是自由的但姐妹儿穿鞋一定是自律的\n防蚊虫是必须的拍照是要大胆尝试的\n旅游要自己开心的😃"
},
{
"content": "▶️清远新世界酒店\n还没退房就已经想要二刷的酒店\n真的太适合躺平溜娃了\n\n🌟酒店环境服务\n很干净整洁的酒店整体装修风格很舒服房间也很大。入住登记后有工作人员帮忙拿行李带路管家服务也很周到大晚上有需求也回复得很及时遇到的工作人员都很热情友好\n入住的时候尽量让安排到3座玩的地方基本都在这边我们住2座每次都要经过主楼走很远。\n\n🎪儿童乐园嗨玩\n儿童乐园区域看似不大但有很多东西玩三个不同类型的室内游戏房户外区域有滑梯、蹦床、沙池、小水池、蔬菜园还有平衡车可以骑适合不同年龄段的小朋友。我们工作日去几乎包场完全解放双手阿乐自己玩得很开心。\n\n⛳娱乐项目活馆\n在3座1楼射箭、迷你高尔夫、地上桌球在一个房间进去室内球类在对面房。有些项目需要提前预约但我们去的时候没什么人都是去了就能玩。迷你高尔夫和地上桌球很适合小朋友尤其地上桌球真的太好玩了我觉得每个宝宝都会喜欢的哈哈哈。\n\n♨温泉900-2230\n🏊🏻泳池 900-2000\n室外游泳池不是恒温的我们去的时候天气有点凉就没去了。带阿乐去泡了温泉室内有一个泡池一个汗蒸房室外区域不大但环境很舒服泡池不大有五六个可以泡建议人少的时候去体验会好很多。\n\n💡以上的酒店活动项目入住前退房后也是可以玩的这点很人性化毕竟2天1夜真的安排不过来哈哈\n\n🍱餐饮\n酒店的自助早餐在主楼的1楼环境很好我们在室外用餐很惬意早餐种类挺多出品我觉得比很多酒店早餐都要好吃很多哈哈\n正餐我们没有在酒店吃附近有很多农庄当晚我们选了【来来农庄】开车5min就到老板很热情菜品也很不错第二天退房后去了【三禾稻里】10min路程我们到店太晚了只有西餐价格偏贵但味道挺好的。不得不说这里真的好美啊如果不带娃的话我可能会选这个民宿哈哈很适合躺平发呆\n\n🌟总结是一家非常不错的亲子酒店很适合亲子家庭入住必二刷"
},
{
"content": "一开始看朋友去了觉得不错还心想清远酒店应该很便宜吧一查居然要1300+当时觉得有点奢侈,但是去了之后觉得好好玩,好舒服,还想再来!\n\n👨👩👦 酒店设施——超多亲子项目\n有三个主题的儿童屋小厨房、钢琴、玩具等户外可以玩沙子、滑滑梯还有小农场可以浇花有大型桌球台、小型高尔夫场还可以射箭。我儿子玩得很开心我自己也觉得很有趣\n\n♨ 酒店房间&温泉——\n房间挺新可以加装围栏房间有个独立的小池子我们晚上把娃带睡之后在露台吃宵夜泡温泉带娃人幸福时刻。公域的温泉环境也不错不过池子有点小\n\n🥣 酒店自助早餐——很丰盛 选择很多\n餐厅环境很好吃早餐的时候看窗外简直心旷神怡。选择也很多光是那碟绿色肠粉就拿了好几盘早餐吃得很满足也挺好吃\n\n🌴 酒店户外环境——很大 绿化很好\n超级适合我崽骑车吃完早餐之后在酒店户外绕了一圈有种像逛公园的感觉光合作用拉满真的很舒服\n\n⭐\n已经是两个月前去的了还是忍不住mark篇帖子记录。当时选了工作日来的人少体验好真的觉得很适合两岁起的娃我们应该还会再去玩滴"
}
]
}

524
code/VideoSplitter.py Normal file
View File

@ -0,0 +1,524 @@
import cv2
import numpy as np
import os
import subprocess
import shutil
from datetime import timedelta
import argparse
from sklearn.metrics.pairwise import cosine_similarity
from skimage.metrics import structural_similarity as ssim
# 设置固定的输入输出路径
INPUT_VIDEO_PATH = "/root/autodl-tmp/hot_video_analyse/source/sample_demo_1.mp4" # 修改为视频文件夹路径
OUTPUT_DIR = "/root/autodl-tmp/hot_video_analyse/source/Splitter" # 请修改为您的实际输出目录路径
# 支持的视频格式
VIDEO_EXTENSIONS = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv']
# 设置参数
SAMPLE_RATE = 1 # 帧采样率
METHOD = "ssim" # 比较方法,可选 "ssim" 或 "cosine"
## 有分镜0.2 无分镜0.7
THRESHOLD = 0.8 # 相似度阈值
# 启用详细日志输出
VERBOSE = True
# FFMPEG可能的路径
FFMPEG_PATHS = [
'ffmpeg', # 系统路径
'/usr/bin/ffmpeg', # 常见Linux路径
'/usr/local/bin/ffmpeg', # 常见macOS路径
'C:\\ffmpeg\\bin\\ffmpeg.exe', # 常见Windows路径
]
def find_ffmpeg():
"""查找系统中可用的ffmpeg路径"""
# 首先尝试使用which/where命令
try:
if os.name == 'nt': # Windows
result = subprocess.run(['where', 'ffmpeg'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode == 0:
return result.stdout.strip().split('\n')[0]
else: # Linux/Mac
result = subprocess.run(['which', 'ffmpeg'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
# 然后检查预定义的路径
for path in FFMPEG_PATHS:
if shutil.which(path):
return path
return None
def extract_frames(video_path, output_dir, sample_rate=1):
"""
从视频中提取帧并保存到指定目录
Args:
video_path: 视频文件路径
output_dir: 输出帧的目录
sample_rate: 采样率每N帧提取一帧
Returns:
frames_info: 包含帧信息的列表 [(frame_number, timestamp, frame_path), ...]
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 打开视频
cap = cv2.VideoCapture(video_path)
# 获取视频属性
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = frame_count / fps
print(f"视频信息:{frame_count}帧, {fps}fps, 时长:{timedelta(seconds=duration)}")
frames_info = []
frame_number = 0
saved_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
if frame_number % sample_rate == 0:
# 计算时间戳(秒)
timestamp = frame_number / fps
# 保存帧
frame_path = os.path.join(output_dir, f"frame_{saved_count:05d}.jpg")
cv2.imwrite(frame_path, frame)
# 记录帧信息
frames_info.append((frame_number, timestamp, frame_path))
saved_count += 1
frame_number += 1
# 显示进度
if frame_number % 100 == 0:
print(f"处理进度: {frame_number}/{frame_count} ({frame_number/frame_count*100:.2f}%)")
cap.release()
print(f"共提取了 {saved_count}")
return frames_info
def compare_frames(frame1_path, frame2_path, method='ssim', threshold=0.85):
"""
比较两帧的相似度
Args:
frame1_path: 第一帧路径
frame2_path: 第二帧路径
method: 比较方法'ssim''cosine'
threshold: 相似度阈值
Returns:
is_similar: 是否相似
similarity: 相似度值
"""
# 读取帧
frame1 = cv2.imread(frame1_path)
frame2 = cv2.imread(frame2_path)
# 调整大小以加快处理
frame1_resized = cv2.resize(frame1, (320, 180))
frame2_resized = cv2.resize(frame2, (320, 180))
# 转换为灰度图
gray1 = cv2.cvtColor(frame1_resized, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(frame2_resized, cv2.COLOR_BGR2GRAY)
if method == 'ssim':
# 使用结构相似性指数
similarity, _ = ssim(gray1, gray2, full=True)
else: # cosine
# 使用余弦相似度
flat1 = gray1.flatten().reshape(1, -1)
flat2 = gray2.flatten().reshape(1, -1)
similarity = cosine_similarity(flat1, flat2)[0][0]
is_similar = similarity >= threshold
return is_similar, similarity
def detect_scene_changes(frames_info, method='ssim', threshold=0.85):
"""
检测场景变化
Args:
frames_info: 帧信息列表
method: 比较方法
threshold: 相似度阈值
Returns:
scenes: 场景信息列表 [(start_frame, end_frame, start_time, end_time), ...]
"""
global SCENE_START_FRAMES # 声明使用全局变量
if len(frames_info) < 2:
return []
scenes = []
clips = []
scene_start = frames_info[0]
for i in range(1, len(frames_info)):
prev_frame_path = frames_info[i-1][2]
curr_frame_path = frames_info[i][2]
is_similar, similarity = compare_frames(
prev_frame_path, curr_frame_path, method, threshold
)
if not is_similar:
# 场景变化,记录上一个场景
scene_end = frames_info[i-1]
scene_duration = scene_end[1] - scene_start[1]
clips.append(scene_start[0])
# 只记录持续时间超过1秒的场景
if scene_duration >= 0.2:
scenes.append((
scene_start[0], # 开始帧号
scene_end[0], # 结束帧号
scene_start[1], # 开始时间
scene_end[1], # 结束时间
scene_duration # 持续时间
))
# 开始新场景
scene_start = frames_info[i]
if VERBOSE:
print(f"检测到场景变化点: 帧 {scene_end[0]}, 时间 {timedelta(seconds=scene_end[1])}, 相似度: {similarity:.4f}")
# 添加最后一个场景
scene_end = frames_info[-1]
scene_duration = scene_end[1] - scene_start[1]
if scene_duration >= 0.2:
scenes.append((
scene_start[0],
scene_end[0],
scene_start[1],
scene_end[1],
scene_duration
))
print(f"\n场景变化统计:")
print(f"检测到 {len(scenes)} 个场景, 平均时长: {sum(s[4] for s in scenes)/max(1, len(scenes)):.2f}")
return scenes , clips
def extract_video_clips(video_path, scenes, output_dir, ffmpeg_path=None):
"""
根据场景信息提取视频片段
Args:
video_path: 视频路径
scenes: 场景信息列表
output_dir: 输出目录
ffmpeg_path: ffmpeg可执行文件路径
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 检查ffmpeg是否可用
if ffmpeg_path is None:
ffmpeg_path = find_ffmpeg()
if ffmpeg_path is None:
print("错误: 找不到ffmpeg。请安装ffmpeg并确保它在系统路径中。")
print("您可以从 https://ffmpeg.org/download.html 下载ffmpeg")
print("或使用包管理器安装: apt-get install ffmpeg / brew install ffmpeg 等")
return []
print(f"\n开始切割视频: {video_path}")
print(f"输出目录: {output_dir}")
print(f"使用ffmpeg: {ffmpeg_path}")
print("-" * 60)
clips_info = []
for i, scene in enumerate(scenes):
start_time = scene[2]
end_time = scene[3]
duration = scene[4]
output_file = os.path.join(output_dir, f"clip_{i:03d}_{duration:.2f}s.mp4")
# 构建ffmpeg命令
cmd = [
ffmpeg_path,
'-i', video_path,
'-ss', f"{start_time:.2f}",
'-to', f"{end_time:.2f}",
'-c:v', 'libx264',
'-c:a', 'aac',
'-y', # 覆盖已存在的文件
output_file
]
try:
print(f"\n切割片段 {i+1}/{len(scenes)}:")
print(f" 开始时间: {timedelta(seconds=start_time)}")
print(f" 结束时间: {timedelta(seconds=end_time)}")
print(f" 时长: {duration:.2f}")
print(f" 输出文件: {os.path.basename(output_file)}")
if VERBOSE:
print(f" 执行命令: {' '.join(cmd)}")
# 使用subprocess执行命令可以获取输出和错误
result = subprocess.run(
cmd,
stdout=subprocess.PIPE if not VERBOSE else None,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
# 获取输出文件信息
file_size = os.path.getsize(output_file) / (1024 * 1024) # MB
print(f" ✓ 切割成功: {os.path.basename(output_file)} ({file_size:.2f} MB)")
clips_info.append({
'index': i,
'file': output_file,
'start': start_time,
'end': end_time,
'duration': duration,
'size_mb': file_size
})
else:
print(f" ✗ 切割失败: {result.stderr}")
except Exception as e:
print(f" ✗ 切割失败: {str(e)}")
# 汇总信息
if clips_info:
total_size = sum(clip['size_mb'] for clip in clips_info)
total_duration = sum(clip['duration'] for clip in clips_info)
print("\n切割完成汇总:")
print(f" 成功切割片段数: {len(clips_info)}/{len(scenes)}")
print(f" 总时长: {timedelta(seconds=total_duration)}")
print(f" 总文件大小: {total_size:.2f} MB")
# 列出所有文件
print("\n输出文件列表:")
for clip in clips_info:
print(f" {os.path.basename(clip['file'])} - {clip['duration']:.2f}秒 ({clip['size_mb']:.2f} MB)")
else:
print("\n没有成功切割任何片段")
return clips_info
def get_video_files(directory):
"""
获取目录中所有视频文件
Args:
directory: 目录路径
Returns:
视频文件路径列表
"""
video_files = []
# 检查是否是单个文件
if os.path.isfile(directory):
ext = os.path.splitext(directory)[1].lower()
if ext in VIDEO_EXTENSIONS:
return [directory]
# 遍历目录
for root, _, files in os.walk(directory):
for file in files:
# 检查文件扩展名
ext = os.path.splitext(file)[1].lower()
if ext in VIDEO_EXTENSIONS:
video_files.append(os.path.join(root, file))
return video_files
def process_video(video_path, output_base_dir, sample_rate, method, threshold, ffmpeg_path):
"""
处理单个视频文件
Args:
video_path: 视频文件路径
output_base_dir: 基础输出目录
sample_rate: 帧采样率
method: 比较方法
threshold: 相似度阈值
ffmpeg_path: ffmpeg路径
Returns:
处理是否成功
"""
# 获取视频文件名(不含扩展名)
video_filename = os.path.splitext(os.path.basename(video_path))[0]
# 为当前视频创建输出目录
video_output_dir = os.path.join(output_base_dir, video_filename)
if not os.path.exists(video_output_dir):
os.makedirs(video_output_dir)
# 创建输出子目录
frames_dir = os.path.join(video_output_dir, 'frames')
clips_dir = os.path.join(video_output_dir, 'clips')
if not os.path.exists(frames_dir):
os.makedirs(frames_dir)
if not os.path.exists(clips_dir):
os.makedirs(clips_dir)
print("\n处理参数:")
print(f"输入视频: {os.path.abspath(video_path)}")
print(f"输出目录: {os.path.abspath(video_output_dir)}")
print(f"帧采样率: 每{sample_rate}")
print(f"比较方法: {method}")
print(f"相似度阈值: {threshold}")
print("-" * 60)
try:
# 步骤1: 提取帧
print("\n步骤1: 正在提取视频帧...")
frames_info = extract_frames(video_path, frames_dir, sample_rate)
# 步骤2: 检测场景变化
print("\n步骤2: 正在检测场景变化...")
scenes = detect_scene_changes(frames_info, method, threshold)
# 输出场景信息
print(f"\n检测到 {len(scenes)} 个场景:")
for i, scene in enumerate(scenes):
start_time = timedelta(seconds=scene[2])
end_time = timedelta(seconds=scene[3])
duration = scene[4]
print(f"场景 {i+1}: {start_time} - {end_time} (时长: {duration:.2f}s)")
# 如果没有ffmpeg提前报错
if not ffmpeg_path:
print("\n错误: 缺少ffmpeg无法继续视频切割步骤。")
print("请安装ffmpeg后重试。")
return False
# 步骤3: 提取视频片段
print("\n步骤3: 正在提取视频片段...")
clips_info = extract_video_clips(video_path, scenes, clips_dir, ffmpeg_path)
# 创建结果摘要文件
summary_file = os.path.join(video_output_dir, 'summary.txt')
try:
with open(summary_file, 'w', encoding='utf-8') as f:
f.write("智能视频切割结果摘要\n")
f.write("=" * 40 + "\n\n")
f.write(f"输入视频: {os.path.abspath(video_path)}\n")
f.write(f"处理时间: {os.path.getmtime(summary_file)}\n\n")
f.write(f"检测到场景数: {len(scenes)}\n")
f.write(f"生成片段数: {len(clips_info)}\n\n")
f.write("片段详情:\n")
for i, clip in enumerate(clips_info):
f.write(f"{i+1}. {os.path.basename(clip['file'])}\n")
f.write(f" 开始: {timedelta(seconds=clip['start'])}\n")
f.write(f" 结束: {timedelta(seconds=clip['end'])}\n")
f.write(f" 时长: {clip['duration']:.2f}\n")
f.write(f" 大小: {clip['size_mb']:.2f} MB\n\n")
print(f"\n已保存处理摘要到: {summary_file}")
except Exception as e:
print(f"保存摘要文件失败: {str(e)}")
print("\n处理完成!")
print(f"帧提取目录: {os.path.abspath(frames_dir)}")
print(f"视频片段目录: {os.path.abspath(clips_dir)}")
return True
except Exception as e:
print(f"\n处理视频 {video_path} 时发生错误: {str(e)}")
return False
def get_parent_folder_name(path):
"""
获取路径中 'video' 上一级文件夹的名字
"""
abs_path = os.path.abspath(path)
# 如果是文件夹,直接用
if os.path.isdir(abs_path):
parent = os.path.dirname(abs_path.rstrip('/'))
folder_name = os.path.basename(parent)
else:
# 如果是文件,取其父目录的父目录
parent = os.path.dirname(os.path.dirname(abs_path))
folder_name = os.path.basename(parent)
return folder_name
def main():
# 欢迎信息
print("=" * 60)
print("智能视频切割工具 - 批量处理版")
print("=" * 60)
# 查找ffmpeg
ffmpeg_path = find_ffmpeg()
if ffmpeg_path:
print(f"已找到ffmpeg: {ffmpeg_path}")
else:
print("警告: 未找到ffmpeg视频切割功能将不可用")
print("请安装ffmpeg并确保它在系统路径中")
# 获取输入目录中的所有视频文件
video_files = get_video_files(INPUT_VIDEO_PATH)
if not video_files:
print(f"错误: 在 '{INPUT_VIDEO_PATH}' 中没有找到视频文件")
print(f"支持的视频格式: {', '.join(VIDEO_EXTENSIONS)}")
return
# 自动获取video上一级文件夹名
parent_folder_name = get_parent_folder_name(INPUT_VIDEO_PATH)
output_dir = os.path.join(OUTPUT_DIR, parent_folder_name)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
print(f"\n输出目录: {output_dir}")
# 处理每个视频文件
successful = 0
failed = 0
for i, video_path in enumerate(video_files):
print("\n" + "=" * 60)
print(f"正在处理视频 [{i+1}/{len(video_files)}]: {os.path.basename(video_path)}")
print("=" * 60)
success = process_video(
video_path=video_path,
output_base_dir=output_dir,
sample_rate=SAMPLE_RATE,
method=METHOD,
threshold=THRESHOLD,
ffmpeg_path=ffmpeg_path
)
if success:
successful += 1
else:
failed += 1
# 打印批量处理总结
print("\n" + "=" * 60)
print("批量处理完成!")
print("=" * 60)
print(f"总共处理: {len(video_files)} 个视频文件")
print(f"成功: {successful}")
print(f"失败: {failed}")
print(f"输出目录: {os.path.abspath(output_dir)}")
if __name__ == "__main__":
main()

Binary file not shown.

242
code/client.py Normal file
View File

@ -0,0 +1,242 @@
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer
from vllm.assets.image import ImageAsset
from vllm.assets.video import VideoAsset
from openai import OpenAI
from dataclasses import dataclass, field
from typing import Optional, ClassVar
import numpy as np
from numpy.typing import NDArray
from PIL import Image
import librosa
import librosa.util
import os
import cv2
# import ray # 注释掉Ray导入
# # 使用本地模式初始化Ray避免分布式通信问题
# ray.init(local_mode=True, ignore_reinit_error=True)
# 设置环境变量,禁用在线检查
os.environ["HF_DATASETS_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"
os.environ["HF_HUB_OFFLINE"] = "1"
@dataclass(frozen=True)
class VideoAsset:
name: str
num_frames: int = -1
_NAME_TO_FILE: ClassVar[dict[str, str]] = {
"baby_reading": "sample_demo_1.mp4",
}
@property
def filename(self) -> str:
return self._NAME_TO_FILE[self.name]
@property
def pil_images(self) -> list[Image.Image]:
video_path = download_video_asset(self.filename)
return video_to_pil_images_list(video_path, self.num_frames)
@property
def np_ndarrays(self) -> NDArray:
video_path = download_video_asset(self.filename)
return video_to_ndarrays(video_path, self.num_frames)
def get_audio(self, sampling_rate: Optional[float] = None) -> NDArray:
video_path = download_video_asset(self.filename)
return librosa.load(video_path, sr=sampling_rate)[0]
@dataclass(frozen=True)
class LocalVideoAsset:
local_path: str
name: str = "local_video"
num_frames: int = -1
@property
def filename(self) -> str:
return self.local_path
@property
def pil_images(self) -> list[Image.Image]:
return video_to_pil_images_list(self.filename, self.num_frames)
@property
def np_ndarrays(self) -> NDArray:
return video_to_ndarrays(self.filename, self.num_frames)
def get_audio(self, sampling_rate: Optional[float] = None) -> NDArray:
try:
if not os.path.exists(self.filename):
print(f"音频文件不存在: {self.filename}")
return np.zeros(1) # 返回空数组
return librosa.load(self.filename, sr=sampling_rate)[0]
except Exception as e:
print(f"加载音频时出错: {e}")
return np.zeros(1) # 出错时返回空数组
# 辅助函数实现
def download_video_asset(filename: str) -> str:
# 如果路径是绝对路径或相对路径,直接返回
if filename.startswith("/") or filename.startswith("./"):
return filename
# 否则执行下载逻辑(原实现)
return f"/path/to/downloaded/{filename}"
def video_to_pil_images_list(video_path: str, num_frames: int) -> list[Image.Image]:
"""将视频转换为PIL图像列表"""
if not os.path.exists(video_path):
print(f"视频文件不存在: {video_path}")
return []
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"无法打开视频: {video_path}")
return []
# 获取视频帧数
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
duration = total_frames / fps if fps > 0 else 0
print(f"视频信息: 总帧数={total_frames}, FPS={fps:.2f}, 时长={duration:.2f}")
# 如果指定了帧数,设置采样间隔;否则读取所有帧
if num_frames > 0 and num_frames < total_frames:
frame_interval = total_frames / num_frames
print(f"将提取 {num_frames} 帧,采样间隔为每 {frame_interval:.2f}")
else:
frame_interval = 1
num_frames = total_frames
print(f"将提取所有 {total_frames}")
pil_images = []
frame_count = 0
success = True
last_progress = -1
while success and len(pil_images) < num_frames:
# 读取下一帧
success, frame = cap.read()
if not success:
break
# 按间隔采样帧
if frame_count % max(1, int(frame_interval)) == 0:
# OpenCV使用BGR转为RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 转为PIL图像
pil_image = Image.fromarray(rgb_frame)
pil_images.append(pil_image)
# 显示进度每10%显示一次)
progress = int(len(pil_images) / num_frames * 10)
if progress > last_progress:
print(f"提取进度: {len(pil_images)}/{num_frames} ({len(pil_images)/num_frames*100:.1f}%)")
last_progress = progress
frame_count += 1
cap.release()
print(f"从视频中共提取了 {len(pil_images)}")
return pil_images
def video_to_ndarrays(video_path: str, num_frames: int) -> NDArray:
"""将视频转换为NumPy数组"""
pil_images = video_to_pil_images_list(video_path, num_frames)
if not pil_images:
print(f"未能从视频中提取帧: {video_path}")
return np.zeros((1, 224, 224, 3))
# 将PIL图像列表转换为NumPy数组
arrays = []
for img in pil_images:
# 调整图像大小为统一尺寸
img_resized = img.resize((224, 224))
# 转换为NumPy数组
arr = np.array(img_resized)
arrays.append(arr)
# 堆叠为单个NumPy数组形状为[num_frames, height, width, channels]
stacked_array = np.stack(arrays, axis=0)
print(f"NumPy数组形状: {stacked_array.shape}")
return stacked_array
# 使用示例
if __name__ == "__main__":
# 使用本地视频资源
local_video = LocalVideoAsset(
local_path="/root/autodl-tmp/hot_vedio_analyse/source/sample_demo_1.mp4",
num_frames= 1
)
print("本地资源:", local_video.filename)
# 获取PIL图像列表实际会调用download_video_asset和转换函数
pil_images = local_video.pil_images
#print("PIL图像数量:", len(pil_images))
# 获取NumPy数组
#np_arrays = local_video.np_ndarrays
# 获取音频数据
#audio = local_video.get_audio(sampling_rate=16000)
#print("音频数据形状:", audio.shape)
try:
print("尝试加载模型...")
# 模型和分词器路径
model_path = "/root/autodl-tmp/llm/Qwen2.5-VL"
# 使用离线模式加载分词器
tokenizer = AutoTokenizer.from_pretrained(
model_path,
local_files_only=True,
trust_remote_code=True
)
# 采样参数
sampling_params = SamplingParams(
temperature=0.6,
top_p=0.95,
top_k=20,
max_tokens=1024
)
openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8000/v1"
client = OpenAI(
api_key=openai_api_key,
base_url=openai_api_base,
)
completion = client.completions.create(model="Qwen/Qwen2.5-1.5B-Instruct",
prompt="San Francisco is a")
print("Completion result:", completion)
prompt = "这个视频展示了什么内容?详细描述一下。"
# 使用generate而不是generate_videos (如果不存在generate_videos方法)
try:
# 尝试使用generate_videos
outputs = client.completions(prompt, videos=[pil_images], sampling_params=sampling_params)
print(outputs[0].outputs[0].text) # 打印模型输出
except AttributeError:
print("generate_videos方法不可用尝试使用普通generate方法...")
# 如果不支持generate_videos使用普通的generate
outputs = llm.generate([prompt], sampling_params=sampling_params)
print(outputs[0].outputs[0].text) # 打印模型输出
except Exception as e:
print(f"模型加载或推理过程中出错: {e}")
import traceback
traceback.print_exc()

82
code/clip.py Normal file
View File

@ -0,0 +1,82 @@
from VideoSplitter import *
import cv2
from PIL import Image
import matplotlib.pyplot as plt
def get_pil_images_from_frame_numbers(video_path, frame_numbers):
"""
从视频中读取指定帧号的帧并转换为PIL图像
Args:
video_path: 视频文件路径
frame_numbers: 要读取的帧号列表
Returns:
pil_images: PIL图像列表
"""
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
pil_images = []
print(f"开始读取 {len(frame_numbers)} 个指定帧并转换为PIL图像...")
for i, frame_number in enumerate(frame_numbers):
# 设置视频位置到指定帧
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
ret, frame = cap.read()
if ret:
# 将OpenCV的BGR格式转换为RGB格式
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 转换为PIL图像
pil_image = Image.fromarray(frame_rgb)
pil_images.append(pil_image)
print(f"已读取并转换帧 {frame_number} ({i+1}/{len(frame_numbers)})")
else:
print(f"无法读取帧 {frame_number}")
cap.release()
print(f"完成! 共获得了 {len(pil_images)} 个PIL图像")
return pil_images
video_path = "/root/autodl-tmp/hot_video_analyse/source/sample_demo_1.mp4"
frames_dir = "/root/autodl-tmp/hot_video_analyse/source/Splitter/hot_video_analyse/sample_demo_1/frames"
output_dir = "/root/autodl-tmp/hot_video_analyse/source/pil_images"
sample_rate = 1
method = "ssim"
threshold = 0.8
# 步骤1: 提取帧
print("\n步骤1: 正在提取视频帧...")
frames_info = extract_frames(video_path, frames_dir, sample_rate)
# 步骤2: 检测场景变化
print("\n步骤2: 正在检测场景变化...")
scenes, a = detect_scene_changes(frames_info, method, threshold)
print(f"场景开始帧号: {a}")
# 步骤3: 将场景开始帧转换为PIL图像
print("\n步骤3: 转换场景开始帧为PIL图像...")
pil_images = get_pil_images_from_frame_numbers(video_path, a)
# 显示结果
print(f"\n成功获得 {len(pil_images)} 个PIL图像")
# 查看第一个PIL图像的信息
if pil_images:
first_pil = pil_images[0]
pil_output_path = os.path.join(output_dir, f"first_scene_frame_{a[0]}.jpg")
first_pil.save(pil_output_path, quality=95)
print(f"第一个PIL图像信息:")
print(f" 尺寸: {first_pil.size}")
print(f" 模式: {first_pil.mode}")
"""
视频帧
音频转文字
全视频
"""

107
code/npy2jpg.py Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as np
from PIL import Image
import os
import sys
def npy_to_images(npy_file):
"""
将NumPy文件转换为图片
Args:
npy_file: .npy文件路径
"""
# 检查文件是否存在
if not os.path.exists(npy_file):
print(f"错误: 文件不存在 - {npy_file}")
return
# 加载NumPy数组
print(f"加载文件: {npy_file}")
arrays = np.load(npy_file)
print(f"数组形状: {arrays.shape}")
print(f"数据类型: {arrays.dtype}")
# 创建输出目录
output_dir = f"images_{os.path.splitext(os.path.basename(npy_file))[0]}"
os.makedirs(output_dir, exist_ok=True)
# 数据类型转换
if arrays.dtype == np.float32 or arrays.dtype == np.float64:
if arrays.max() <= 1.0:
# 范围 [0, 1],转换为 [0, 255]
arrays = (arrays * 255).astype(np.uint8)
else:
# 范围可能是 [0, 255] 的浮点数
arrays = np.clip(arrays, 0, 255).astype(np.uint8)
else:
arrays = arrays.astype(np.uint8)
# 根据数组维度处理
if len(arrays.shape) == 4:
# 4D数组: 多张图片
print(f"检测到4D数组转换 {arrays.shape[0]} 张图片")
for i in range(arrays.shape[0]):
frame = arrays[i]
# 处理通道顺序 (CHW -> HWC)
if frame.shape[0] == 3 or frame.shape[0] == 1:
frame = np.transpose(frame, (1, 2, 0))
# 保存图片
if len(frame.shape) == 3 and frame.shape[2] == 1:
frame = frame.squeeze(axis=2) # 移除单通道维度
img = Image.fromarray(frame)
filename = f"{output_dir}/image_{i+1:04d}.jpg"
img.save(filename)
if (i + 1) % 10 == 0 or i == 0:
print(f"已保存: {filename}")
elif len(arrays.shape) == 3:
# 3D数组: 单张图片或多张灰度图
if arrays.shape[2] == 3 or arrays.shape[2] == 1:
# 单张彩色或灰度图片 (H, W, C)
print("检测到3D数组转换为单张图片")
if arrays.shape[2] == 1:
arrays = arrays.squeeze(axis=2)
img = Image.fromarray(arrays)
filename = f"{output_dir}/image.jpg"
img.save(filename)
print(f"已保存: {filename}")
else:
# 多张灰度图片 (N, H, W)
print(f"检测到3D数组转换 {arrays.shape[0]} 张灰度图片")
for i in range(arrays.shape[0]):
img = Image.fromarray(arrays[i])
filename = f"{output_dir}/image_{i+1:04d}.jpg"
img.save(filename)
if (i + 1) % 10 == 0 or i == 0:
print(f"已保存: {filename}")
elif len(arrays.shape) == 2:
# 2D数组: 单张灰度图片
print("检测到2D数组转换为灰度图片")
img = Image.fromarray(arrays)
filename = f"{output_dir}/image.jpg"
img.save(filename)
print(f"已保存: {filename}")
else:
print(f"不支持的数组维度: {arrays.shape}")
return
print(f"所有图片已保存到目录: {output_dir}")
def main():
"""主函数"""
npy_file = "/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_arrays.npy"
npy_to_images(npy_file)
if __name__ == "__main__":
main()

280
code/qwen.py Normal file
View File

@ -0,0 +1,280 @@
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer
from vllm.assets.image import ImageAsset
from vllm.assets.video import VideoAsset
from dataclasses import dataclass, field
from typing import Optional, ClassVar, List, Tuple
import numpy as np
from numpy.typing import NDArray
from PIL import Image
import librosa
import librosa.util
import cv2
import os
from openai import OpenAI
import base64
# import ray # 注释掉Ray导入
# # 使用本地模式初始化Ray避免分布式通信问题
# ray.init(local_mode=True, ignore_reinit_error=True)
# 设置环境变量,禁用在线检查
os.environ["HF_DATASETS_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"
os.environ["HF_HUB_OFFLINE"] = "1"
@dataclass(frozen=True)
class VideoAsset:
name: str
num_frames: int = -1
_NAME_TO_FILE: ClassVar[dict[str, str]] = {
"baby_reading": "sample_demo_1.mp4",
}
@property
def filename(self) -> str:
return self._NAME_TO_FILE[self.name]
@property
def pil_images(self) -> list[Image.Image]:
video_path = download_video_asset(self.filename)
return video_to_pil_images_list(video_path, self.num_frames)
@property
def np_ndarrays(self) -> NDArray:
video_path = download_video_asset(self.filename)
return video_to_ndarrays(video_path, self.num_frames)
def get_audio(self, sampling_rate: Optional[float] = None) -> NDArray:
video_path = download_video_asset(self.filename)
return librosa.load(video_path, sr=sampling_rate)[0]
@dataclass(frozen=True)
class LocalVideoAsset:
local_path: str
name: str = "local_video"
num_frames: int = -1
@property
def filename(self) -> str:
return self.local_path
@property
def pil_images(self) -> list[Image.Image]:
return video_to_pil_images_list(self.filename, self.num_frames)
@property
def np_ndarrays(self) -> NDArray:
return video_to_ndarrays(self.filename, self.num_frames)
def get_audio(self, sampling_rate: Optional[float] = None) -> NDArray:
try:
if not os.path.exists(self.filename):
print(f"音频文件不存在: {self.filename}")
return np.zeros(1) # 返回空数组
return librosa.load(self.filename, sr=sampling_rate)[0]
except Exception as e:
print(f"加载音频时出错: {e}")
return np.zeros(1) # 出错时返回空数组
# 辅助函数实现
def download_video_asset(filename: str) -> str:
# 如果路径是绝对路径或相对路径,直接返回
if filename.startswith("/") or filename.startswith("./"):
return filename
# 否则执行下载逻辑(原实现)
return f"/path/to/downloaded/{filename}"
def video_to_pil_images_list(video_path: str, num_frames: int) -> list[Image.Image]:
"""将视频转换为PIL图像列表"""
if not os.path.exists(video_path):
print(f"视频文件不存在: {video_path}")
return []
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"无法打开视频: {video_path}")
return []
# 获取视频帧数
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
duration = total_frames / fps if fps > 0 else 0
print(f"视频信息: 总帧数={total_frames}, FPS={fps:.2f}, 时长={duration:.2f}")
# 如果指定了帧数,设置采样间隔;否则读取所有帧
if num_frames > 0 and num_frames < total_frames:
frame_interval = total_frames / num_frames
print(f"将提取 {num_frames} 帧,采样间隔为每 {frame_interval:.2f}")
else:
frame_interval = 1
num_frames = total_frames
print(f"将提取所有 {total_frames}")
pil_images = []
frame_count = 0
success = True
last_progress = -1
while success and len(pil_images) < num_frames:
# 读取下一帧
success, frame = cap.read()
if not success:
break
# 按间隔采样帧
if frame_count % max(1, int(frame_interval)) == 0:
# OpenCV使用BGR转为RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 转为PIL图像
pil_image = Image.fromarray(rgb_frame)
pil_images.append(pil_image)
# 显示进度每10%显示一次)
progress = int(len(pil_images) / num_frames * 10)
if progress > last_progress:
print(f"提取进度: {len(pil_images)}/{num_frames} ({len(pil_images)/num_frames*100:.1f}%)")
last_progress = progress
frame_count += 1
cap.release()
print(f"从视频中共提取了 {len(pil_images)}")
return pil_images
def video_to_ndarrays(video_path: str, num_frames: int) -> NDArray:
"""将视频转换为NumPy数组"""
pil_images = video_to_pil_images_list(video_path, num_frames)
if not pil_images:
print(f"未能从视频中提取帧: {video_path}")
return np.zeros((1, 224, 224, 3))
# 将PIL图像列表转换为NumPy数组
arrays = []
for img in pil_images:
# 调整图像大小为统一尺寸
img_resized = img.resize((224, 224))
# 转换为NumPy数组
arr = np.array(img_resized)
arrays.append(arr)
# 堆叠为单个NumPy数组形状为[num_frames, height, width, channels]
stacked_array = np.stack(arrays, axis=0)
print(f"NumPy数组形状: {stacked_array.shape}")
return stacked_array
def encode_image(image_path):
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
def encode_audio(audio_path):
with open(audio_path, "rb") as audio_file:
return base64.b64encode(audio_file.read()).decode("utf-8")
# 使用示例
if __name__ == "__main__":
# 使用本地视频资源
# local_video = LocalVideoAsset(
# local_path="/root/autodl-tmp/hot_video_analyse/source/sample_demo_1.mp4",
# num_frames = -1 # 限制帧数以加快测试速度
# )
# print("本地资源:", local_video.filename)
# 获取PIL图像列表实际会调用download_video_asset和转换函数
# pil_images = local_video.pil_images
# print("PIL图像数量:", len(pil_images))
# print(pil_images[0])
# 获取NumPy数组
# print("\n=== 加载视频帧 ===")
# np_arrays = local_video.np_ndarrays
# print(f"视频数组形状: {np_arrays.shape}")
# scene_change_arrays = np.load('/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_arrays.npy')
# print(f"场景变化帧数组形状: {scene_change_arrays.shape}")
# 获取音频数据
# audio = local_video.get_audio(sampling_rate=16000)
# print("音频数据形状:", audio.shape)
# print(type(audio))
base64_audio = encode_audio("/root/autodl-tmp/hot_video_analyse/source/transcription/sample_demo_1_audio.wav")
client = OpenAI(
# 若没有配置环境变量请用阿里云百炼API Key将下行替换为api_key="sk-xxx",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
base64_image_0 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000034_t1.13s_ssim0.5673.jpg")
base64_image_1 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000117_t3.90s_ssim0.1989.jpg")
base64_image_2 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000119_t3.97s_ssim0.2138.jpg")
base64_image_3 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000140_t4.67s_ssim0.5160.jpg")
base64_image_4 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000160_t5.33s_ssim0.4934.jpg")
base64_image_5 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000180_t6.00s_ssim0.3577.jpg")
base64_image_6 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000201_t6.70s_ssim0.3738.jpg")
base64_image_7 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000222_t7.40s_ssim0.6104.jpg")
base64_image_8 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000243_t8.10s_ssim0.5099.jpg")
base64_image_9 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000261_t8.70s_ssim0.4735.jpg")
base64_image_10 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000281_t9.37s_ssim0.2703.jpg")
base64_image_11 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000301_t10.03s_ssim0.2772.jpg")
base64_image_12 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000321_t10.70s_ssim0.3721.jpg")
base64_image_13 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000341_t11.37s_ssim0.3700.jpg")
base64_image_14 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000361_t12.03s_ssim0.3494.jpg")
base64_image_15 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000382_t12.73s_ssim0.3423.jpg")
base64_image_16 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000402_t13.40s_ssim0.3252.jpg")
base64_image_17 = encode_image("/root/autodl-tmp/hot_video_analyse/source/scene_change/scene_change_frames/scene_change_000421_t14.03s_ssim0.2086.jpg")
completion = client.chat.completions.create(
model="/root/autodl-tmp/llm/Qwen2.5-Omni",
messages=[
{
"role": "user",
"content": [
{
"type": "video",
"video": [
f"data:image/jpeg;base64,{base64_image_1}",
f"data:image/jpeg;base64,{base64_image_2}",
f"data:image/jpeg;base64,{base64_image_3}",
f"data:image/jpeg;base64,{base64_image_4}",
f"data:image/jpeg;base64,{base64_image_5}",
f"data:image/jpeg;base64,{base64_image_6}",
f"data:image/jpeg;base64,{base64_image_7}",
f"data:image/jpeg;base64,{base64_image_8}",
f"data:image/jpeg;base64,{base64_image_9}",
f"data:image/jpeg;base64,{base64_image_10}",
f"data:image/jpeg;base64,{base64_image_11}",
f"data:image/jpeg;base64,{base64_image_12}",
f"data:image/jpeg;base64,{base64_image_13}",
f"data:image/jpeg;base64,{base64_image_14}",
f"data:image/jpeg;base64,{base64_image_15}",
f"data:image/jpeg;base64,{base64_image_16}",
f"data:image/jpeg;base64,{base64_image_17}",
],
},
{
"type": "audio",
"audio": f"data:audio/mpeg;base64,{base64_audio}",
"format": "wav",
},
{"type": "text", "text": "描述这个视频的具体过程"},
],
}
],
# 设置输出数据的模态,当前支持两种:["text","audio"]、["text"]
modalities=["text"],
#audio={"voice": "Cherry", "format": "wav"},
# stream 必须设置为 True否则会报错
stream=True,
stream_options={"include_usage": True},
)
for chunk in completion:
if chunk.choices:
print(chunk.choices[0].delta)
else:
print(chunk.usage)

540
code/video_split.py Normal file
View File

@ -0,0 +1,540 @@
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer
from vllm.assets.image import ImageAsset
from vllm.assets.video import VideoAsset
from dataclasses import dataclass, field
from typing import Optional, ClassVar, List, Tuple
import numpy as np
from numpy.typing import NDArray
from PIL import Image
import librosa
import librosa.util
import os
import cv2
from skimage.metrics import structural_similarity as ssim
# import ray # 注释掉Ray导入
# # 使用本地模式初始化Ray避免分布式通信问题
# ray.init(local_mode=True, ignore_reinit_error=True)
# 设置环境变量,禁用在线检查
os.environ["HF_DATASETS_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"
os.environ["HF_HUB_OFFLINE"] = "1"
@dataclass(frozen=True)
class VideoAsset:
name: str
num_frames: int = -1
_NAME_TO_FILE: ClassVar[dict[str, str]] = {
"baby_reading": "sample_demo_1.mp4",
}
@property
def filename(self) -> str:
return self._NAME_TO_FILE[self.name]
@property
def pil_images(self) -> list[Image.Image]:
video_path = download_video_asset(self.filename)
return video_to_pil_images_list(video_path, self.num_frames)
@property
def np_ndarrays(self) -> NDArray:
video_path = download_video_asset(self.filename)
return video_to_ndarrays(video_path, self.num_frames)
def get_audio(self, sampling_rate: Optional[float] = None) -> NDArray:
video_path = download_video_asset(self.filename)
return librosa.load(video_path, sr=sampling_rate)[0]
@dataclass(frozen=True)
class LocalVideoAsset:
local_path: str
name: str = "local_video"
num_frames: int = -1
@property
def filename(self) -> str:
return self.local_path
@property
def pil_images(self) -> list[Image.Image]:
return video_to_pil_images_list(self.filename, self.num_frames)
@property
def np_ndarrays(self) -> NDArray:
return video_to_ndarrays(self.filename, self.num_frames)
def get_audio(self, sampling_rate: Optional[float] = None) -> NDArray:
try:
if not os.path.exists(self.filename):
print(f"音频文件不存在: {self.filename}")
return np.zeros(1) # 返回空数组
return librosa.load(self.filename, sr=sampling_rate)[0]
except Exception as e:
print(f"加载音频时出错: {e}")
return np.zeros(1) # 出错时返回空数组
# 辅助函数实现
def download_video_asset(filename: str) -> str:
# 如果路径是绝对路径或相对路径,直接返回
if filename.startswith("/") or filename.startswith("./"):
return filename
# 否则执行下载逻辑(原实现)
return f"/path/to/downloaded/{filename}"
def video_to_pil_images_list(video_path: str, num_frames: int) -> list[Image.Image]:
"""将视频转换为PIL图像列表"""
if not os.path.exists(video_path):
print(f"视频文件不存在: {video_path}")
return []
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"无法打开视频: {video_path}")
return []
# 获取视频帧数
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
duration = total_frames / fps if fps > 0 else 0
print(f"视频信息: 总帧数={total_frames}, FPS={fps:.2f}, 时长={duration:.2f}")
# 如果指定了帧数,设置采样间隔;否则读取所有帧
if num_frames > 0 and num_frames < total_frames:
frame_interval = total_frames / num_frames
print(f"将提取 {num_frames} 帧,采样间隔为每 {frame_interval:.2f}")
else:
frame_interval = 1
num_frames = total_frames
print(f"将提取所有 {total_frames}")
pil_images = []
frame_count = 0
success = True
last_progress = -1
while success and len(pil_images) < num_frames:
# 读取下一帧
success, frame = cap.read()
if not success:
break
# 按间隔采样帧
if frame_count % max(1, int(frame_interval)) == 0:
# OpenCV使用BGR转为RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 转为PIL图像
pil_image = Image.fromarray(rgb_frame)
pil_images.append(pil_image)
# 显示进度每10%显示一次)
progress = int(len(pil_images) / num_frames * 10)
if progress > last_progress:
print(f"提取进度: {len(pil_images)}/{num_frames} ({len(pil_images)/num_frames*100:.1f}%)")
last_progress = progress
frame_count += 1
cap.release()
print(f"从视频中共提取了 {len(pil_images)}")
return pil_images
def video_to_ndarrays(video_path: str, num_frames: int) -> NDArray:
"""将视频转换为NumPy数组"""
pil_images = video_to_pil_images_list(video_path, num_frames)
if not pil_images:
print(f"未能从视频中提取帧: {video_path}")
return np.zeros((1, 224, 224, 3))
# 将PIL图像列表转换为NumPy数组
arrays = []
for img in pil_images:
# 调整图像大小为统一尺寸
img_resized = img.resize((224, 224))
# 转换为NumPy数组
arr = np.array(img_resized)
arrays.append(arr)
# 堆叠为单个NumPy数组形状为[num_frames, height, width, channels]
stacked_array = np.stack(arrays, axis=0)
print(f"NumPy数组形状: {stacked_array.shape}")
return stacked_array
@dataclass
class SceneChangeFrame:
"""场景变化帧信息"""
frame_index: int
timestamp: float
ssim_score: float
is_scene_change: bool
def calculate_ssim_between_frames(frame1: np.ndarray, frame2: np.ndarray) -> float:
"""
计算两帧之间的SSIM相似度
Args:
frame1: 第一帧图像 (H, W, C)
frame2: 第二帧图像 (H, W, C)
Returns:
SSIM相似度分数 (0-1之间)
"""
try:
# 转换为灰度图像以提高计算效率
if len(frame1.shape) == 3:
gray1 = cv2.cvtColor(frame1, cv2.COLOR_RGB2GRAY)
else:
gray1 = frame1
if len(frame2.shape) == 3:
gray2 = cv2.cvtColor(frame2, cv2.COLOR_RGB2GRAY)
else:
gray2 = frame2
# 确保两个图像尺寸相同
if gray1.shape != gray2.shape:
gray2 = cv2.resize(gray2, (gray1.shape[1], gray1.shape[0]))
# 计算SSIM
score = ssim(gray1, gray2, data_range=255)
return score
except Exception as e:
print(f"计算SSIM时出错: {e}")
return 1.0 # 出错时返回高相似度
def detect_scene_changes_from_arrays(video_arrays: np.ndarray,
similarity_threshold: float = 0.8,
fps: float = 30.0) -> List[SceneChangeFrame]:
"""
从视频NumPy数组中检测场景变化
Args:
video_arrays: 视频帧数组形状为 (num_frames, height, width, channels)
similarity_threshold: SSIM相似度阈值低于此值认为是场景变化
fps: 视频帧率用于计算时间戳
Returns:
场景变化帧列表
"""
if len(video_arrays) < 2:
print("视频帧数不足,无法进行场景变化检测")
return []
print(f"开始检测场景变化...")
print(f"视频帧数: {len(video_arrays)}")
print(f"帧尺寸: {video_arrays.shape[1:3]}")
print(f"SSIM相似度阈值: {similarity_threshold}")
print(f"视频帧率: {fps} FPS")
scene_changes = []
# 逐帧比较相似度
for i in range(1, len(video_arrays)):
# 获取前后两帧
prev_frame = video_arrays[i-1]
curr_frame = video_arrays[i]
# 计算SSIM相似度
ssim_score = calculate_ssim_between_frames(prev_frame, curr_frame)
# 计算时间戳
timestamp = i / fps
# 判断是否为场景变化
is_scene_change = ssim_score < similarity_threshold
# 创建场景变化信息
scene_change = SceneChangeFrame(
frame_index=i,
timestamp=timestamp,
ssim_score=ssim_score,
is_scene_change=is_scene_change
)
scene_changes.append(scene_change)
# 输出场景变化信息
if is_scene_change:
print(f"场景变化检测到! 帧号: {i}, 时间: {timestamp:.2f}s, SSIM: {ssim_score:.4f}")
# 显示进度
if i % 100 == 0:
progress = i / len(video_arrays) * 100
print(f"检测进度: {i}/{len(video_arrays)} ({progress:.1f}%)")
# 统计结果
scene_change_count = sum(1 for sc in scene_changes if sc.is_scene_change)
print(f"\n检测完成!")
print(f"总比较帧数: {len(scene_changes)}")
print(f"场景变化数量: {scene_change_count}")
return scene_changes
def get_scene_change_frames(scene_changes: List[SceneChangeFrame]) -> List[SceneChangeFrame]:
"""
获取所有场景变化帧相似度低于阈值的帧
Args:
scene_changes: 场景变化检测结果列表
Returns:
场景变化帧列表
"""
return [sc for sc in scene_changes if sc.is_scene_change]
def save_scene_change_frames_from_arrays(video_arrays: np.ndarray,
scene_change_frames: List[SceneChangeFrame],
output_dir: str = "scene_change_frames"):
"""
保存场景变化帧为图像文件
Args:
video_arrays: 视频帧数组
scene_change_frames: 场景变化帧列表
output_dir: 输出目录
"""
if not scene_change_frames:
print("没有检测到场景变化帧")
return
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)
print(f"保存 {len(scene_change_frames)} 个场景变化帧到 {output_dir}")
for i, scene_change in enumerate(scene_change_frames):
frame_index = scene_change.frame_index
if frame_index < len(video_arrays):
# 获取帧图像
frame = video_arrays[frame_index]
# 转换为PIL图像并保存
pil_image = Image.fromarray(frame.astype(np.uint8))
filename = f"scene_change_{frame_index:06d}_t{scene_change.timestamp:.2f}s_ssim{scene_change.ssim_score:.4f}.jpg"
filepath = os.path.join(output_dir, filename)
pil_image.save(filepath)
if (i + 1) % 10 == 0:
print(f"已保存 {i + 1}/{len(scene_change_frames)}")
print(f"所有场景变化帧已保存到: {output_dir}")
def export_scene_change_results(scene_change_frames: List[SceneChangeFrame],
similarity_threshold: float,
output_file: str = "scene_change_results.txt"):
"""
导出场景变化检测结果到文件
Args:
scene_change_frames: 场景变化帧列表
similarity_threshold: 使用的相似度阈值
output_file: 输出文件路径
"""
with open(output_file, 'w', encoding='utf-8') as f:
f.write(f"场景变化检测结果\n")
f.write(f"相似度阈值: {similarity_threshold}\n")
f.write(f"场景变化数量: {len(scene_change_frames)}\n")
f.write("-" * 50 + "\n")
for sc in scene_change_frames:
f.write(f"帧号: {sc.frame_index:6d}, "
f"时间: {sc.timestamp:8.2f}s, "
f"SSIM: {sc.ssim_score:.6f}\n")
print(f"检测结果已导出到: {output_file}")
def get_scene_change_frames_arrays(video_arrays: np.ndarray,
scene_change_frames: List[SceneChangeFrame]) -> np.ndarray:
"""
获取场景变化帧对应的NumPy数组集合
Args:
video_arrays: 完整的视频帧数组形状为 (num_frames, height, width, channels)
scene_change_frames: 场景变化帧列表
Returns:
场景变化帧的NumPy数组形状为 (num_scene_changes, height, width, channels)
"""
if not scene_change_frames:
print("没有场景变化帧")
return np.array([])
# 提取场景变化帧的索引
scene_change_indices = [sc.frame_index for sc in scene_change_frames]
# 确保索引在有效范围内
valid_indices = [idx for idx in scene_change_indices if 0 <= idx < len(video_arrays)]
if not valid_indices:
print("没有有效的场景变化帧索引")
return np.array([])
# 提取对应的帧数组
scene_change_arrays = video_arrays[valid_indices]
print(f"提取了 {len(valid_indices)} 个场景变化帧的NumPy数组")
print(f"场景变化帧数组形状: {scene_change_arrays.shape}")
return scene_change_arrays
# 使用示例
if __name__ == "__main__":
# 使用本地视频资源
local_video = LocalVideoAsset(
local_path="/root/autodl-tmp/hot_video_analyse/source/sample_demo_1.mp4",
num_frames = -1 # 限制帧数以加快测试速度
)
print("本地资源:", local_video.filename)
# 获取PIL图像列表实际会调用download_video_asset和转换函数
# pil_images = local_video.pil_images
# print("PIL图像数量:", len(pil_images))
# print(pil_images[0])
# 获取NumPy数组
print("\n=== 加载视频帧 ===")
np_arrays = local_video.np_ndarrays
print(f"视频数组形状: {np_arrays.shape}")
# 设置检测参数
similarity_threshold = 0.7 # SSIM相似度阈值
fps = 30.0 # 假设帧率为30fps可以根据实际视频调整
print(f"\n=== 开始场景变化检测 ===")
print(f"相似度阈值: {similarity_threshold}")
# 检测场景变化
scene_changes = detect_scene_changes_from_arrays(
video_arrays=np_arrays,
similarity_threshold=similarity_threshold,
fps=fps
)
# 获取场景变化帧(相似度低于阈值的帧)
scene_change_frames = get_scene_change_frames(scene_changes)
# 获取场景变化帧对应的NumPy数组集合
scene_change_arrays = get_scene_change_frames_arrays(np_arrays, scene_change_frames)
print(f"\n=== 场景变化帧NumPy数组信息 ===")
if len(scene_change_arrays) > 0:
print(f"场景变化帧数组形状: {scene_change_arrays.shape}")
print(f"数据类型: {scene_change_arrays.dtype}")
print(f"数组大小: {scene_change_arrays.nbytes / (1024*1024):.2f} MB")
# 获取详细信息(包含帧信息和数组)
print(f"\n=== 检测结果摘要 ===")
print(f"低于阈值 {similarity_threshold} 的帧数: {len(scene_change_frames)}")
if scene_change_frames:
print(f"\n所有场景变化帧:")
for i, sc in enumerate(scene_change_frames):
print(f"{i+1:3d}. 帧号: {sc.frame_index:6d}, "
f"时间: {sc.timestamp:7.2f}s, "
f"SSIM: {sc.ssim_score:.6f}")
# 保存场景变化帧图像
print(f"\n=== 保存场景变化帧 ===")
save_scene_change_frames_from_arrays(
video_arrays=np_arrays,
scene_change_frames=scene_change_frames,
output_dir="scene_change_frames"
)
# 导出结果到文件
export_scene_change_results(
scene_change_frames=scene_change_frames,
similarity_threshold=similarity_threshold,
output_file="scene_change_results.txt"
)
# 演示如何使用场景变化帧的NumPy数组
print(f"\n=== 场景变化帧NumPy数组使用示例 ===")
print(f"场景变化帧数组变量名: scene_change_arrays")
print(f"可以直接使用这个数组进行进一步处理,例如:")
print(f"- 输入到深度学习模型")
print(f"- 进行图像处理操作")
print(f"- 保存为其他格式")
# 保存场景变化帧数组到文件(可选)
print(f"\n=== 保存场景变化帧数组 ===")
if len(scene_change_arrays) > 0:
np.save("scene_change_arrays.npy", scene_change_arrays)
print(f"场景变化帧NumPy数组已保存到: scene_change_arrays.npy")
print(f"可以使用 np.load('scene_change_arrays.npy') 重新加载")
else:
print("没有检测到场景变化帧")
print("建议:")
print("1. 降低相似度阈值如0.7或0.6")
print("2. 检查视频是否包含场景变化")
print("3. 增加视频帧数进行更全面的检测")
# 获取音频数据
audio = local_video.get_audio(sampling_rate=16000)
print("音频数据形状:", audio.shape)
print(type(audio))
"""
try:
print("尝试加载模型...")
# 模型和分词器路径
model_path = "/root/autodl-tmp/llm/Qwen2.5-VL"
# 使用离线模式加载分词器
tokenizer = AutoTokenizer.from_pretrained(
model_path,
local_files_only=True,
trust_remote_code=True
)
# 采样参数
sampling_params = SamplingParams(
temperature=0.6,
top_p=0.95,
top_k=20,
max_tokens=1024
)
# 离线模式初始化模型
llm = LLM(
model=model_path,
tokenizer=model_path, # 使用相同的路径作为分词器路径
max_model_len=4096,
tensor_parallel_size=1, # 减少为1避免多GPU通信问题
gpu_memory_utilization=0.8, # 稍微降低内存使用率
trust_remote_code=True,
enable_lora=False, # 禁用LoRA
)
# 使用提取的PIL图像
print("生成视频描述...")
prompt = "这个视频展示了什么内容?详细描述一下。"
# 使用generate而不是generate_videos (如果不存在generate_videos方法)
try:
# 尝试使用generate_videos
outputs = llm.generate_videos(prompt, videos=[pil_images], sampling_params=sampling_params)
print(outputs[0].outputs[0].text) # 打印模型输出
except AttributeError:
print("generate_videos方法不可用尝试使用普通generate方法...")
# 如果不支持generate_videos使用普通的generate
outputs = llm.generate([prompt], sampling_params=sampling_params)
print(outputs[0].outputs[0].text) # 打印模型输出
except Exception as e:
print(f"模型加载或推理过程中出错: {e}")
import traceback
traceback.print_exc()
"""

267
code/video_to_text.py Normal file
View File

@ -0,0 +1,267 @@
import os
import subprocess
import whisper
import torch
from moviepy import VideoFileClip
import warnings
warnings.filterwarnings("ignore")
class VideoToTextConverter:
def __init__(self, model_size="base"):
"""
初始化视频转文字转换器
Args:
model_size: Whisper模型大小 ("tiny", "base", "small", "medium", "large")
"""
self.model_size = model_size
self.model = None
self.load_model()
def load_model(self):
"""加载Whisper模型"""
try:
print(f"正在加载Whisper {self.model_size} 模型...")
self.model = whisper.load_model(self.model_size)
print("模型加载成功!")
except Exception as e:
print(f"模型加载失败: {e}")
print("尝试使用CPU模式...")
self.model = whisper.load_model(self.model_size, device="cpu")
def extract_audio_moviepy(self, video_path, audio_path):
"""
使用moviepy提取音频
Args:
video_path: 视频文件路径
audio_path: 输出音频文件路径
"""
try:
print("正在使用moviepy提取音频...")
video = VideoFileClip(video_path)
audio = video.audio
audio.write_audiofile(audio_path, verbose=False, logger=None)
audio.close()
video.close()
print(f"音频提取成功: {audio_path}")
return True
except Exception as e:
print(f"moviepy提取音频失败: {e}")
return False
def extract_audio_ffmpeg(self, video_path, audio_path):
"""
使用ffmpeg提取音频
Args:
video_path: 视频文件路径
audio_path: 输出音频文件路径
"""
try:
print("正在使用ffmpeg提取音频...")
cmd = [
'ffmpeg', '-i', video_path,
'-vn', # 不包含视频
'-acodec', 'pcm_s16le', # 音频编码
'-ar', '16000', # 采样率
'-ac', '1', # 单声道
'-y', # 覆盖输出文件
audio_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"音频提取成功: {audio_path}")
return True
else:
print(f"ffmpeg错误: {result.stderr}")
return False
except Exception as e:
print(f"ffmpeg提取音频失败: {e}")
return False
def extract_audio(self, video_path, audio_path=None):
"""
提取视频中的音频
Args:
video_path: 视频文件路径
audio_path: 输出音频文件路径如果为None则自动生成
Returns:
audio_path: 成功提取的音频文件路径失败返回None
"""
if audio_path is None:
# 自动生成音频文件路径
video_dir = os.path.dirname(video_path)
video_name = os.path.splitext(os.path.basename(video_path))[0]
audio_path = os.path.join(video_dir, f"{video_name}_audio.wav")
# 确保输出目录存在
os.makedirs(os.path.dirname(audio_path), exist_ok=True)
# 首先尝试使用moviepy
if self.extract_audio_moviepy(video_path, audio_path):
return audio_path
# 如果moviepy失败尝试使用ffmpeg
if self.extract_audio_ffmpeg(video_path, audio_path):
return audio_path
print("所有音频提取方法都失败了")
return None
def transcribe_audio(self, audio_path, language="zh"):
"""
将音频转换为文字
Args:
audio_path: 音频文件路径
language: 语言代码 ("zh"中文, "en"英文, None自动检测)
Returns:
dict: 包含转录结果的字典
"""
if self.model is None:
print("模型未加载,无法进行转录")
return None
try:
print("正在进行语音识别...")
# 设置转录选项
options = {
"language": language if language != "auto" else None,
"task": "transcribe",
"fp16": torch.cuda.is_available() # 如果有GPU则使用半精度
}
result = self.model.transcribe(audio_path, **options)
print("语音识别完成!")
return result
except Exception as e:
print(f"语音识别失败: {e}")
return None
def video_to_text(self, video_path, output_dir=None, language="zh", save_audio=True):
"""
完整的视频转文字流程
Args:
video_path: 视频文件路径
output_dir: 输出目录如果为None则使用视频所在目录
language: 语言代码
save_audio: 是否保存提取的音频文件
Returns:
dict: 包含转录结果和文件路径的字典
"""
if output_dir is None:
output_dir = os.path.dirname(video_path)
video_name = os.path.splitext(os.path.basename(video_path))[0]
# 提取音频
audio_path = os.path.join(output_dir, f"{video_name}_audio.wav")
extracted_audio = self.extract_audio(video_path, audio_path)
if extracted_audio is None:
return {"success": False, "error": "音频提取失败"}
# 转录音频
transcription = self.transcribe_audio(extracted_audio, language)
if transcription is None:
return {"success": False, "error": "语音识别失败"}
# 保存转录结果
text_path = os.path.join(output_dir, f"{video_name}_transcription.txt")
with open(text_path, 'w', encoding='utf-8') as f:
f.write(transcription["text"])
# 保存详细结果(包含时间戳)
detailed_path = os.path.join(output_dir, f"{video_name}_detailed.txt")
with open(detailed_path, 'w', encoding='utf-8') as f:
for segment in transcription["segments"]:
start_time = segment["start"]
end_time = segment["end"]
text = segment["text"]
f.write(f"[{start_time:.2f}s - {end_time:.2f}s]: {text}\n")
# 如果不需要保存音频文件,则删除
if not save_audio and os.path.exists(extracted_audio):
os.remove(extracted_audio)
extracted_audio = None
result = {
"success": True,
"text": transcription["text"],
"segments": transcription["segments"],
"language": transcription["language"],
"audio_path": extracted_audio,
"text_path": text_path,
"detailed_path": detailed_path
}
return result
def print_result(self, result):
"""打印转录结果"""
if not result["success"]:
print(f"转录失败: {result['error']}")
return
print("\n" + "="*50)
print("视频转文字结果")
print("="*50)
print(f"检测到的语言: {result['language']}")
print(f"文本文件: {result['text_path']}")
print(f"详细文件: {result['detailed_path']}")
if result['audio_path']:
print(f"音频文件: {result['audio_path']}")
print("\n完整文本:")
print("-" * 30)
print(result["text"])
print("\n分段文本 (前5段):")
print("-" * 30)
for i, segment in enumerate(result["segments"][:5]):
start_time = segment["start"]
end_time = segment["end"]
text = segment["text"]
print(f"[{start_time:.2f}s - {end_time:.2f}s]: {text}")
if len(result["segments"]) > 5:
print(f"... 还有 {len(result['segments']) - 5} 个分段")
def main():
"""示例用法"""
# 初始化转换器
converter = VideoToTextConverter(model_size="base")
# 视频文件路径
video_path = "/root/autodl-tmp/hot_video_analyse/source/sample_demo_1.mp4"
output_dir = "/root/autodl-tmp/hot_video_analyse/source/transcription"
# 确保输出目录存在
os.makedirs(output_dir, exist_ok=True)
# 转换视频为文字
result = converter.video_to_text(
video_path=video_path,
output_dir=output_dir,
language="auto", # 中文,也可以用"en"英文或"auto"自动检测
save_audio=True
)
# 打印结果
converter.print_result(result)
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Some files were not shown because too many files have changed in this diff Show More