Files
wehub-resource-sync 41b710f9c7
CI / Frontend checks (push) Failing after 0s
CI / Backend tests (push) Failing after 1s
I18n Documentation Sync / sync-docs (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:28:40 +08:00

295 lines
10 KiB
Python

"""
文本处理工具
"""
import json
import logging
import re
from typing import List, Dict, Any, Optional
from pathlib import Path
# 修复导入问题
try:
from ..core.shared_config import CHUNK_SIZE
except ImportError:
# 如果相对导入失败,尝试绝对导入
import sys
from pathlib import Path
backend_path = Path(__file__).parent.parent
if str(backend_path) not in sys.path:
sys.path.insert(0, str(backend_path))
from core.shared_config import CHUNK_SIZE
import pysrt
logger = logging.getLogger(__name__)
class TextProcessor:
"""文本处理工具类"""
@staticmethod
def chunk_text(text: str, chunk_size: int = CHUNK_SIZE) -> List[str]:
"""
将长文本按指定大小分块
Args:
text: 输入文本
chunk_size: 分块大小
Returns:
文本块列表
"""
if len(text) <= chunk_size:
return [text]
chunks = []
current_chunk = ""
# 按段落分割
paragraphs = text.split('\n')
for paragraph in paragraphs:
# 如果当前块加上新段落不超过限制,则添加
if len(current_chunk) + len(paragraph) + 1 <= chunk_size:
current_chunk += paragraph + '\n'
else:
# 如果当前块不为空,保存它
if current_chunk.strip():
chunks.append(current_chunk.strip())
# 如果单个段落就超过限制,需要进一步分割
if len(paragraph) > chunk_size:
# 按句子分割
sentences = re.split(r'[。!?]', paragraph)
temp_chunk = ""
for sentence in sentences:
if len(temp_chunk) + len(sentence) + 1 <= chunk_size:
temp_chunk += sentence + "。"
else:
if temp_chunk:
chunks.append(temp_chunk.strip())
temp_chunk = sentence + "。"
current_chunk = temp_chunk
else:
current_chunk = paragraph + '\n'
# 添加最后一个块
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks
def chunk_srt_data(self, srt_data: List[Dict], interval_minutes: int = 30, pause_threshold_ms: int = 1000) -> List[Dict]:
"""
根据停顿时间,将SRT数据切分为大约相等时间长度的块。
这可以避免在对话中间断开。
Args:
srt_data: SRT数据列表
interval_minutes: 每个块的目标时间长度(分钟)
pause_threshold_ms: 识别为停顿的最小毫秒数
Returns:
结构化的块列表,其中的 srt_entries 不包含临时处理字段。
"""
if not srt_data:
return []
# 创建一个带有秒数的新列表,而不是修改原始数据
srt_data_with_seconds = []
for sub in srt_data:
entry = sub.copy()
entry['start_seconds'] = self.time_to_seconds(sub['start_time'])
entry['end_seconds'] = self.time_to_seconds(sub['end_time'])
srt_data_with_seconds.append(entry)
interval_seconds = interval_minutes * 60
chunks = []
current_chunk_start_index = 0
chunk_index = 0
last_cut_time = 0
while current_chunk_start_index < len(srt_data_with_seconds):
target_cut_time = last_cut_time + interval_seconds
# 寻找接近目标时间的最佳切分点
best_cut_index = -1
# 查找从当前块开始后的 90% 到 110% 目标时间内的一个停顿
search_start_index = current_chunk_start_index
while search_start_index < len(srt_data_with_seconds) and srt_data_with_seconds[search_start_index]['start_seconds'] < target_cut_time * 0.9:
search_start_index += 1
# 从搜索起点开始寻找超过阈值的停顿
for i in range(search_start_index, len(srt_data_with_seconds) - 1):
current_sub = srt_data_with_seconds[i]
next_sub = srt_data_with_seconds[i+1]
# 如果我们已经超出了目标时间的110%,就停止搜索
if current_sub['start_seconds'] > target_cut_time * 1.1:
break
# 计算两个字幕条目之间的停顿时间
pause = next_sub['start_seconds'] - current_sub['end_seconds']
if pause * 1000 >= pause_threshold_ms:
best_cut_index = i + 1 # 在停顿后切分
break
# 如果没有找到合适的停顿点,就在目标时间点强制切分
if best_cut_index == -1:
# 寻找最接近目标时间的字幕条目
i = current_chunk_start_index
while i < len(srt_data_with_seconds) and srt_data_with_seconds[i]['start_seconds'] < target_cut_time:
i += 1
best_cut_index = i if i < len(srt_data_with_seconds) else len(srt_data_with_seconds)
# 如果切分点无效或过小,则将所有剩余部分作为一个块
if best_cut_index <= current_chunk_start_index:
best_cut_index = len(srt_data_with_seconds)
# 创建块
chunk_entries_with_seconds = srt_data_with_seconds[current_chunk_start_index:best_cut_index]
if not chunk_entries_with_seconds:
break
# 移除临时字段,得到干净的srt_entries
chunk_entries = []
for entry in chunk_entries_with_seconds:
clean_entry = entry.copy()
del clean_entry['start_seconds']
del clean_entry['end_seconds']
chunk_entries.append(clean_entry)
start_time = chunk_entries[0]['start_time']
end_time = chunk_entries[-1]['end_time']
text = " ".join([entry['text'] for entry in chunk_entries])
chunks.append({
"chunk_index": chunk_index,
"text": text,
"start_time": start_time,
"end_time": end_time,
"srt_entries": chunk_entries
})
chunk_index += 1
last_cut_time = chunk_entries_with_seconds[-1]['end_seconds']
current_chunk_start_index = best_cut_index
return chunks
@staticmethod
def parse_srt(srt_path: Path) -> List[Dict]:
"""
解析SRT字幕文件
Args:
srt_path: SRT文件路径
Returns:
字幕数据列表,每个元素包含时间戳和文本
"""
if not srt_path.exists():
logger.error(f"SRT文件不存在: {srt_path}")
return []
if srt_path.stat().st_size == 0:
logger.warning(f"SRT文件为空: {srt_path}")
return []
try:
try:
subs = pysrt.open(str(srt_path), encoding='utf-8')
except UnicodeDecodeError:
logger.warning("UTF-8解码失败,尝试使用 utf-8-sig...")
subs = pysrt.open(str(srt_path), encoding='utf-8-sig')
subtitles = []
for sub in subs:
subtitles.append({
'start_time': str(sub.start),
'end_time': str(sub.end),
'text': sub.text.strip(),
'index': sub.index
})
if not subtitles:
logger.warning(f"成功打开SRT文件但未能解析出任何字幕内容: {srt_path}")
return subtitles
except Exception as e:
logger.error(f"使用pysrt解析SRT文件'{srt_path}'时发生未知错误: {e}", exc_info=True)
return []
@staticmethod
def extract_text_by_time_range(text: str, srt_data: List[Dict],
start_time: str, end_time: str) -> str:
"""
根据时间范围从文本中提取对应内容
Args:
text: 完整文本
srt_data: SRT字幕数据
start_time: 开始时间 (格式: "00:01:25")
end_time: 结束时间 (格式: "00:02:53")
Returns:
对应时间范围的文本内容
"""
# 找到时间范围内的字幕
target_subtitles = []
for sub in srt_data:
sub_start = sub['start_time']
sub_end = sub['end_time']
# 检查时间重叠
if (sub_start <= end_time and sub_end >= start_time):
target_subtitles.append(sub)
# 提取对应的文本
extracted_text = ""
for sub in target_subtitles:
extracted_text += sub['text'] + " "
return extracted_text.strip()
@staticmethod
def time_to_seconds(time_str: str) -> float:
"""
将SRT时间字符串(HH:MM:SS,mmm)转换为秒数
Args:
time_str: 时间字符串
Returns:
秒数
"""
time_str = time_str.replace(',', '.')
parts = time_str.split(':')
if len(parts) == 3:
h = int(parts[0])
m = int(parts[1])
s_parts = parts[2].split('.')
s = int(s_parts[0])
ms = int(s_parts[1]) if len(s_parts) > 1 else 0
return h * 3600 + m * 60 + s + ms / 1000.0
raise ValueError(f"无效的时间格式: {time_str}")
@staticmethod
def seconds_to_time(seconds: float) -> str:
"""
将秒数转换为时间字符串
Args:
seconds: 秒数
Returns:
时间字符串 (格式: "00:01:25")
"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
return f"{hours:02d}:{minutes:02d}:{secs:02d}"